C语言-三子棋(井字棋)

三子棋是一种民间传统游戏,又叫九宫棋、圈圈叉叉棋、一条龙、井字棋等。游戏分为双方对战,双方依次在9宫格棋盘上摆放棋子,率先将自己的三个棋子走成一条线就视为胜利,而对方就算输了,但是三子棋在很多时候会出现和棋的局面。

我们按照项目需求拆分成以下三个文件:

  • main.c 测试游戏的逻辑
  • game.h 关于游戏相关的函数声明、符号声明以及头文件
  • game.c 游戏相关函数的实现

可以参考上一篇游戏文章“猜数字”,为了实现游戏可以不断进行下一轮,我们采用do……while循环,与上节思想类似,当input输入为1的时候,while进入下一轮循环(即下一轮游戏)。

首先定义一个简易的菜单函数menu,当输入为1的时候,运行游戏,当输入为其他值时,重新选择并给出错误提示。当输入为0时,退出游戏。代码逻辑和上一篇游戏文章“猜数字”相同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/*******main.c*********/
#define _CRT_SECURE_NO_WARNINGS
#include "game.h"

int main() {
int input = 0;

do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
printf("Play game!\n");
break;
case 0:
printf("Exit game!\n");
break;
default:
printf("Error!\n");
break;
}
} while(input);
return 0;
}
/**********************/

/*******game.h*********/
#pragma once
#include <stdio.h>

void menu();

/**********************/

/*******game.c*********/
#include "game.h"

void menu()
{
printf("**************************\n");
printf("****** 1.play ******\n");
printf("****** 0.exit ******\n");
printf("**************************\n");
}
/**********************/

image-20240204100737340

实现逻辑是当input输入不为0时,对于while判断来说都为真,进行下一轮循环,由此实现输入1进行下一轮游戏和输入其他数字重新选择。

经测试逻辑符合所需,主体代码设计完成,接下来设计具体游戏部分。

分析井字棋可得,其为 3*3 的棋盘,由此我们可以创建一个3*3的二维数组来存放这个数据。

1
2
3
4
5
6
7
8
9
// 棋盘行列
#define ROW 3
#define COL 3

// 游戏主体
void game()
{
char board[ROW][COL];
}

对于棋盘来说,一开始都是空的,那么我们是不是要将棋盘全都初始化为' '呢?接下来再定义一个InitBoard函数初始化棋盘。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 初始化棋盘
void InitBoard(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
board[i][j] = ' ';
}
}
}

初始化棋盘之后,如果我们想打印出棋盘,是不是还要定义一个函数用于打印棋盘呢?接下来定义一个DisplayBoard函数用于打印棋盘。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void DisplayBoard(char board[ROW][COL], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf(" %c ", board[i][j]);
if (j < col - 1)
printf("|");
}
printf("\n");
if (i < row - 1)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf("---");
if (j < col - 1)
printf("|");
}
printf("\n");
}
}
}

函数通过两层嵌套的 for 循环遍历棋盘的每一个格子。外层循环遍历每一行,内层循环遍历每一行中的每一个格子。

在内层循环中,使用 printf(" %c ", board[i][j]); 打印出当前格子中的字符。如果当前不是该行的最后一个格子,则后面会打印一个竖线 "|" 作为列之间的分隔符。

内层循环结束后,如果当前行不是最后一行,那么会打印一个分隔行,由连续的 "---""|" 组成,以模拟棋盘的横向分隔线。

image-20240204110847763

这样子打印棋盘的好处是,当ROWCOL改变成其他数字时,也能正确打印出想要的棋盘,例如五子棋。

image-20240204110914244

接下来就是游戏具体下棋步骤的实现,首先定义一个PlayerMove函数用于实现玩家下棋步骤,接着再定义一个ComputerMove函数实现电脑下棋步骤。

1
2
3
4
5
6
7
while (1)
{
// 玩家下棋
PlayerMove(board, ROW, COL);
// 电脑下棋
ComputerMove(board, ROW, COL);
}

首先实现玩家下棋,定义了一个名为 PlayerMove 的函数,这个函数接受一个二维字符数组 board 作为棋盘,以及两个整数 rowcol 来表示棋盘的实际行数和列数。

在函数内部,定义了两个整数变量 xy,用于存储玩家输入的坐标。

