本笔记参考B站up鹏哥C语言的视频
本笔记并没有优化电脑下棋算法
三子棋要求
主函数
菜单
game(游戏部分)
1.创建棋盘
2.初始化棋盘
3.打印棋盘
4.下棋部分
玩家下棋
电脑下棋
判断胜利方
IsWin函数部分
代码(检查 - 行\列)
代码(检查 - 对角线)
平局/游戏继续
完整代码
game.h
test.c
game.c
分为三个模块
test.c 测试游戏的逻辑
game.h 关于游戏相关的函数声明,符号声明,头文件的包含
game.c 游戏相关函数的实现
首先要有菜单,告诉玩家如何选择
分出情况:
1.如果想玩游戏 - 弹出游戏界面
2.如果不想玩游戏
3.如果输入错误
代码:
int main()
{
int input = 0;
do
{
menu();
printf("请选择: ");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择有误,请重新选择\n");
break;
}
} while (input);//0直接跳出循环,其它情况重新进入循环
return 0;
}
-----
出现可供玩家选择的选项
void menu()
{
printf("**********************************\n");
printf("**** play:输入1 ****\n");
printf("**** exit:输入0 ****\n");
printf("**********************************\n");
}
-----
(三子棋 - 九宫格 - 3*3的空间 - 二维数组)
1.三子棋本质上是一种输入字符的二维数组,故首先要创建字符数组
2.但是如果需要改变三子棋玩法,扩张三子棋界面,仅仅创建数组“board[3][3]”就略显不足。
所以为了更改方便,就在 game.h 中创建变量
//符号的定义
#define ROW 3
#define COL 3
之后直接引用[board[ROW][COL]],就方便了,需要扩展棋盘时可以直接更改头文件内容。
三子棋的棋盘是由3*3的空格组成的,就如
所以还需要初始化棋盘。
运用函数:[InitBoard(board,ROW,COL)]
为了可以使用函数,需要在头文件 game.h 中声明函数:
void InitBoard(char board[ROW][COL], int row, int col);
而为了可以运行函数达成我们的目的,还要在 game.c 补全函数内容:
#include"game.h"
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] = ' ';
}
}
}
ps:这里别忘了引用头文件,如果嫌引头文件麻烦,可以将头文件包含进 game.h 中,即头文件的包含。
备注:
数组在传参的时候:
形参可以写成两种形式:
1. 数组形式(如:void test1(int arr[10]){}、void test1(int arr[]){})
2. 指针形式(如:void test1(int* arr))
注意:数组传参传的是数组,但是形参可以写成指针的形式。但本质上都是指针
运用函数[DisplayBoard(board,ROW,COL)]
同样,为了使用函数,在 game.h 中进行定义:
void DisplayBoard(char board[ROW][COL],int row, int col);
在 game.c 补全函数内容:
void DisplayBoard(char board[ROW][COL], int row, int col)
{
int i = 0;
for ( i = 0; i < row; i++)
{
printf(" %c | %c | %c \n",board[i][0],board[i][1],board[1][2]);
if(i < row - 1)//最后一行不要打印,美观
printf("---|---|---\n");
}
}
运行程序,可以看见:
而在上一步中如果不初始化棋盘,可能变成这样:
(初始化是重要的)
--- --- ---
但是,这一步结束了吗?
别忘了为什么要定义 ROW 和 COL 这两个变量。如果改变上述变量的定义,再用上述代码运行,由于[printf(" %c | %c | %c \n",board[i][0],board[i][1],board[1][2])]并没有和变量联系起来,这会导致:
(此处 ROW 和 COL 定义都是 5)
可以看到:列数与 COL 定义的实数并不相等。代码存在问题
改进:
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");
}
}
}
思路上
改变了原来通过单个循环打印的想法,将需要打印的行和列的元素拆分到了不同循环里,if语句用来保障不会出现多余的[ "|" ]。
i 作为计数器使用。
两个 j 分别在各自的循环里建立。
由于最后不用出现[ | ],所以if语句条件中的 col(行数) 减一。
运用函数[PlayerMove(board,ROW,COL)]
在 game.h 中的定义
void PlayerMove(char board[][COL],int row,int col);
在 game.c 中的应用
PlayerMove(char board[][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");
}
}
}
注意
x 、 y都是玩家输入的值,但是数组坐标是从0开始,在处理玩家输入的数据时,必须减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"); }
再搞定外侧循环部分;
下棋是应该循环的过程,所以所以while循环确定整体。当要跳出循环时,直接break即可。
运用函数[ComputerMove(board, ROW, COL)]
在 game.h 中的定义
void ComputerMove(char board[][COL], int row, int col);
在 game.c 中填充函数内容
void ComputerMove(char board[][COL], int row, int col)
{
printf("电脑走:\n");
while (1)
{
int x = rand() % row;//%row的余数是 0 到 row-1
int y = rand() % col;//col同理
//判断落子
if (board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}
注意
依旧是从内到外,但是变量x和变量y没有减1,这是因为使用 rand函数 时依旧通过 % 限制了范围;
使用 rand函数 时,需要调用头文件
;使用 time函数 时,需要调用头文件 。此外,在使用 rand函数 生成随机数之前,要使用 srand函数 设置随机数的生成器, srand函数再运用时间戳设置随机数的起点。(由于只需设置一次随机数的起点,所以将srand函数放在主函数中) 所以在主函数中,存在:
srand((unsigned int)time(NULL));
之所以没有像玩家移动时一样判断合法性,是因为变量定义时已经确定了范围。(0 到 row-1、0到col-1)
在游戏进行的过程中,有四种情况:
1. 玩家获胜
2. 电脑获胜
3. 平局
4. 游戏继续
运用函数[IsWin]判断游戏的状态
假设:
游戏结束
玩家获胜 - 返回 *
电脑获胜 - 返回 #
平局 - 返回 Q
------
游戏继续 - 返回 C
(IsWin 返回字符,用char类型)
代码(在前面代码的基础上):
char ret = 0;//用来获取游戏状态
while (1)
{
//玩家下棋
PlayerMove(board,ROW,COL);
DisplayBoard(board, ROW, COL);
//判断玩家是否赢得游戏
char ret = IsWin(board, ROW, COL);
if (ret != 'C')
break;
//电脑下棋
ComputerMove(board, ROW, COL);
DisplayBoard(board, ROW, COL);
//判断电脑是否赢得游戏
char ret = IsWin(board, ROW, COL);
if (ret != 'C')
break;
}
if (ret == '*')
{
printf("玩家获胜\n");
}
else if (ret == '#')
{
printf("电脑获胜\n");
}
else
{
printf("平局\n");
}
DisplayBoard(board, ROW, COL);
}
通过if语句判断输赢,得出结果。
-----
要求:
1.判断获胜方
(1)检查每一行
(2)检查每一列
(3)检查两行对角线
2.判断平局
3.游戏继续
现在 ROW 和 COL 可改变的前提下,通过 board函数 直接指名数组内容的思路就行不通了。于是为了满足要求,可以检查数组的每一行和每一列,将循环加入 IsWin函数 部分。
但是,只一个循环无法完成判断部分,使用 switch函数 进行多情况处理。
char IsWin(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
int a = 0;
int ret = 0;
//判断三行
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
ret += my_count(board[i][j]);
}
j -= 1;
if (ret == row || ret == -row)
a = (ret == row|| ret == -row);
switch (a)
{
case 1:
return board[i][j];
break;
default:
break;
}
ret = 0;
}
}
这里引入了 my_count 函数作为计数器使用,通过比较 ret 的值和 row 是否一致进行判断,返回时可以直接返回 board[i][j] 。
my_count 部分:
int my_count(char ret) { int count = 0; if (ret == '#') { count--; } else if (ret == '*') { count++; } else ; return count; }
对每一列的检查与对行的检查类似,不赘述。(但是要注意返回时字符是否正确)
------
先放代码(为了方便观察,开头部分是重复的):
char IsWin(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
int a = 0;
int ret = 0;
//判断三行
for (i = 0, j = 0; i < col; i++, j++)
{
ret += my_count(board[i][j]);
if (ret == row || ret == -row)
a = (ret == row || ret == -row);
switch (a)
{
case 1:
return board[i][j];
break;
default:
break;
}
}
ret = 0;
for (i = row - 1, j = 0; i > 0; i--, j++)
{
ret += my_count(board[i][j]);
if (ret == row || ret == -row)
a = (ret == row || ret == -row);
switch (a)
{
case 1:
return board[i][j];
break;
default:
break;
}
}
思路上依旧是循环写法,用 for循环 计数,用 switch语句 判断情况。
不同的是 for循环 的条件改为了对两条对角线的描述。
for (i = 0, j = 0; i < col; i++, j++) for (i = row - 1, j = 0; i > 0; i--, j++)
-----
当上面三种情况结束,并且每有返回值时,就要判断是否平局,或者让游戏继续。
引入函数[IsFull(board, row, col)],该函数只需在 game.c 内生效,故无需在头文件中进行定义。
IsWin函数 内的部分:
int b = IsFull(board, row, col);
if (b == 1)
{
return 'Q';
}
//游戏继续
return 'C';
}
(Q、C分别代表平局、游戏继续)
IsFull函数部分:
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; i++)
{
if (board[i][j] != ' ')
{
return 0;//棋盘没满
}
}
}
return 1;
}
加入两个循环分别表示行和列,依次输入来检测数组的每一个元素。
判断完毕后返回值,整个三子棋游戏就结束了。
//头文件的包含
#include
#include
//符号的定义
#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 PlayerMove(char board[][COL],int row,int col);
//电脑下棋
void ComputerMove(char board[][COL], int row, int col);
//假设:
//游戏结束
// 玩家获胜 - 返回 *
// 电脑获胜 - 返回 #
// 平局 - 返回 Q
//------
//游戏继续 - 返回 C
char IsWin(char board[ROW][COL], int row, int col);
-------------------------
#define _CRT_SECURE_NO_WARNINGS
#include"game.h"
#include
#include
void menu()
{
printf("**********************************\n");
printf("**** play:输入1 ****\n");
printf("**** exit:输入0 ****\n");
printf("**********************************\n");
}
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);
//判断电脑是否赢得游戏
char ret = IsWin(board, ROW, COL);
if (ret != 'C')
break;
}
if (ret == '*')
{
printf("玩家获胜\n");
}
else if (ret == '#')
{
printf("电脑获胜\n");
}
else
{
printf("平局\n");
}
DisplayBoard(board, ROW, COL);
}
int main()
{
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);//0直接跳出循环,其它情况重新进入循环
return 0;
}
-------------------------
#define _CRT_SECURE_NO_WARNINGS
#include"game.h"
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] = ' ';
}
}
}
//void DisplayBoard(char board[ROW][COL], int row, int col)
//{
// int i = 0;
// for ( i = 0; i < row; i++)
// {
// printf(" %c | %c | %c \n",board[i][0],board[i][1],board[1][2]);
// if(i < row - 1)//最后一行不要打印,美观
// printf("---|---|---\n");
// }
//}
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");
}
}
}
//玩家走
void PlayerMove(char board[][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 ComputerMove(char board[][COL], int row, int col)
{
printf("电脑走:\n");
while (1)
{
int x = rand() % row;//%row的余数是 0 到 row-1
int y = rand() % col;//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; i++)
{
if (board[i][j] != ' ')
{
return 0;//棋盘没满
}
}
}
return 1;
}
int my_count(char ret)
{
int count = 0;
if (ret == '#')
{
count--;
}
else if (ret == '*')
{
count++;
}
else
;
return count;
}
char IsWin(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
int a = 0;
int ret = 0;
//判断三行
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
ret += my_count(board[i][j]);
}
j -= 1;
if (ret == row || ret == -row)
a = (ret == row|| ret == -row);
switch (a)
{
case 1:
return board[i][j];
break;
default:
break;
}
ret = 0;
}
//判断三列
for (i = 0; i < col; i++)
{
for (j = 0; j < row; j++)
{
ret += my_count(board[j][i]);
}
j -= 1;
if (ret == row || ret == -row)
a = (ret == row || ret == -row);
switch (a)
{
case 1:
return board[j][i];
break;
default:
break;
}
ret = 0;
}
//判断对角线
for (i = 0, j = 0; i < col; i++, j++)
{
ret += my_count(board[i][j]);
if (ret == row || ret == -row)
a = (ret == row || ret == -row);
switch (a)
{
case 1:
return board[i][j];
break;
default:
break;
}
}
ret = 0;
for (i = row - 1, j = 0; i > 0; i--, j++)
{
ret += my_count(board[i][j]);
if (ret == row || ret == -row)
a = (ret == row || ret == -row);
switch (a)
{
case 1:
return board[i][j];
break;
default:
break;
}
}
//判断平局
//如果棋盘满了,返回1;不满,返回0
int b = IsFull(board, row, col);
if (b == 1)
{
return 'Q';
}
//游戏继续
return 'C';
}