@toc

问题引入 - 什么是三子棋?

想必大家儿时都玩过:#字棋

image-20211109151347377

如图,即一个3x3的格子,玩家需要在横、竖、斜三条线上布下自己的棋子

先连成一条线上3个棋子的玩家获胜

如果格子填满后还未有胜者,即为平局

今天就让我们用c语言代码来完成一个基础的三子棋小游戏

涉及知识点:

二维数组、循环语句、自定义函数、自定义头文件

关联博客:扫雷游戏


自定义头文件

本次三子棋代码实现中,我们需要使用到自定义头文件

使用自定义函数前,需要先对函数进行声明

头文件就是一堆声明的集合

问:为什么不把函数声明直接main函数前面?

答:因为这样更方便团队进行同一个项目的分工制作

如何创建自定义头文件?

在vs编译器的“解决资源管理器”里,我们能看到头文件的文件夹

右键它,新建项

image-20211109185607074

选择头文件进行新建,后缀是.h

image-20211109190409687

创建完后,我们可以在另外一个源文件中引用头文件

1
#include "game.h"

另外,因为我们使用的两个源文件(下面会提到)都需要使用c语言的库函数

所以我们可以在自定义头文件中引用库函数的头文件

image-20211109191153981

这样只用在其他源文件中引用我们的自定义头文件

就无需再引用C语言的库函数头文件了


项目中不同源文件之间的连接

在三子棋代码中,我们需要编写两个源文件

  • 一个源文件为main函数所在的源文件
  • 另一个是自定义函数的定义部分所在的头文件

在实际项目合作中,这种方式可以将不同的自定义函数(实现不同功能)的内容分工给每个人

同样是为了方便团队合作

在两个源文件中编写自定义函数的方式和在一个源文件中编写是一样的

如在头文件game.h中声明了初始化棋盘的函数

1
2
//初始化棋盘
void Intboard(char board[ROW][COL], int row, int col);

在实现自定义函数的game.c文件中只需要引用头文件game.h

就能正常编写自定义函数的实现部分

1
2
3
4
5
6
7
#include "game.h"

//初始化棋盘
void Intboard(char board[ROW][COL], int row, int col)
{

}

代码实现

1.打印菜单&初始化棋盘

游戏嘛,肯定需要一个最基本的初始菜单了

玩家通过输入1或0来选择进入游戏还是退出游戏

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

接下来我们需要弄一个初始化的函数

三子棋一共是3x3的格子,所以我们需要一个二维数组来存放棋盘

然后要先把这个棋盘中的每一个棋子都初始化为空格

game.h

1
2
3
4
5
#define ROW 3//行
#define COL 3//列

//初始化棋盘
void Intboard(char board[ROW][COL], int row, int col);

test.c

1
2
3
4
5
char ret = 0;
//存储数据-二维数组
char board[ROW][COL] = { 0 };
//初始化棋盘 - 初始化空格
Intboard(board, ROW, COL);//自定义函数传参

game.c

1
2
3
4
5
6
7
8
9
10
11
12
13
//初始化棋盘
void Intboard(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] = ' ';//将每个棋盘都初始化为空格
}
}
}

2.打印棋盘

玩家/电脑每走一步,游戏都需要打印一次棋盘,告诉玩家自己下在了哪里,以及对手(电脑)落子的位置

和简单打印二维数组不同

我们需要在每个元素之间打印一个分割线,达到以下效果

1
2
3
4
5
//   |   |   
//---|---|---
// | |
//---|---|---
// | |

game.h和test.c文件中写法同上,重点来看game.c文件中函数的实现

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
//打印棋盘
void Display(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("|");//竖分割线
}//效果 a | b | c
printf("\n");//换行
if (i < row - 1)
{

for (j = 0; j < col; j++)
{
printf("---");//横分割线
if (j < col - 1)
printf("|");//竖分割线
}//效果---|---|---
printf("\n");
}

}
}

这样我们就能输出一个如图所示的棋盘

image-20211110081936609

你可能注意到,这里多了两行提示

这是我们test.c函数中的内容

三子棋游戏需要以函数返回值来判断是否胜利(平局)

不过这里的test函数主要用于玩家最开始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
void test()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,请重新选择!\n");
break;
}
} while (input);
}

int main()
{
test();
return 0;
}

这里的srand以及time函数是用来生成随机数,作为电脑落子坐标的

猜数字游戏的博客里也有提到这两个函数 [链接]

1
srand((unsigned int)time(NULL));

3.游戏基本流程

打印完棋盘后,开始编写玩家落子、电脑落子以及判断谁最终胜利的函数

在日常编写代码的时候,我们也最好使用这种方式

  • 先想好游戏的具体流程
  • 写出流程需要的函数名和判断过程
  • 最后补齐自定义函数的实现

