井字棋游戏

什么是井字棋

  1. 在 3 * 3 的棋盘中,双方轮流放子,当某一方的三个子连成一线(行,列,对角)时,该方获胜;
  2. 如果棋盘已放满但并未出现三个子连成一线的情况,则打成平手;

游戏实现的思路

0. 逻辑分析
  1. 首先提供一个选择菜单,有进入游戏和退出游戏选项;
  2. 显示棋盘,且每落子一次,都需要棋盘当前状态显示出来;
  3. 游戏开始,双方轮流落子,落子后判断输赢;
  4. 当一方获胜后,再次提供选择:退出游戏 or 再玩一次(我输了,我不服嘛,不过和非人工智能的电脑玩,怎么可能会输);
1. main 函数

在 main 函数中写出程序的整体逻辑,然后再具体实现:

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
49
50
51
52
#include <stdio.h>  //输入输出
#include <stdlib.h> //电脑随机落子
#include <time.h> //电脑落子后休眠 1 秒
#include <unistd.h> //sleep 函数

//定义棋盘的行和列
//伪需求,不能自定行和列,因为逻辑已经写死了
//只是为了便于引用(维护)
#define ROW 3
#define COL 3

/*没有工程化,这么写是不是脱裤子放屁?*/
//显示菜单
void display_menu();
//游戏开始
void play_game(char arr[ROW][COL], int row, int col);

int main()
{
//通常,在玩井字棋游戏时,红方用 X 表示,蓝方用 O(字母,非 0) 表示,所以需要 char 数组来存储数据
char arr[ROW][COL] = {0};
//接收用户输入
int input = 0;

//菜单至少会显示一次,do while 正合适
do
{
//显示菜单
display_menu();
//提示用户输入
printf("请输入选择:>");
//接收用户输入
scanf("%d", &input);

switch (input)
{
case 1: //输入 1 表示开始游戏
play_game(arr, ROW, COL);
break;
case 0: //输入 0 表示退出游戏
printf("退出游戏!\n");
break;
default: //非 0 非 1 不正确,重新输入
printf("输入有误,请重新输入!\n");
break;
}

} while (input); // 0 退出,非 0 为真,刚刚好

return 0;
}

2. 菜单

通常,提供菜单会让用户感到尊重和愉悦,多种选择也意味着服务的精细,废话不多说,上代码:

1
2
3
4
5
6
7
8
void display_menu()
{
printf("*************************\n");
printf("******** 1. play ********\n");
printf("******** 0. exit ********\n");
printf("*************************\n");
}

菜单简陋,聊胜于无;

3. 开始游戏
1
2
3
4
5
6
7
void play_game(char arr[ROW][COL], int row, int col)
{
//初始化棋盘
init_board(arr, ROW, COL);
//显示棋盘
display_board(arr, ROW, COL);
}

首先是初始化棋盘,然后显示棋盘,有棋盘了才能落子嘛;

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

游戏开始时,棋盘应该为空,所以将 char 数组的内容重新初始化;

5. 打印并显示棋盘

根据行和列生成棋盘,划分如图:

划分

如果将每行要显示的元素和分隔线看作一个整体的话,棋盘可以按照行和列完美分割,只是最后一行和最后一列没有分隔线,实现如下:

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
void display_board(char arr[ROW][COL], int row, int col)
{
for (int i = 0; i < row; i++)
{
//打印坐标 - 棋盘内容
for (int j = 0; j < col; j++)
{
printf(" %c ", arr[i][j]);
//如果不是每行的最后一个元素,则打印列分隔线
if (j < col - 1)
printf("|");
}
printf("\n");

//打印分隔符
//如果不是最后一行,则打印行分隔线
if (i < row - 1)
{
for (int j = 0; j < col; j++)
{
printf("---");
//如果不是每行的最后一个元素,则打印列分隔线
if (j < col - 1)
printf("|");
}
printf("\n");
}
}
}