判断坐标合法性:

  • 检查玩家输入的坐标是否在棋盘范围内,即 xy 是否都在 [1, row][1, col] 范围内。
  • 如果坐标合法,再检查该坐标对应的棋盘位置是否为空(用 ' ' 表示)。如果位置为空,则将该位置标记为玩家的棋子('*'),并退出循环。
  • 如果坐标已被占用,即该位置不为空,则提示“坐标被占用,请重新输入”,并要求玩家重新输入坐标。
  • 如果坐标非法(即不在棋盘范围内),则提示“坐标非法,请重新输入”,同样要求玩家重新输入坐标。

这个过程会一直循环,直到玩家输入了一个合法且未被占用的坐标为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 玩家下棋
void PlayerMove(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");
}
}
}

image-20240214113613428

接下来写电脑走的逻辑,首先定义void ComputerMove(char board[ROW][COL], int row, int col); 函数,用于电脑走。我们让电脑随机走,所以定义两个变量x y

1
2
3
4
5
6
7
8
9
// main.c
srand((unsigned int)time(NULL));
//game.h
#include <stdlib.h>
#include <time.h>
// game.c
printf("电脑走:>\n");
int x = rand() % row;
int y = rand() % col;

rand函数生成的随机数%模上rowcol他的范围就是0~2,不需要再判断坐标的合法性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 电脑下棋
void ComputerMove(char board[ROW][COL], int row, int col)
{
printf("电脑走:>\n");
while (1)
{
int x = rand() % row;
int y = rand() % col;
if (board[x][y] = ' ')
{
board[x][y] = '#';
break;
}
}
}

电脑走的逻辑很容易实现,因为是随机走,所以只需要判断是否被占用即可。

image-20240214115417379

接下来是判断游戏结束,那么在游戏进行的过程中会有以下四种状态:

  • 玩家赢
  • 电脑赢
  • 平局
  • 游戏继续

也就是说当玩家走完和电脑走完,会有以上四种状态可能,那么我们如何判断游戏是否结束呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 游戏主体
void game()
{
// 存储数据
char board[ROW][COL];
// 初始化棋盘
InitBoard(board, ROW, COL);
// 打印棋盘
DisplayBoard(board, ROW, COL);
char ret = 0; // 接受游戏返回状态
while (1)
{
// 玩家下棋
PlayerMove(board, ROW, COL);
DisplayBoard(board, ROW, COL);

//判断游戏状态
ret = IsWin(board, ROW, COL);
if (ret != 'C')
break;

// 电脑下棋
ComputerMove(board, ROW, COL);
DisplayBoard(board, ROW, COL);
}
if (ret == '*')
{
printf("玩家赢!\n");
}
else if (ret == '#')
{
printf("电脑赢!\n");
}
else
{
printf("平局!\n");
}
DisplayBoard(board, ROW, COL);
}

将游戏主体函数game()稍作修改,判断IsWin的返回值ret是什么,如果是*则玩家赢,#则电脑赢,C则游戏继续,否则就为平局。

接下来就是IsWin()函数的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 判断游戏状态
char IsWin(char board[ROW][COL], int row, int col)
{
int i = 0;
// 判断三行是否有相等的
for (i = 0; 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];
}
// 判断平局
// 如果棋盘满了返回1,不满则返回0
int ret = IsFull(board, row, col);
if (ret == 1)
{
return 'Q';
}

// 游戏继续
return 'C';
}

它通过遍历每一行和每一列,判断是否存在三个连续的相同字符(’#’或’*’),如果有,则返回对应字符。

接下来,它再判断两条对角线是否存在三个相同字符,如果有,则返回对应字符。

最后,如果棋盘已满,即没有空位了,而且没有任何一方胜出,那么返回’Q’表示平局。

如果以上情况都不满足,那么游戏仍在继续,返回’C’表示继续。

以上判断条件仅适用于井字棋(即三子棋),若要适用于五子棋等多子棋应根据要求相应修改判断条件。

IsFull()函数用于判断棋盘是否填满:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 判断棋盘是否填满
int IsFull(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
if (board[i][j] == ' ')
{
return 0;
}
}
}
return 1;
}

如果填满则返回1,未满则返回0

测试一下游戏:

image-20240215210127424

image-20240215210650894

image-20240215211101701

有一说一,平局可比“玩家赢”或者“电脑赢”难打多了……

完整代码大约260行,已上传至GitHub,有机会可以试着自己思考一下五子棋的代码应该又是怎么样的呢?