test.c

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
void game()
{
char ret = 0;
//存储数据-二维数组
char board[ROW][COL] = { 0 };
//初始化棋盘 - 初始化空格
Intboard(board, ROW, COL);
//打印棋盘
Display(board, ROW, COL);

while (1)
{
//玩家走
PlayerMove(board, ROW, COL);
Display(board, ROW, COL);
//判断玩家是否胜利
ret = Iswin(board, ROW, COL);
if (ret != 'C')
{
break;
}
//电脑走
BotMove(board, ROW, COL);
Display(board, ROW, COL);
//判断电脑是否胜利
ret = Iswin(board, ROW, COL);
if (ret != 'C')
{
break;
}
}
//判断状态
if (ret == '*')
{
printf("玩家赢了\n");
}
else if (ret == '#')
{
printf("电脑赢了\n");
}
else
{
printf("平局\n");
}
}

代码中的Iswin函数用于判断谁胜利

因为每走一步的结果有4种情况

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

所以这里我们不再使用数字(也是避免和原有menu选择弄混)

以字符的形式来表达这4种情况

1
2
3
4
5
//在游戏进行的过程中--Iswin判断
//1.玩家赢了 -*
//2.电脑赢了 -#
//3.平局 -Q
//4.继续 -C

4.玩家走棋

玩家走棋的时候,我们需要注意以下几点

  • 玩家应以[1,3]的数字作为落子坐标,而不应从0开始(下标从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
//玩家走
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");
}
}
}

细心的你肯定发现了,这里的玩家落子使用的是*,也正是玩家胜利时的返回字符

下面电脑落子的时候用的就是#


5.电脑落子

电脑落子比玩家落子相对较简单

我们只需要让生成的两个随机数在1-3之间即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void BotMove(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("电脑走: \n");//提示玩家此处是电脑落子

while (1)
{
x = rand() % row;
y = rand() % col;
if (board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}

6.判断是否胜利

上面提到,每走一步,我们都需要用Iswin函数判断是否有人胜利

三子棋胜利有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
27
28
29
30
31
32
33
34
35
36
37
38
//判断胜利
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[2][0] == board[1][1] && board[1][1] == board[0][2] && board[1][1] != ' ')
return board[1][1];

//判断平局
//满了返回1,不满返回2

if (1 == IsFull(board, row, col))
{
return 'Q';
}

//继续
return 'C';
}

这里又出现了一个新的函数,IsFull


7.判断是否平局

当上面的判断胜利的语句走完后,若还没出现返回的情况

我们就要判断是否是平局,即判断棋盘的9个棋子是否都已落下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;//棋盘满了
}

如果的判断胜利和平局的语句都走完后,还没出现返回的情况

即说明游戏并未停止,我们需要继续游戏


游戏效果

到这里,我们的三子棋游戏就完成了!

跑一下看看效果吧!

image-20230312122907480


完整代码

这里贴出三子棋函数的完整代码供大家参考

game.c

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
#define _CRT_SECURE_NO_WARNINGS 1

#include "game.h"

//初始化棋盘
void Intboard(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] = ' ';
}
}
}
//打印棋盘
void Display(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)
{

for (j = 0; j < col; j++)
{
printf("---");
if (j < col - 1)
printf("|");
}
printf("\n");
}

}
}
//玩家走
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");
}
}
}

void BotMove(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("电脑走: \n");

while (1)
{
x = rand() % row;
y = rand() % col;
if (board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}

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;//棋盘满了
}


//判断胜利
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[2][0] == board[1][1] && board[1][1] == board[0][2] && board[1][1] != ' ')
return board[1][1];

//判断平局
//满了返回1,不满返回2

if (1 == IsFull(board, row, col))
{
return 'Q';
}

//继续
return 'C';
}

test.c

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
#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 };
//初始化棋盘 - 初始化空格
Intboard(board, ROW, COL);
//打印棋盘
Display(board, ROW, COL);

while (1)
{
//玩家走
PlayerMove(board, ROW, COL);
Display(board, ROW, COL);
//判断玩家是否胜利
ret = Iswin(board, ROW, COL);
if (ret != 'C')
{
break;
}
//电脑走
BotMove(board, ROW, COL);
Display(board, ROW, COL);
//判断电脑是否胜利

ret = Iswin(board, ROW, COL);
if (ret != 'C')
{
break;
}
}
//判断状态
if (ret == '*')
{
printf("玩家赢了\n");
}
else if (ret == '#')
{
printf("电脑赢了\n");
}
else
{
printf("平局\n");
}
}


void test()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,请重新选择!\n");
break;
}
} while (input);
}

int main()
{
test();
return 0;
}

game.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define ROW 3
#define COL 3

//初始化棋盘
void Intboard(char board[ROW][COL], int row, int col);
//打印棋盘
void Display(char board[ROW][COL], int row, int col);

//玩家走
void PlayerMove(char board[ROW][COL], int row, int col);
//电脑走
void BotMove(char board[ROW][COL], int row, int col);

//判断是否胜利
char Iswin(char board[ROW][COL], int row, int col);
//判断是否有空位
#pragma once

结语

本篇三子棋的博客到此结束了

感谢你看到最后!

下篇博客会教大家,如何打包我们的代码为dll文件(此文件无法编译)

点个赞再走吧,球球了!