在这篇博客中,我将从头到尾整理三子棋游戏的代码,争取能将这个小游戏里面包含的细节能全整理出来。为什么要整理呢?因为我觉得如果不看任何参考能用C出一个小游戏,是一件蛮厉害的事情,要做到这件事情,需要对游戏的细节完完全全地掌握。当然,三子棋是个小游戏,实现这个游戏并不需要很多高深的技术,甚至连指针都不需要,单纯靠二维数组就可以做到。但是,实现这个小游戏还是蛮费劲的,需要一定的逻辑,实现过程就感觉跟做一道很长的数学分析证明题,尽管整个题的思路你很清楚,但做起来还是挺麻烦,一环套一环,要做到每个细节的准确才能做出来。(我的废话真多!)
目录 三子棋游戏介绍三子棋整体思路三子棋文件划分按文件分析代码头文件主程序文件函数实现文件 程序运行棋盘玩家下一个棋玩家获胜 总结与畅想 三子棋游戏介绍三子棋与五子棋很相似,规则也都差不多,相当于五子棋的简化游戏。与五子棋主要有两个区别,一是只需要有三个棋子连成一行就能赢,二是三子棋的棋盘大小是3*3的,这跟五子棋相比会简单很多,程序也好写一些。
三子棋整体思路我们先来考虑程序需要实现什么,从一个玩家的角度来看:
我们需要输出一个菜单,让玩家选择开始游戏或者退出选择开始游戏后,要输出一个棋盘然后提示玩家选一个位置下棋,玩家下完棋之后,电脑下一个棋,这个过程需要多次进行判断胜负 三子棋文件划分三子棋游戏的代码还是有点长的,如果在一个源文件里面写完整个代码,会非常麻烦。为了方便,我们把代码放到三个文件中。具体文件划分如下:
头文件:sanziqi.h
这个文件中放我们需要引用的头文件和函数声明主程序文件:sanziqi.c
这个文件中写我们函数的主体函数实现文件:hanshu.c
这个文件中具体实现我们要用的函数
按文件分析代码
头文件
sanziqi.h
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
#include
// 宏定义棋盘的大小,这样后面我们想改变棋盘大小时直接改这一处即可
#define ROW 3 // 行数
#define COL 3 // 列数
// 初始化数组
void InitBoard(char Board[ROW][COL], int row, int col);
// 打印棋盘
void PrintBoard(char Board[ROW][COL], int row, int col);
// 玩家下棋
void PlayerWalk(char Board[ROW][COL], int row, int col);
// 电脑下棋
void ComputerWalk(char Board[ROW][COL], int row, int col);
// 判断输赢
int CheckWin(char Board[ROW][COL], int row, int col);
这个文件中包含的是:
需要用到的头文件通过宏定义程序中经常出现的变量,方便后期修改所有的功能函数的声明有了这个文件,我们在其他文件中引用它,这样就不用写再其它头文件的引用了,可以精简代码,程序也会更加清晰。
主程序文件sanziqi.c
void menu()
{
printf("****************************************\n");
printf("*********** 1. 开始游戏 ************\n");
printf("*********** 0. 退出游戏 ************\n");
printf("****************************************\n");
}
void game() // 游戏主函数
{
// 初始化一个数组存储数据
char Board[ROW][COL] = { 0 }; // 棋盘上每一个位位置都是存在这个数组中
InitBoard(Board, ROW, COL); // 初始化一个棋盘
// 打印棋盘
PrintBoard(Board, ROW, COL);
// 玩游戏时
// 1.玩家赢 - '*'
// 2.电脑赢 - '#'
// 3.平局了 - 'Q'
// 4.继续 - 'C'
char ret = 0; // 初始化一个字符变量,按照棋盘上的情况将对应的上面的字符赋给ret
while (1)
{
PlayerWalk(Board, ROW, COL); // 让玩家下
// 不论是玩家还是电脑下了一个棋后,我们都要判断棋盘上的局势
// 判断输赢
ret = CheckWin(Board, ROW, COL); // 得到棋盘上的情况相对应的字符
if (ret != 'C') // 如果返回的字符是C,那么代表程序需要继续
{
// 如果能进入循环,说明结局已定不需要再循环了
break; // 直接跳出此次循环
}
PrintBoard(Board, ROW, COL); // 如果没有跳出循环,那么打印玩家下完棋后的棋盘
ComputerWalk(Board, ROW, COL); // 现在轮到电脑下棋了,所有流程同上
// 判断输赢
ret = CheckWin(Board, ROW, COL);
if (ret != 'C')
{
break;
}
PrintBoard(Board, ROW, COL);
}
if (ret == '*')
{
printf("玩家赢\n");
}
else if (ret == '#')
{
printf("电脑赢\n");
}
else if (ret == 'Q')
{
printf("平局了\n");
}
// 打印完结局之后,需要让玩家看一下棋局
PrintBoard(Board, ROW, COL);
}
int main()
{
int input = 0; // 初始化一个整型变量,后面用来存储玩家的选择
// 下面是设置随机种子,是为了后面生成电脑下棋的随即坐标,具体用法可以参考我的上一篇博客
srand((unsigned)time(NULL));
// 下面使用一个 do while 循环,因为程序一运行就要看到菜单,让玩家选择
do
{
menu(); // 打印菜单
printf("请选择:>");
scanf("%d", &input);
// 根据用户的选择执行对应的 *** 作
switch (input)
{
case 1:
game(); // 调用游戏主函数
break;
case 0:
printf("退出游戏!\n");
exit(0);
default:
printf("输入错误!\n");
}
} while (input);
return 0;
}
这个文件中写的是游戏的主体框架,game
函数和menu
函数并非是功能函数,所以不需要写在函数文件中。关于主体框架的详细思路,我在上述代码中写了很详细的注释,可以说是非常的详细。
hanshu.c
#include "sanziqi.h"
// 函数实现
// 初始化存储棋盘上数据的数组,我们对棋盘上所有的位置赋上空格,因为初始时棋盘上所有位置都是空的
void InitBoard(char Board[ROW][COL], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
Board[i][j] = ' ';
}
}
}
// 打印棋盘,包括棋盘上的落子情况
void PrintBoard(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 PlayerWalk(char Board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("玩家请开始下棋:\n");
while (1) // 无论如何玩家都得下一个棋
{
printf("请选择下棋位置:> ");
scanf("%d%d", &x, &y);
// 对于玩家来说,棋盘的左上角位置就是第一行第一个位置,即(1,1),但是数组却是从(0,0)开始的,
// 因此我们需要对玩家输入的坐标减一,这样能保证玩家直观理解,同时也不浪费数组空间
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (Board[x - 1][y - 1] == ' ')
{
Board[x - 1][y - 1] = '*';
break;
}
}
else
{
printf("输入有误,请重新输入!\n");
}
}
}
// 电脑下棋
// 电脑的下棋的坐标我们利用随机数生成
void ComputerWalk(char Board[ROW][COL], int row, int col)
{
printf("电脑走:>\n");
while (1)
{
int x = rand() % ROW; // 这块生成的随机数范围是0~ROW-1的
int y = rand() % COL;
if (Board[x][y] == ' ') // 如果位置为空,那么就下一个棋
{
Board[x][y] = '#';
break;
}
}
}
// 判断棋盘是否满了,这块使用static关键字是因为此函数只在此文件中使用,其他文件中不会调用
// 这样写这个函数也只能在这个函数中使用
static int IsFull(char Board[ROW][COL], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
if (Board[i][j] == ' ')
{
return 0;
}
}
}
return 1; // 满了返回1
}
// 判断棋盘上的局势
int CheckWin(char Board[ROW][COL], int row, int col)
{
// 每次判断的时候,先判断有没有赢的,没有的话就检查棋盘有没有满,根据情况返回对应字符
// 按行检查有没有凑齐的
for (int i = 0; i < row; i++)
{
int flag = 1; // 定义一个标记变量 1表示凑齐了 0表示没凑齐
for (int j = 0; j < col; j++)
{
if (Board[i][0] != Board[i][j]) // 判断是否跟第一个元素相同
{
flag = 0; // 如果出现一个不同的,那这行剩下的就不用再判断了,直接跳到下一行
break;
}
}
if (Board[i][0] != ' ' && flag != 0) // 判断这一行元素是不是空的,有没有凑齐
{
return Board[i][0]; // 满足条件就返回这行的首元素,作为反馈字符
}
}
// 按列检查有没有凑齐的,思想同上
for (int i = 0; i < col; i++)
{
int flag = 1;
for (int j = 0; j < row; j++)
{
if (Board[0][i] != Board[j][i])
{
flag = 0;
break;
}
}
if (Board[0][i] != ' ' && flag != 0)
{
return Board[0][i];
}
}
// 检查主对角线
{
int flag = 1;
for (int i = 1; i < row; i++)
{
if (Board[0][0] != Board[i][i])
{
flag = 0;
break;
}
}
if (Board[0][0] != ' ' && flag != 0)
{
return Board[0][0];
}
}
// 检查次对角线 下面注释的这行是只针对三子棋的
/*{
if (Board[0][2] == Board[1][1] && Board[1][1] == Board[2][0] && Board[0][2] != ' ')
return Board[1][1];
}*/
// 下面的代码是针对任意情况的,如果我们更改了棋盘大小也能判断
{
int flag = 1;
for (int i = 0, j = col - 1; i < row; i++, j--)
{
if (Board[0][col - 1] != Board[i][j])
{
flag = 0;
break;
}
}
if (Board[0][col - 1] != ' ' && flag != 0)
{
return Board[0][col - 1];
}
}
// 检查棋盘
// 判断棋盘是否满了
if (IsFull(Board, ROW, COL) == 1)
{
return 'Q';
}
// 不是平局,游戏继续
return 'C';
}
在这个文件所实现的功能函数中,CheckWin
函数最复杂也最为巧妙,复杂是因为要检查出棋盘上的所有情况,特别是判断有没有赢家的时候,要按行、按列、按对角线判断。巧妙体现在这个函数的返回值上,当我们检查某一行三个元素都相同时,那我们直接返回这一行的某个元素即可。还有值得注意的是static关键字的使用,static修饰函数会将这个函数的使用范围限制在当前文件中。其余的细节,我十分详细地加了注释在代码之中。
图片下面的水印我没有找到去除的方法。。。
我们来观察这个棋盘,直观看起来是3*3的,但其实它有5行,中间有两行是用来分隔得,具体的细节放大看这个图片就会很明显。
这个水印挡住了最下面一行位置得棋子。。。我下去再好好研究一下CSDN上传照片的规则。
以上就是三子棋游戏的所有代码了,我是在VS2019编译器中写的,在别的编译器运行起来应该没毛病,但是我没有特别详细地测试代码,不过在我有限的测试下没出什么问题,不得不说测试还是蛮无聊的。上面的代码其实改一下宏定义变量的大小,立马就能改成阉割版的五子棋。
那么我为啥要写一个三子棋游戏而不去写一个五子棋呢?主要就是电脑下棋的过程和判断胜负过程实现起来有点复杂。不过我也简单思考了一下五子棋与三子棋实现起来的区别。首先就是五子棋通常是生成一个固定大小的棋盘,每下一个棋需要判断这个这个棋子周围(半径为5个棋子)所有棋子(情况还要细分),然后就是电脑自动下棋的事情,三子棋中就那么几个格子,随便下一个位置就能起到一定的影响,但是在大棋盘中,如何实现电脑智能下棋是个问题,还有如何实现电脑智能度的控制等等。这个我以后再写吧。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)