至此,已经可以显示一个空棋盘了,测试一下:

测试空棋盘

空棋盘正常显示,其它逻辑也无误;

棋盘准备好了,接下来就该主角儿登场了;

6. 玩家落子 / 电脑落子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void play_game(char arr[ROW][COL], int row, int col)
{
//电脑落子随机数种子,放在 main 函数中会不会更好
srand((unsigned int)time(NULL));

init_board(arr, ROW, COL);
display_board(arr, ROW, COL);

/**
* 用户标识为 'X'
* 电脑标识为 'O'
*/
while (1)
{
//玩家落子
player_move(arr, ROW, COL);
//电脑落子
computer_move(arr, ROW, COL);
}

首先是用户输入,接收用户输入后判断,规则如下:

  1. 输入是否越界,越界则重新输入;
  2. 输入的坐标是否被占用(已经落子),占用则重新输入;
  3. 以上两种情况都不存在,则将用户标识(X)放入数组中坐标对应的位置;
  4. 然后显示棋盘;
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
void player_move(char arr[ROW][COL], int row, int col)
{
while (1)
{
int x = 0;
int y = 0;
printf("请输入要落子的坐标:>");
scanf("%d%d", &x, &y);
printf("\n");

//判断输入越界
if (x <= 0 || x > row || y <= 0 || y > col)
{
printf("坐标非法,请重新输入!\n");
display_board(arr, ROW, COL);
printf("\n");
}
//判断坐标占用
else if (arr[x - 1][y - 1] != ' ')
{
printf("坐标已被占用,请重新输入!\n");
display_board(arr, ROW, COL);
printf("\n");
}
else
{
//存放用户标识 'X'
arr[x - 1][y - 1] = 'X';
display_board(arr, ROW, COL);
printf("\n");
break;
}
}
}

接下来是电脑落子,生成随机坐标,然后判断,规则如下:

  1. 坐标是否被占用,占用则重新生成随机数;
  2. 坐标没有被占用,则将电脑标识(O)存入数组中坐标对应的位置;
  3. 然后显示棋盘;
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 computer_move(char arr[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("电脑落子...\n");
printf("\n");

while (1)
{
// m % n = 0 ~ n - 1
//以 3 为例,9 % 3 = 0,10 % 3 = 1,11 % 3 = 2,余数永远是 0 ~ (n - 1)
x = rand() % row;
y = rand() % col;
//为空说明坐标没有被占用(没有落子),则将电脑标识放入数组中坐标对应的位置
if (arr[x][y] == ' ')
{
//存放电脑标识 'O'
arr[x][y] = 'O';
//休眠 1 秒,表示电脑在思考(🌹🐔)
sleep(1);
display_board(arr, ROW, COL);
printf("\n");
break;
}
}
}

再来测试一下:

再来测试一下

对输入越界的判断正常,对坐标占用的判断正常,唯独在用户已经胜出的时候没有终止程序,接下来就需要判断胜负了;

7. 棋盘状态

胜负判断的规则很简单:

  1. 任意行、列、对角线的三个字符相同,则该字符表示的玩家胜出;
  2. 棋盘放满但没有玩家胜出,则平局;
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
49
50
51
52
53
54
55
//这个函数就是不能改行和列的原因
char is_win(char arr[ROW][COL], int row, int col)
{
/**
* 'X' 为玩家输入
* 'O' 为电脑输入
* 'Q' 为平局
* 'C' 为继续
*/
char rst = 0;

//如果同一行的所有字符全部相同,返回任意单元的字符
for (int i = 0; i < row; i++)
{
if (arr[i][0] == arr[i][1] && arr[i][1] == arr[i][2] && arr[i][0] != ' ')
{
rst = arr[i][0];
}
}

//如果同一列的所有字符全部相同,返回任意单元的字符
for (int i = 0; i < col; i++)
{
if (arr[0][i] == arr[1][i] && arr[1][i] == arr[2][i] && arr[0][i] != ' ')
{
rst = arr[0][i];
}
}

//左上至右下的对角线
if (arr[0][0] == arr[1][1] && arr[1][1] == arr[2][2] && arr[2][2] != ' ')
{
rst = arr[1][1];
}

//右上至左下的对角线
if (arr[0][2] == arr[1][1] && arr[1][1] == arr[2][0] && arr[2][0] != ' ')
{
rst = arr[1][1];
}

//棋盘未满且没有序列字符相同,返回继续
if (is_board_full(arr, row, col) == 1 && rst == 0)
{
rst = 'C';
}

//棋盘已满且没有序列字符相同,返回平局
if (is_board_full(arr, row, col) == 0 && rst == 0)
{
rst = 'Q';
}

return rst;
}

判断棋盘是否已满:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int is_board_full(char arr[ROW][COL], int row, int col)
{
/**
* 返回 1 棋盘未满
* 返回 0 棋盘已满
*/
int rst = 0;
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
if (arr[i][j] == ' ')
rst = 1;
}
}

return rst;
}
8. 定胜负

