1. 创建main.c、game.c、game.h三个文件
本次三子棋游戏的实现包含三个文件,第一个文件是main.c,控制游戏的执行逻辑。第二个文件是游戏头文件game.h,用来声明游戏实现的函数。第三个是game.c,主要是用来存放实现游戏的具体函数。
2. 游戏展示
下面三子棋游戏展示界面。
3. 主函数main()编写
3.1 开始菜单打印
首先游戏开始时要打印游戏开始界面,供玩家选择和退出游戏,可以调用一个menu()来打印。menu()具体代码如下:
void menu() {
printf("**********************************\n");
printf("**** 1. play 0.exit ****\n");
printf("**********************************\n");
printf("请输入:>");
}
3.2 do...while()控制游戏执行
当主函数main()调用menu()打印出菜单时,玩家选择玩游戏或者退出游戏。当玩家选1时,玩家开始游戏,当游戏结束后,主函数需要再次打印菜单,如果还想再玩,选1,进入游戏,直到玩家不再想玩了,玩家选0,退出游戏。
上面的实现逻辑可以使用do...while()循环,先打印一个游戏开始菜单,再根据玩家输入的选项决定进入游戏或者是退出游戏。可以用scanf()函数接收用户输入的选项。具体实现代码如下:
int main() {
//input用来接收用户输入的选项
int input = 0; //input初始化
do {
menu(); //打印游戏开始菜单
scanf("%d", &input); //用scanf()函数在命令行窗口接收用户输入的选项,并传递给input变量
switch (input) {
case 1:
printf("游戏开始!\n"); //选1,游戏开始
game();
break;
case 0:
break; //选0,跳出switch语句,然后游戏会退出
default:
printf("选项输入错误,请重新输入!\n"); //当选项不是0或1时,跳出switch语句,重新输入
break;
}
} while (input); //用变量input作为循环的判断条件,input为0,跳出循环退出游戏,非0时循环继续
return 0;
}
4. game()函数编写
4.1 game()执行流程
当玩家选1后,开始玩游戏,调用game()函数。下面来分析一下game()函数执行流程:game()函数的执行分为粗略分几个步骤:玩家下棋、棋盘展示 、输赢判断、电脑下棋、棋盘展示、输赢判断。以上步骤需要不断重复,直到一方为赢或者双方平局。玩家下棋、棋盘展示 、输赢判断、电脑下棋可以分别用PlayerMove()、DisplayBoard()、IsWin()、ComputerMove()表示,下面是大概的执行流程:
void game() {
while (判断条件) {
PlayerMove(); //玩家下棋
DisplayBoard(); //棋盘展示
IsWin(); //判断输赢
ComputerMove(); //电脑下棋
DisplayBoard(); //棋盘展示
IsWin(); //判断输赢
}
...
}
4.2 棋盘初始化
根据上面执行流程,首先玩者想下棋,就需要根据棋盘位置进行落子。所以在落子前需要先打印棋盘。我们观察一下三子棋不同状态的棋盘,即空棋盘、落子后的棋盘,方便我们构思如何打印棋盘和棋盘落子后如何存储落子数据等问题。
通过上面观察,我们发现,三子棋的棋盘可分为两部分:分隔符和棋盘内容。分隔符是固定不变的(横为虚线,竖为实线),可以在每次打印时加上分隔符,然后打印出来。而且棋盘内容在未落子前为空,落子后变成'*'或者'#'。进一步来说,对于空的棋盘状态,可以看成填满空格字符' '的棋盘,在落子时,只是将空格' '替换成'*'或'#'('*'表示玩家下棋,'#'表示电脑下棋),最后将棋盘内容打印出来。因为这个是三子棋,有三行三列。可以用一个二维数组存储棋盘内容,其二维数组的类型为char,可以存放三种不同状态,即未下棋' '、玩家下棋'*'、电脑下棋'#'。
经过上面讨论,所以在下棋前,还需要一个步骤,就是将棋盘的内容全部初始化成空格字符,才能打印出空棋盘,然后玩家根据棋盘进行落子。初始化棋盘用InitBoard()表示,实现代码如下:
//初始化棋盘
//ROW、COL分别表示棋盘的行数和列数,在头文件中已定义为常量
//ROW、COL定义为常量,方便后面不把代码写死,方便后面改为四子、五子棋、等n子棋。
void InitBoard(char board[ROW][COL], int row, int col) { //row表示棋盘行数,col表示棋盘的列数
int i = 0;
int j = 0;
for (i = 0; i < row; i++) {
for (j = 0; j < col; j++) {
board[i][j] = ' '; //将棋盘的每个元素初始化为字符空格' '
}
}
}
4.3 棋盘展示打印
初始化棋盘后,需要对棋盘打印,打印棋盘也就是打印分隔符+棋盘内容。这里为了不把程序写死,方便后改成打印四子棋、五子棋、n子棋等。实际上,后面的代码都基于这个理念。实现下面的代码如下:
//棋盘显示
void DisplayBoard(char board[ROW][COL], int row, int col) {
int i = 0;
for (i = 0; i < row; i++) { //按行来打印,总共需要打印row行
//打印" %c | %c | %c "
int j = 0;
for (j = 0; j < col; j++) {
printf(" %c ", board[i][j]); //打印每行的%c
if (j < col - 1) { //为了使打单行印出的'|'数量比%c少1个
printf("|"); //打印%c结束后,在后面加一个分隔符'|'
}
}
printf("\n");
//打印分割信息"---|---|---"
if (i < row - 1) { //控制棋盘最后一行不打印分割信息
int k = 0;
for (k = 0; k < col; k++) {
printf("---"); //打印分隔符"---"
if (k < col - 1) { //为了使打单行印出的'|'数量比"---"少1个
printf("|"); //打印"---"结束后,在后面加一个分隔符'|'
}
}
printf("\n");
}
}
}
所以现在game()执行的流程:初始化棋盘、空棋盘显示、玩家下棋、棋盘展示 、输赢判断、电脑下棋、棋盘展示、输赢判断。
void game() {
InitBoard(); //初始化棋盘
DisplayBoard(); //空棋盘展示
while (判断条件) {
PlayerMove(); //玩家下棋
DisplayBoard(); //棋盘展示
IsWin(); //判断输赢
ComputerMove(); //电脑下棋
DisplayBoard(); //棋盘展示
IsWin(); //判断输赢
}
...
}
上面提到,为了不把三子棋程序写死,方便改为四子棋、五子棋等。我将棋盘的行数和列数,即ROW和COL定义为常量,写在头文件game.h上,同时main.c文件上再包含头文件"game.h"。如果后续想改为四子棋、五子棋等,只需要将game.h文件中ROW和COL改为特定的值即可,具体定义常量如下。
#define ROW 3
#define COL 3
并且在main.c文件中包含game.h的头文件,具体如下:
#include "game.h"
现在game()函数上定义char类型二维数组表示,然后进行棋盘初始化和棋盘展示。
void game() {
char board[ROW][COL] = {0}; //定义一个char类型的二维数组
InitBoard(board, ROW, COL); //初始化棋盘
DisplayBoard(board,ROW,COL); //棋盘展示
while (判断条件) {
...
}
...
}
测试一下棋盘的展现效果!
4.4 玩家下棋
完成棋盘打印后,下面开始写玩家下棋的代码,玩家由PlayerMove()函数控制。玩家下棋时,需要输入落子行数和列数,如输入坐标(3,1),表示落子在第3行、第1列。
当玩家输入坐标(3,1)时,只需要将玩家输入的坐标转化为board数组的坐标索引,即board[2][0],然后该位置空格字符‘ ’替换成字符'*'即可。
当然,玩家也有可能输入的坐标超出棋盘范围,需要增加一个输入坐标的合法判断,也就是输入的坐标要大于等于1并且小于等于行数或列数。
而且,当输入的坐标是合法后,如果之前这个坐标有落子,位置被占用了,也是不能落子的,需要增加落子位置是否占用的判断。PlayerMove()具体实现如下:
//玩家下棋
//玩家下棋为标记为*
void PlayerMove(char board[ROW][COL], int row, int col) {
int x = 0; //用来接收玩家下棋输入的行数
int y = 0; //用来接收玩家下棋输入的列数
while (1) {
printf("玩家下棋:>");
scanf("%d %d", &x, &y); //用scanf()函数在屏幕上接收玩家输入的行数和列数
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");
}
}
}
4.5 电脑下棋
实现了玩家下棋,下面开始写电脑下棋代码,和玩家下棋代码大同异,用ComputerMove()表示。因为暂时没有学过复杂的电脑下棋代码,知识有限,暂时用随机生成值代替电脑下棋,哈哈,简单粗暴。
因为电脑下棋需要随机生成值,用rand()进行随机值生成,将生成随机值模上行数或列数,就能得到棋盘二维数组的索引坐标,这样生成的坐标是合法的,又减少了坐标合法判断的代码,只需判断位置是否被占用就可以了,具体如下:
//电脑下棋
//电脑下棋标记为#
void ComputerMove(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;
}
}
}
在调用rand()函数时,需要设置一个随机数序列的起始点,即随机生成序列的"种子",可以通过获取当前系统时间的秒数作为"种子"。放在main函数上调用一次可以了。然后包含上头文件time.h。
int main() {
srand((unsigned int)time(NULL)); //将当前系统时间的秒数作为随机生成序列的"种子"传给srand()
...
return 0;
}
现在测试一下电脑随机下棋的效果,成功!
4.6 输赢判断
当执行完玩家下棋/电脑下棋、棋盘展示,需要进行输赢判断。如果有一方赢了,另一方就不需要继续下棋了,直接宣布输赢的结果。如果没有赢,游戏需要继续,不断重复:玩家下棋——输赢判断——电脑下棋——输赢判断。
根据上面执行流程,可以设置一个while(1)的循环,当一方赢或者平局时,break跳出这个循环。可以让IsWin()判断后返回一个值,如果电脑赢返回字符'#',如果玩家赢返回'*',如果是平局,返回'Q',如果以上情况都不是,则游戏继续,返回'C'。下面是game()游戏的执行流程框架。
void game() {
char ret = 0; //设置输赢返回标记ret
char board[ROW][COL] = {0};
InitBoard(board, ROW, COL);
DisplayBoard(board,ROW,COL);
while (1) {
PlayerMove(board, ROW, COL);
DisplayBoard(board, ROW, COL);
ret = IsWin(board, ROW, COL); //玩家下棋后,进行输赢判断,返回一个值
if (ret != 'C') { //如果返回的不是继续游戏,即'!C'
break; //返回的不是继续,需要跳出循环
}
ComputerMove(board, ROW, COL);
DisplayBoard(board, ROW, COL);
ret = IsWin(board, ROW, COL); //玩家下棋后,进行输赢判断,返回一个值
if (ret != 'C') { //如果返回的不是继续游戏,即'!C'
break; //返回的不是继续,需要跳出循环
}
}
//玩家赢、电脑赢、平局的判断
if (ret == '*') {
printf("玩家赢\n");
} else if (ret == '#') {
printf("电脑赢\n");
} else {
printf("电脑赢\n");
}
}
通过IsWin()返回的结果和if进行判断结果决定是否跳出循环,就可以让游戏正常运行下去。
接下来进行输赢结果代码实现分析,用IsWin()表示。三子棋的输赢判断有三个维度:列成三子,横成三子,对角线成三子。如果其中一个维度赢了,就不需要继续判断了。
下面简单描述一下判断代码,以判断行为例,首先先判断第一行,再判断下一行,一行一行地判断。对于每一行,从第2列的值开始,分别和第1列值比较,如果两者相同,计数标志count就加1,当count值等于row - 1时,说明已经三行成子了,其中一方已经赢了,此时只需要返回该行任意一个值即可,当返回的是'*',说明是玩家赢了,当返回的是'#',说明电脑赢了。
对于对角线判断也是同样思路,分为反斜杠对角线‘\’和正斜杠对角线‘/’两种,对于反斜杠对角线‘\’,只需要比较棋盘board[0][0]、board[1][1]...board[row-1][col-1]的值;对于正斜杠对角线‘/’,只需要比较棋盘board[0][col-1]、board[1][col-2]...board[row-1][0]的值。对角线的判断,不需要像判断列或者行一样,需要设置两层循环,只要设置一层循环即可。
下面是判断输赢判断IsWin()代码的实现,同样也是从三子棋扩展到n子棋的判断,代码如下:
//判断棋盘是否为满
//返回1为棋盘落子为满
//返回0为棋盘落子不满
int IsFull(char board[COL][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;
}
//判断输赢
//IsWin返回'*'表示玩家赢
//IsWin返回'#'表示电脑赢
//IsWin'Q'表示平局
//IsWin返回'C'表示游戏继续
char IsWin(char board[ROW][COL], int row, int col) {
//判断行是否为赢
int i = 0;
int j = 0;
int count = 0;
for (i = 0; i < row; i++) { //判断一行,再判断下一行
for (j = 1; j < col; j++) { //从每行的第二个值开始比较
if (board[i][j] == ' ') { //如果该值等于空,说明该行没有赢,需要跳出循环
break; //跳出循环
}
else if (board[i][0] == board[i][j]) { //从每行的第二个值开始,和每行第1个值比较
count++; //如果发现两者值相同,count就加1
}
else if (board[i][0] != board[i][j]) { //如果要比较值不相等,也就是是'*'和'#',这种情况也需要跳出循环
break; //跳出循环
}
}
if (count == row - 1) { //当count累积的数量为row -1时,说明有一方已经赢了
return board[i][0]; //返回是赢方的标记,返回行的其中一个值即可
}
else {
count = 0; //判断一行结束后,需要count进行清零,方便下一行判断
}
}
//判断列是否为赢
int k = 0;
int l = 0;
count = 0;
for (k = 0; k < col; k++) { //判断一列,再判断下一列
for (l = 1; l < row; l++) { //从每列的第二个值开始比较
if (board[l][k] == ' ') { //如果该值等于空,说明该列没有赢,需要跳出循环
break;
}
else if (board[0][k] == board[l][k]) { //从每列的第二个值开始,和每列第1个值比较
count++; //如果发现两者值相同,count就加1
}
else if (board[0][k] != board[l][k]) { //如果要比较值不相等,也就是是'*'和'#',这种情况也需要跳出循环
break; //当count累积的数量为count -1时,说明有一方已经赢了
}
}
if (count == col - 1) { //当count累积的数量为col -1时,说明有一方已经赢了
return board[0][k]; //返回是赢方的标记,返回列的其中一个值即可
}
else {
count = 0; //判断一列结束后,需要count进行清零,方便下一行判断
}
}
//判断对角是否为赢
//反斜杠对角线判断(\)
int d = 0;
for (d = 1; d < row; d++) { //从第二值开始,分别和对角线的第一个值比较
if (board[d][d] == ' ') { //当该值为空时,说明该对角线棋子未满,需要跳出循环
break;
}
else if (board[0][0] == board[d][d]) { //如果要比较的值与第一个值相同,count就加1
count++;
}
else if (board[0][0] != board[d][d]) { //如果要比较值不相等,也就是是'*'和'#',这种情况也需要跳出循环
break;
}
}
if (count == row - 1) { //当count累积的数量为row -1时,说明有一方已经赢了
return board[0][0]; //返回是赢方的标记,返回该对角线的其中一个值即可
}
//斜杠对角线判断(\)
int n = 0;
count = 0;
for (n = 1; n < col; n++) { //从第二值开始,分别和对角线的第一个值比较
if (board[n][col - n - 1] == ' ') { //当该值为空时,说明该对角线棋子未满,需要跳出循环
break;
}
else if (board[0][col - 1] == board[n][col - n - 1]) { //如果要比较的值与第一个值相同,count就加1
count++;
}
else if (board[0][col - 1] != board[n][col - n - 1]) { //如果要比较值不相等,也就是是'*'和'#',这种情况也需要跳出循环
break;
}
}
if (count == col - 1) { //当count累积的数量为col -1时,说明有一方已经赢了
return board[0][col - 1];//返回是赢方的标记,返回该对角线的其中一个值即可
}
// 如果上面没有判断出哪一方赢了,就需要判断是否为平局
if (IsFull(board, row, col)) { //如果没有哪一方赢,并且此时棋盘满了,说明已经平局了
return 'Q'; //平局返回Q
}
return 'C'; //如果没有哪一方是赢,棋盘又没有满,游戏需要继续,返回'C'
}
4.7 输赢判断测试
写完IsWin()后,测试一下是否符合要求。
测试行是否赢,OK!
测试列是否赢,也OK!
测试对角线是否赢,都OK!
上面都输赢测试都通过!
5. 四子棋、五子棋等测试
写到这里,游戏程序实现的代码基本写完了,接下来测试一下,除了三子棋,四子棋是否能够成功运行起来,只需要将头文件game.h中常量定义的ROW和COL改成4即可,修改如下。
#define ROW 4
#define COL 4
测试一下四子棋运行效果:
测试一下五子棋运行效果:
#define ROW 5
#define COL 5
6. game.h内容
最后附上头文件game.h的内容
#pragma once
#define ROW 3
#define COL 3
#include <stdio.h>
#include <time.h>
//初始化棋盘
void InitBoard(char board[ROW][COL],int row,int col);
//显示棋盘
void DisplayBoard(char board[ROW][COL], int row, int col);
//玩家下棋
void PlayerMove(char board[ROW][COL], int row, int col);
//电脑下棋
void ComputerMove(char board[ROW][COL], int row, int col);
//判断输赢
char IsWin(char board[ROW][COL], int row, int col);
7. game.c内容
game.c文件中包括相关函数
#define _CRT_SECURE_NO_WARNINGS
#include "game.h"
//初始化棋盘
void InitBoard(char board[ROW][COL], int row, int col) {
...
}
//棋盘显示
void DisplayBoard(char board[ROW][COL], int row, int col) {
...
}
//玩家下棋
void PlayerMove(char board[ROW][COL], int row, int col) {
...
}
//电脑下棋
void ComputerMove(char board[ROW][COL], int row, int col) {
...
}
//判断棋盘是否为满
int IsFull(char board[COL][COL], int row, int col) {
...
}
//判断输赢
char IsWin(char board[ROW][COL], int row, int col) {
...
}
8. main.c内容
main.c文件中包括相关函数如下
#define _CRT_SECURE_NO_WARNINGS
#include "game.h"
void menu() {
...
}
void game() {
...
}
int main() {
...
}
正在学习C语言,文章有写不正确的地方,欢迎多多指正,谢谢!上面的代码会打包上传码云上gitte。链接:https://gitee.com/yiqixuexiba/Tic-Tac-Toe
注:文章中“五子棋”并非我们平常玩的五子棋,只是由三子棋扩展出来五子棋,哈哈!
相关参考:比特鹏哥的C语言的学习视频。