三子棋
- 1. 前言
- 2. 准备工作
- 3. 使用二维数组存储下棋的数据
- 4. 初始化棋盘为全空格
- 5. 打印棋盘
- 6. 玩家下棋
- 7. 电脑下棋
- 8. 判断输赢
- 9. 效果展示
- 10. 完整代码
1. 前言
大家好,我是努力学习游泳的鱼,今天我们会用C语言实现三子棋。所谓三子棋,就是三行三列的棋盘,玩家可以和电脑下棋,率先连成三个的获胜。话不多说,我们开始吧。
2. 准备工作
我们可以在一个项目中创建三个文件,分别是:
test.c
,测试游戏的逻辑。game.c
,游戏的实现。game.h
,函数声明,符号的定义。
测试这个游戏时,我们玩一把肯定不过瘾,所以需要使用do while
循环,每次可以选择继续玩或者退出游戏。先把大致的框架搭出来。
#include <stdio.h>
void menu()
{
printf("****************************\n");
printf("******** 1. play ******\n");
printf("******** 0. exit ******\n");
printf("****************************\n");
}
int main()
{
int input = 0;
do
{
menu();
printf("请选择(1/0):>");
scanf("%d", &input);
switch (input)
{
case 1:
printf("三子棋\n");
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
当然,我们的游戏不可能只是打印“三子棋”这三个字这么简单,具体的实现我们会封装成一个函数,暂且取名为game
。
3. 使用二维数组存储下棋的数据
当我们下三子棋的时候,需要把下棋的数据存起来。由于三子棋的棋盘是3×3
的,我们就需要一个三行三列的数组来存储下棋的数据。char board[3][3] = { 0 };
4. 初始化棋盘为全空格
当我们还没开始下棋时,棋盘上应该啥都没有,但是真的是啥都没有吗?事实上,如果我们打印棋盘时,能打印出“没有棋子”的效果,数组里应该是全空格。所以,我们需要写一个函数,初始化棋盘为全空格。InitBoard(board, 3, 3);
这个函数的声明,我们会放在game.h
里。具体的实现,我们会放在game.c
里。以下的函数同理。该函数的声明:void InitBoard(char board[3][3], int row, int col);
(以下的函数均省略声明)。该函数的实现,只需遍历这个二维数组,全部赋值为空格。
void InitBoard(char board[3][3], int row, int col)
{
int i = 0;
for (; i < row; ++i)
{
int j = 0;
for (; j < col; ++j)
{
board[i][j] = ' ';
}
}
}
有没有发现,这个程序中,到处都要用到数组“三行三列”这个特点。如果我们想要改变这一点,比如改成五行五列,就需要改很多地方,非常麻烦。怎么解决这个问题呢?我们可以在game.h
里定义两个常量ROW
和COL
,这样每次只需要改变#define
后的值就行了。
#define ROW 3
#define COL 3
这样以上出现的所有3
都可以用ROW
和COL
替代了(此处省略)。注意:如果想使用game.h
里的符号,我们需要在game.c
和test.c
里引用这个头文件。引用自己写的头文件要用双引号。#include "game.h"
而对于其他头文件如stdio.h
,我们不需要在game.c
和test.c
里包含两次,只需在game.h
里包含就行了。
5. 打印棋盘
我们初始化棋盘后,会想要看一看棋盘长啥样,所以接下来写一个打印棋盘的函数DisplayBoard(board, ROW, COL);
如果你认为打印出数组的数据就行了,从而这样写:
void DisplayBoard(char board[ROW][COL], int row, int col)
{
int i = 0;
for (; i < row; ++i)
{
int j = 0;
for (; j < col; ++j)
{
printf("%c", board[i][j]);
}
printf("\n");
}
}
实际运行时,你会发现,打印了,但没完全打印。
事实上,此时打印的是一堆空格,非常难看。如果我们想打印得好看点,可以考虑加上一些横向和竖向的分割。比如我设想了这样一种打印的效果:
| |
---|---|---
| |
---|---|---
| |
假设把
| |
---|---|---
当成一组,总共需要打印3
组,为什么呢?因为有3
行。
每一组里面,又分为数据行和分割行。我们需要先打印数据行,再打印分割行。
| | // 数据行
---|---|--- // 分割行
所以一个简单的想法是这么写:
void DisplayBoard(char board[ROW][COL], int row, int col)
{
int i = 0;
for (; i < row; ++i)
{
// 打印数据
printf(" %c | %c | %c \n", board[i][0], board[i][1], board[i][2]);
// 打印分割的行
printf("---|---|---\n");
}
}
效果如下:
我们发现多打印了一行分割行,所以打印分割行时要加一条判断,不是最后一行才打印分割行。
if (i < row - 1)
printf("---|---|---\n");
这样子打印就好看多了。
但是这么写的话,就相当于,一行一定要打印三列,后面就没有改变的可能了。比如我把ROW
和COL
都改成5
,效果如下:
所以这种写法还是不够好。最好的写法是,再用一层循环控制列的打印。比如对于数据行:
| |
我们可以把
|
当成一组数据,打印三组,最后一组就不用加上|
了(同上一种写法用一个if
语句来控制)。
对于分割行
---|---|---
我们可以把
---|
当成一组数据,打印三组,最后一组就不用加上|
了(还是用if语句来控制)。
实现如下:
void DisplayBoard(char board[ROW][COL], int row, int col)
{
int i = 0;
for (; i < row; ++i)
{
// 打印数据
int j = 0;
for (; j < col; ++j)
{
printf(" %c ", board[i][j]);
if (j < col - 1)
printf("|");
}
printf("\n");
// 打印分割的行
if (i < row - 1)
{
for (j = 0; j < col; ++j)
{
printf("---");
if (j < col - 1)
printf("|");
}
printf("\n");
}
}
}
效果也没问题:
若把ROW
和COL
改成10
,打印出来的效果如下:
这样写出来的代码就比较通用,具有比较强的可维护性。
6. 玩家下棋
接下来写玩家下棋的函数。player_move(board, ROW, COL);
先让玩家输入坐标,若x
和y
都在1
到3
之间,则输入的坐标合法,在数组对应的位置是board[x-1][y-1]
,若该位置仍然是空格,则这个位置没有被下过,就把数组的这个元素改成*
。由于若玩家输入的坐标非法或者被占用时,需要重新输入,故需要一个循环。
void player_move(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("玩家下棋\n");
while (1)
{
printf("请输入坐标:>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
// 下棋
if (board[x - 1][y - 1] == ' ')
{
board[x - 1][y - 1] = '*';
break;
}
else
{
printf("该坐标被占用,请重新输入\n");
}
}
else
{
printf("坐标非法,请重新输入\n");
}
}
}
玩家下棋后再把棋盘打印一下DisplayBoard(board, ROW, COL);
效果如下:
7. 电脑下棋
玩家下完棋后就轮到电脑下棋computer_move(board, ROW, COL);
我们让电脑随机下棋,只需生成两个随机的坐标x
和y
,若该位置是空格,就下在这个位置,如果是空格,那就再次产生随机坐标,可见,这也需要一个循环来实现。
void computer_move(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("电脑下棋\n");
while (1)
{
x = rand() % row; // 0~2
y = rand() % col; // 0~2
if (board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}
使用rand
函数前需要使用srand
函数来设置随机数生成器的起点。我们需要给srand
函数传一个时间戳,这就要用到time
函数。srand((unsigned int)time(NULL));
使用rand
和srand
都需要引用头文件stdlib.h
,使用time
函数需要引用头文件time.h
。
我们用一个循环,就能实现玩家和电脑轮流下棋的效果。
while (1)
{
// 玩家下棋
player_move(board, ROW, COL);
DisplayBoard(board, ROW, COL);
// 电脑下棋
computer_move(board, ROW, COL); // 随机下棋
DisplayBoard(board, ROW, COL);
}
8. 判断输赢
什么时候游戏就结束了呢?如果玩家赢了,或者电脑赢了,或者平局,游戏就结束了,否则游戏继续。
我们来设计一个is_win
函数来判断棋局是上面四种状态的哪一种。我们这么设计is_win
函数的返回值:
- 玩家赢 -
'*'
- 电脑赢 -
'#'
- 平局 -
'Q'
- 继续 -
'C'
当棋局状态不是C
时说明游戏结束了,就跳出循环,接着根据不同情况打印不同的结果。
char ret = 0;
while (1)
{
// 玩家下棋
player_move(board, ROW, COL);
DisplayBoard(board, ROW, COL);
// 判断输赢
ret = is_win(board, ROW, COL);
if (ret != 'C')
{
break;
}
// 电脑下棋
computer_move(board, ROW, COL); // 随机下棋
DisplayBoard(board, ROW, COL);
ret = is_win(board, ROW, COL);
if (ret != 'C')
{
break;
}
}
if (ret == '*')
{
printf("玩家赢\n");
}
else if (ret == '#')
{
printf("电脑赢\n");
}
else
{
printf("平局\n");
}
如何实现is_win
函数呢?只需判断每行,每列,每条对角线是否有三个同样的棋子就行了。
先判断行(由于玩家赢和电脑赢都是返回对应的棋子——*
和#
,所以直接把对应的数组元素返回就行了。):
int i = 0;
for (; i < row; ++i)
{
if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][1] != ' ')
{
return board[i][1];
}
}
判断列同理:
for (i = 0; i < col; ++i)
{
if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[1][i] != ' ')
{
return board[1][i];
}
}
最后判断对角线:
if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[1][1] != ' ')
{
return board[1][1];
}
if (board[0][2] == board[1][1] && board[1][1] == board[2][0] && board[1][1] != ' ')
{
return board[1][1];
}
如果有人赢了,在前面的代码中就会返回。如果没人赢,再来判断是否平局。如果棋盘没有空格了,那就是平局。我们可以写一个is_full
函数来判断棋盘是否满了。如果满了就返回1
,没满就返回0
。
if (is_full(board, row, col) == 1)
{
return 'Q';
}
由于is_full
函数只是写给is_win
函数的,只需要在game.c
这个文件内使用,所以加上static
。具体的实现,只需要遍历数组,若发现空格,说明棋盘没满,就返回0
,否则返回1
。
static int is_full(char board[ROW][COL], int row, int col)
{
int i = 0;
for (; i < row; ++i)
{
int j = 0;
for (; j < col; ++j)
{
if (board[i][j] == ' ')
{
return 0; // 没有满
}
}
}
return 1; // 满了
}
而如果没人赢,也不是平局,则游戏继续,return 'C';
即可。
9. 效果展示
写到这,我们就把三子棋程序写完啦。接下来看看效果:
玩家赢:
电脑赢:
平局:
10. 完整代码
下面是完整的代码:
game.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ROW 3
#define COL 3
// 初始化棋盘
void InitBoard(char board[ROW][COL], int row, int col);
// 打印棋盘
void DisplayBoard(char board[ROW][COL], int row, int col);
// 玩家下棋
void player_move(char board[ROW][COL], int row, int col);
// 电脑下棋
void computer_move(char board[ROW][COL], int row, int col);
// 判断输赢
char is_win(char board[ROW][COL], int row, int col);
game.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
void InitBoard(char board[ROW][COL], int row, int col)
{
int i = 0;
for (; i < row; ++i)
{
int j = 0;
for (; j < col; ++j)
{
board[i][j] = ' ';
}
}
}
//void DisplayBoard(char board[ROW][COL], int row, int col)
//{
// int i = 0;
// for (; i < row; ++i)
// {
// int j = 0;
// for (; j < col; ++j)
// {
// printf("%c", board[i][j]);
// }
// printf("\n");
// }
//}
//void DisplayBoard(char board[ROW][COL], int row, int col)
//{
// int i = 0;
// for (; i < row; ++i)
// {
// // 打印数据
// printf(" %c | %c | %c \n", board[i][0], board[i][1], board[i][2]);
// // 打印分割的行
// if (i < row - 1)
// printf("---|---|---\n");
// }
//}
void DisplayBoard(char board[ROW][COL], int row, int col)
{
int i = 0;
for (; i < row; ++i)
{
// 打印数据
int j = 0;
for (; j < col; ++j)
{
printf(" %c ", board[i][j]);
if (j < col - 1)
printf("|");
}
printf("\n");
// 打印分割的行
if (i < row - 1)
{
for (j = 0; j < col; ++j)
{
printf("---");
if (j < col - 1)
printf("|");
}
printf("\n");
}
}
}
void player_move(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("玩家下棋\n");
while (1)
{
printf("请输入坐标:>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
// 下棋
if (board[x - 1][y - 1] == ' ')
{
board[x - 1][y - 1] = '*';
break;
}
else
{
printf("该坐标被占用,请重新输入\n");
}
}
else
{
printf("坐标非法,请重新输入\n");
}
}
}
void computer_move(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("电脑下棋\n");
while (1)
{
x = rand() % row; // 0~2
y = rand() % col; // 0~2
if (board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}
static int is_full(char board[ROW][COL], int row, int col)
{
int i = 0;
for (; i < row; ++i)
{
int j = 0;
for (; j < col; ++j)
{
if (board[i][j] == ' ')
{
return 0; // 没有满
}
}
}
return 1; // 满了
}
char is_win(char board[ROW][COL], int row, int col)
{
int i = 0;
// 判断行
for (; i < row; ++i)
{
if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][1] != ' ')
{
return board[i][1];
}
}
// 判断列
for (i = 0; i < col; ++i)
{
if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[1][i] != ' ')
{
return board[1][i];
}
}
// 对角线
if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[1][1] != ' ')
{
return board[1][1];
}
if (board[0][2] == board[1][1] && board[1][1] == board[2][0] && board[1][1] != ' ')
{
return board[1][1];
}
// 判断平局
if (is_full(board, row, col) == 1)
{
return 'Q';
}
// 继续
return 'C';
}
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
void menu()
{
printf("****************************\n");
printf("******** 1. play ******\n");
printf("******** 0. exit ******\n");
printf("****************************\n");
}
void game()
{
// 三子棋小游戏的具体实现
char ret = 0;
// 存放下棋的数据
char board[ROW][COL] = { 0 };
// 初始化棋盘为全空格
InitBoard(board, ROW, COL);
// 打印棋盘
DisplayBoard(board, ROW, COL);
while (1)
{
// 玩家下棋
player_move(board, ROW, COL);
DisplayBoard(board, ROW, COL);
// 判断输赢
ret = is_win(board, ROW, COL);
if (ret != 'C')
{
break;
}
// 电脑下棋
computer_move(board, ROW, COL); // 随机下棋
DisplayBoard(board, ROW, COL);
ret = is_win(board, ROW, COL);
if (ret != 'C')
{
break;
}
}
if (ret == '*')
{
printf("玩家赢\n");
}
else if (ret == '#')
{
printf("电脑赢\n");
}
else
{
printf("平局\n");
}
//DisplayBoard(board, ROW, COL);
}
//
// 什么时候,游戏就结束了
// 玩家赢 - '*'
// 电脑赢 - '#'
// 平局 - 'Q'
// 继续 - 'C'
//
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请选择(1/0):>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}