有了棋盘状态,现在可以定胜负了:

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
49
50
51
52
53
54
void play_game(char arr[ROW][COL], int row, int col)
{
//棋盘状态
char rst = 0;
//电脑落子随机数种子
srand((unsigned int)time(NULL));

init_board(arr, ROW, COL);
display_board(arr, ROW, COL);

/**
* 'X' 为玩家输入
* 'O' 为电脑输入
* 'Q' 为平局
* 'C' 为继续
*/
while (1)
{
player_move(arr, ROW, COL);
rst = is_win(arr, ROW, COL);
//用户落子后,棋盘未满则继续
if (rst != 'C')
{
break;
}

computer_move(arr, ROW, COL);
rst = is_win(arr, ROW, COL);
//电脑落子后,棋盘未满则继续
if (rst != 'C')
{
break;
}
}

//执行到此处表示上面的循环终止了;
//意味着返回的字符不是 'C';
//则说明找到相同的字符或棋盘满了
if (rst == 'X')
{
printf("玩家赢!\n");
printf("\n");
}
else if (rst == 'O')
{
printf("电脑赢!\n");
printf("\n");
}
else
{
printf("平局!\n");
printf("\n");
}
}

测试一下:

最终测试

9. 完整代码
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// #include <unistd.h>

//定义棋盘的行和列
#define ROW 3
#define COL 3

//显示菜单
void display_menu();
//初始化棋盘内容
void init_board(char arr[ROW][COL], int row, int col);
//生成并显示棋盘
void display_board(char arr[ROW][COL], int row, int col);
//游戏开始
void play_game(char arr[ROW][COL], int row, int col);
//玩家落子
void player_move(char arr[ROW][COL], int row, int col);
//电脑落子
void computer_move(char arr[ROW][COL], int row, int col);
//返回棋盘状态
char is_win(char arr[ROW][COL], int row, int col);
//判断棋盘是否放满
int is_board_full(char arr[ROW][COL], int row, int col);

int main()
{
//井字棋游戏
char arr[ROW][COL] = {0};
int input = 0;

do
{
display_menu();
printf("请输入选择:>");
scanf("%d", &input);

switch (input)
{
case 1:
play_game(arr, ROW, COL);
break;
case 0:
printf("退出游戏!\n");
break;
default:
printf("输入有误,请重新输入!\n");
break;
}

} while (input);

return 0;
}

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

void init_board(char arr[ROW][COL], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
arr[i][j] = ' ';
}
}
}

void display_board(char arr[ROW][COL], int row, int col)
{
for (int i = 0; i < row; i++)
{
//打印坐标
for (int j = 0; j < col; j++)
{
printf(" %c ", arr[i][j]);
if (j < col - 1)
printf("|");
}
printf("\n");

//打印分隔符
if (i < row - 1)
{
for (int j = 0; j < col; j++)
{
printf("---");
if (j < col - 1)
printf("|");
}
printf("\n");
}
}
}

void play_game(char arr[ROW][COL], int row, int col)
{
//棋盘状态
char rst = 0;
//电脑落子随机数种子
srand((unsigned int)time(NULL));

init_board(arr, ROW, COL);
display_board(arr, ROW, COL);

/**
* 'X' 为玩家输入
* 'O' 为电脑输入
* 'Q' 为平局
* 'C' 为继续
*/
while (1)
{
player_move(arr, ROW, COL);
rst = is_win(arr, ROW, COL);
if (rst != 'C')
{
break;
}

computer_move(arr, ROW, COL);
rst = is_win(arr, ROW, COL);
if (rst != 'C')
{
break;
}
}

if (rst == 'X')
{
printf("玩家赢!\n");
printf("\n");
}
else if (rst == 'O')
{
printf("电脑赢!\n");
printf("\n");
}
else if (rst == 'Q')
{
printf("平局!\n");
printf("\n");
}
}

void player_move(char arr[ROW][COL], int row, int col)
{
while (1)
{
int x = 0;
int y = 0;
printf("请输入要落子的坐标:>");
scanf("%d%d", &x, &y);
printf("\n");

if (x <= 0 || x > row || y <= 0 || y > col)
{
printf("坐标非法,请重新输入!\n");
display_board(arr, ROW, COL);
printf("\n");
}
else if (arr[x - 1][y - 1] != ' ')
{
printf("坐标已被占用,请重新输入!\n");
display_board(arr, ROW, COL);
printf("\n");
}
else
{
arr[x - 1][y - 1] = 'X';
display_board(arr, ROW, COL);
printf("\n");
break;
}
}
}

void computer_move(char arr[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("电脑落子...\n");
printf("\n");

while (1)
{
x = rand() % row;
y = rand() % col;
if (arr[x][y] == ' ')
{
arr[x][y] = 'O';
sleep(1);
display_board(arr, ROW, COL);
printf("\n");
break;
}
}
}

char is_win(char arr[ROW][COL], int row, int col)
{
/**
* 'X' 为玩家输入
* 'O' 为电脑输入
* 'Q' 为平局
* 'C' 为继续
*/
char rst = 0;

//如果同一行的所有单元字符全部相同,返回任意单元的字符
for (int i = 0; i < row; i++)
{
if (arr[i][0] == arr[i][1] && arr[i][1] == arr[i][2] && arr[i][0] != ' ')
{
rst = arr[i][0];
}
}

//如果同一列的所有单元字符全部相同,返回任意单元的字符
for (int i = 0; i < col; i++)
{
if (arr[0][i] == arr[1][i] && arr[1][i] == arr[2][i] && arr[0][i] != ' ')
{
rst = arr[0][i];
}
}

//左上至右下的对角线
if (arr[0][0] == arr[1][1] && arr[1][1] == arr[2][2] && arr[2][2] != ' ')
{
rst = arr[1][1];
}

//右上至左下的对角线
if (arr[0][2] == arr[1][1] && arr[1][1] == arr[2][0] && arr[2][0] != ' ')
{
rst = arr[1][1];
}

//棋盘未满且没有序列字符相同,返回继续
if (is_board_full(arr, row, col) == 1 && rst == 0)
{
rst = 'C';
}

//棋盘已满且没有序列字符相同,返回平局
if (is_board_full(arr, row, col) == 0 && rst == 0)
{
rst = 'Q';
}

return rst;
}

int is_board_full(char arr[ROW][COL], int row, int col)
{
/**
* 返回 1 棋盘未满
* 返回 0 棋盘已满
*/
int rst = 0;
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
if (arr[i][j] == ' ')
rst = 1;
}
}

return rst;
}

至此,一个简单的井字棋游戏就完成了,内容简陋,可以优化的地方还有很多,各位可自行扩展,玩的开心!😄😄😄