前情提要
在学习了数组之后,我们了解了数组的创建,掌握了数组在内存中的存放,也认识到了一维数组和二维数组的结构,因此,下面就让我们用二维数组来实战一下,利用所学,实现一个简易的三子棋游戏,Let’s go。
一.代码展示部分
三子棋.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
void game()
{
int tmp;
char board[ROW][COL];
initialboard(board);//棋盘初始化
printboard(board, ROW, COL);//打印棋盘
while (1)//下多次
{
printf("玩家下棋:\n");
playermove(board, ROW, COL);
printboard(board, ROW, COL);
tmp = judgefull(board, ROW, COL);
if (tmp == 0)
{
printf("棋盘已满\n");
break;
}
tmp = judgewin(board, ROW, COL);
if (tmp == 0)
{
printf("玩家胜!\n");
break;
}
printf("电脑下棋:\n");
computermove(board, ROW, COL);
printboard(board, ROW, COL);
tmp = judgefull(board, ROW, COL);
if (tmp == 0)
{
printf("棋盘已满\n");
break;
}
tmp = judgewin(board, ROW, COL);
if (tmp == 0)
{
printf("电脑胜!\n");
break;
}
}
}
void menu()
{
printf("************************\n");
printf("******* 1.play *******\n");
printf("******* 0.exit *******\n");
printf("************************\n");
}
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");
}
}
} while (input);
return 0;
}
game.c
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"//引头文件
void initialboard(char board[ROW][COL])
{
int i, j;
for (i = 0; i < ROW; i++)
{
for (j = 0; j < COL; j++)
{
board[i][j] = ' ';
}
}
}
//棋盘初始化
void printboard(char board[ROW][COL], int row, int col)
{
int i, j;
for (i = 0; i < row; i++)
{
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 i, j;
printf("请输入您要下的位置坐标>:");
scanf("%d%d", &i, &j);
if (i >= 1 && i <= row&&j >= 1 && j <= col)
{
if (board[i - 1][j - 1] == ' ')
{
board[i - 1][j - 1] = '*';
}
else
{
printf("此坐标对应位置已被下过,请重下!\n");
}
}
else
{
printf("坐标非法,请重新输入!\n");
}
}
//玩家下棋
void computermove(char board[ROW][COL], int row, int col)
{
int i, j;
while (1)
{
i = rand() % row;//生成随机坐标
j = rand() % col;
if (board[i][j] == ' ')
{
board[i][j] = '!';
break;
}
}
}
//电脑下棋
int judgefull(char board[ROW][COL], int row, int col)
{
int i, j;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
if (board[i][j] == ' ')
return 1;
}
}
return 0;
}
//判断棋盘是否下满
int judgewin(char board[ROW][COL], int row, int col)
{
int i, j, k;
for (i = 0; i < row; i++)//行
{
for (j = 0; j < col - 2; j++)
{
if (board[i][j] == board[i][j + 1] && board[i][j] == board[i][j + 2] && board[i][j] != ' ')
return 0;
}
}
for (i = 0; i < col; i++)//列
{
for (j = 0; j < row - 2; j++)
{
if (board[j][i] == board[j + 1][i] && board[j][i] == board[j + 2][i] && board[j][i] != ' ')
return 0;
}
}
for (i = 0; i < row - 2; i++)//主对角线
{
for (j = i; j < col - 2; j++)
{
if (board[i][j] == board[i + 1][j + 1] && board[i][j] == board[i + 2][j + 2] && board[i][j] != ' ')
return 0;
}
}
for (i = 0; i < row - 2; i++)//副对角线
{
for (j = col - i; j > 1; j--)
{
if (board[i][j] == board[i + 1][j - 1] && board[i][j] == board[i + 2][j - 2] && board[i][j] != ' ')
return 0;
}
}
for (i = 1; i < row - 2; i++)//主对角线下部分
{
for (j = 0, k = i; j < col - 2; j++)
{
if (i - j == k)
{
if (board[i][j] == board[i + 1][j + 1] && board[i][j] == board[i + 2][j + 2] && board[i][j] != ' ')
return 0;
}
}
}
for (j = 1; j < col - 2; j++)//主对角线上部分
{
for (i = 0, k = j; i < row - 2; i++)
{
if (j - i == k)
{
if (board[i][j] == board[i + 1][j + 1] && board[i][j] == board[i + 2][j + 2] && board[i][j] != ' ')
return 0;
}
}
}
for (i = 0; i < row - 2; i++)//副对角线上部分
{
for (j = col - 2, k = j; j > 1; j--)
{
if (i + j == k)
{
if (board[i][j] == board[i + 1][j - 1] && board[i][j] == board[i + 2][j - 2] && board[i][j] != ' ')
return 0;
}
}
}
for (i = row - 1, k = 1; i > 1; i--, k++)//副对角线下部分
{
for (j = k; j < col - 2; j++)
{
if (board[i][j] == board[i - 1][j + 1] && board[i][j] == board[i - 2][j + 2] && board[i][j] != ' ')
return 0;
}
}
}
//判断输赢
game.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include <stdlib.h>
#include<time.h>
#define ROW 5
#define COL 5
void game();
//游戏声明
void initialboard(char board[ROW][COL]);
//棋盘初始化声明
void printboard(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 judgefull(char board[ROW][COL], int row, int col);
//判断棋盘是否满了
int judgewin(char board[ROW][COL], int row, int col);
//判断输赢
当看到这一堆的代码时,想必大家脑袋瓜子嗡嗡的吧!
没关系,下面我带大家逐句理解。
二.分析代码部分
由于实现三子棋较为复杂,代码量相对比较多,所以我们分模块来写。三子棋总的有三个模块——三子棋.c、game.c、game.h。
模块 | 功能 |
---|---|
三子棋.c | 主要实现三子棋游戏的逻辑和结构 |
game.c | 主要实现三子棋游戏的具体实现 |
game.h | 定义声明三子棋的所用到函数 |
[1]三子棋.c部分
既然在三子棋.c模块下我们要实现三子棋游戏的逻辑和结构,所以游戏执行的流程我们一定要清楚。
试想一下,我们在玩游戏的时候,最先看到的是一个游戏的界面,界面上有一个选项卡供我们选择,我们可选择开始游戏或是退出游戏。并且,一旦开始游戏,我们可以玩好几把,也可以选择退出游戏。
想到这,我们大概就知道这里要用到循环语句和分支语句。那么下面让我们来实现一下:
void menu()//选项卡
{
printf("************************\n");
printf("******* 1.play *******\n");
printf("******* 0.exit *******\n");
printf("************************\n");
}
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");
}
}
} while (input);//若input=0,判断为假,终止循环,退出游戏
return 0;
}
由上面代码,我们实现了三子棋游戏的基本结构
从代码中我们可以看到,正如我们所想的,用到了循环和分支语句。
循环这里用到的是do···while语句,因为我们至少要进行一次选择是否要开始游戏。
分支这里用到的是switch语句,因为这里有三种情况,用switch语句结构会更加清晰。
完成了三子棋.c的基本结构后,我们还要实现它的游戏逻辑。
试想一下,在我们玩三子棋时,首先得要有棋盘吧,所以我们要创建一个棋盘,并且要有一个函数将棋盘打印出来让我们看到,这样我们才知道将棋下在哪儿。
还有,最初的棋盘不能有别的东西,也不能有棋子,所以我们还得初始一下棋盘。
接着,有了棋盘,我们开始下棋,下了棋后,我们要知道棋下在哪了,并且我们也要知道电脑下哪了,所以在双方落子后要将棋子的位置存起来,并将棋盘再次打印出来,让玩家看到。
再来,游戏总得有输赢,所以在双方落子后要判断有没有获胜,如果有一方获胜,则本轮游戏结束(注意不是退出游戏);否则游戏继续。还有一个特殊情况,就是棋盘下满了也没有决出胜负,所以还要判断棋盘是否满了,若满了,则双方平局,本轮游戏结束。
通过上述的分析,我们知道了我们要做什么了,所以我们用到一些函数来帮我们完成我们要完成的事情。
#define ROW 5
#define COL 5
//棋盘大小
void game();
//实现游戏
void initialboard(char board[ROW][COL]);
//实现棋盘初始化
void printboard(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 judgefull(char board[ROW][COL], int row, int col);
//判断棋盘是否满了
int judgewin(char board[ROW][COL], int row, int col);
//判断输赢
定义好这些个函数后,我们就可以干大事了
#include"game.h"//引头文件
void game()
{
int tmp;
char board[ROW][COL];
initialboard(board);//棋盘初始化
printboard(board, ROW, COL);//打印棋盘
while (1)//下多次,直到决出胜负(或下满)
{
printf("玩家下棋:\n");
playermove(board, ROW, COL);//玩家下
printboard(board, ROW, COL);//打印下的结果
tmp = judgefull(board, ROW, COL);//判断满没
if (tmp == 0)
{
printf("棋盘已满\n");
break;
}
tmp = judgewin(board, ROW, COL);//判断赢没
if (tmp == 0)
{
printf("玩家胜!\n");
break;
}
printf("电脑下棋:\n");
computermove(board, ROW, COL);//电脑下
printboard(board, ROW, COL);//打印下的结果
tmp = judgefull(board, ROW, COL);//判断满没
if (tmp == 0)
{
printf("棋盘已满\n");
break;
}
tmp = judgewin(board, ROW, COL);//判断赢没
if (tmp == 0)
{
printf("电脑胜!\n");
break;
}
}
}
[2]game.c部分
game.c部分是整个项目最核心的部分,也是最难的部分,但只要掌握了二维数组行列下标的几个关系,也就迎刃而解了。
下面,我们来逐一分析:
(1)棋盘初始化——initialboard(board)
先亮代码
void initialboard(char board[ROW][COL])
{
int i, j;
for (i = 0; i < ROW; i++)
{
for (j = 0; j < COL; j++)
{
board[i][j] = ' ';
}
}
}
//棋盘初始化
因为最开始棋盘什么也没有,但棋盘上要有下棋的空间,所以我们最开始给棋盘赋空格(‘ ’),并将棋盘的内容存到board[ROW][COL]数组中,这样我们之后就方便对棋盘进行操作了(也就是下棋)。
(2)棋盘打印——printboard(board, ROW, COL)
先亮代码
void printboard(char board[ROW][COL], int row, int col)
{
int i, j;
for (i = 0; i < row; i++)
{
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");
}
}
首先,我们打印棋盘,要知道棋盘的样子。在这,我设计的棋盘是由竖杠和减号构成的:
可以看到,棋盘中的空格是用来落子的,棋子的两侧各有一个空格。
我们还可知,要打印棋盘一定要用到循环,且为二重循环。
所以我们可以将“ %c ”(%c为下的内容),“|”,"—"看做小的循环单位,将每两行看做一个大的循环单位。
(3)玩家下棋——playermove(board, ROW, COL)
先亮代码
void playermove(char board[ROW][COL], int row, int col)
{
int i, j;
printf("请输入您要下的位置坐标>:");
scanf("%d%d", &i, &j);
if (i >= 1 && i <= row&&j >= 1 && j <= col)
{
if (board[i - 1][j - 1] == ' ')
{
board[i - 1][j - 1] = '*';
}
else
{
printf("此坐标对应位置已被下过,请重下!\n");
}
}
else
{
printf("坐标非法,请重新输入!\n");
}
}
玩家是通过输入棋盘坐标来实现下棋的,并且只能下在没有下过的地方,所以需要判断下的位置之前有没有落子(若是空格,则之前没有落子)。
这里还要注意棋盘坐标和数组下标的区别。
(4)电脑下棋——computermove(board, ROW, COL)
先亮代码
void computermove(char board[ROW][COL], int row, int col)
{
int i, j;
while (1)
{
i = rand() % row;//生成随机坐标
j = rand() % col;
if (board[i][j] == ' ')
{
board[i][j] = '!';
break;
}
}
}
因为实现的是简易的三子棋,所以在这电脑下棋是随机的,所以这里用到了rand()来生成随机数,同样需要判断所下位置是否有被下过。
(5)判断棋盘是否满了——judgefull(board, ROW, COL)
先亮代码
int judgefull(char board[ROW][COL], int row, int col)
{
int i, j;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
if (board[i][j] == ' ')
return 1;
}
}
return 0;
}
判断棋盘是否满了很简单,就是遍历一遍数组,看看有没有空格。若没有,则棋盘满了,返回0;否则没满,返回1;
(6)判断输赢——judgewin(board, ROW, COL)(重头戏)
先亮代码
int judgewin(char board[ROW][COL], int row, int col)
{
int i, j, k;
for (i = 0; i < row; i++)//行
{
for (j = 0; j < col - 2; j++)
{
if (board[i][j] == board[i][j + 1] && board[i][j] == board[i][j + 2] && board[i][j] != ' ')
return 0;
}
}
for (i = 0; i < col; i++)//列
{
for (j = 0; j < row - 2; j++)
{
if (board[j][i] == board[j + 1][i] && board[j][i] == board[j + 2][i] && board[j][i] != ' ')
return 0;
}
}
for (i = 0; i < row - 2; i++)//主对角线
{
for (j = i; j < col - 2; j++)
{
if (board[i][j] == board[i + 1][j + 1] && board[i][j] == board[i + 2][j + 2] && board[i][j] != ' ')
return 0;
}
}
for (i = 0; i < row - 2; i++)//副对角线
{
for (j = col - i; j > 1; j--)
{
if (board[i][j] == board[i + 1][j - 1] && board[i][j] == board[i + 2][j - 2] && board[i][j] != ' ')
return 0;
}
}
for (i = 1; i < row - 2; i++)//主对角线下部分
{
for (j = 0, k = i; j < col - 2; j++)
{
if (i - j == k)
{
if (board[i][j] == board[i + 1][j + 1] && board[i][j] == board[i + 2][j + 2] && board[i][j] != ' ')
return 0;
}
}
}
for (j = 1; j < col - 2; j++)//主对角线上部分
{
for (i = 0, k = j; i < row - 2; i++)
{
if (j - i == k)
{
if (board[i][j] == board[i + 1][j + 1] && board[i][j] == board[i + 2][j + 2] && board[i][j] != ' ')
return 0;
}
}
}
for (i = 0; i < row - 2; i++)//副对角线上部分
{
for (j = col - 2, k = j; j > 1; j--)
{
if (i + j == k)
{
if (board[i][j] == board[i + 1][j - 1] && board[i][j] == board[i + 2][j - 2] && board[i][j] != ' ')
return 0;
}
}
}
for (i = row - 1, k = 1; i > 1; i--, k++)//副对角线下部分
{
for (j = k; j < col - 2; j++)
{
if (board[i][j] == board[i - 1][j + 1] && board[i][j] == board[i - 2][j + 2] && board[i][j] != ' ')
return 0;
}
}
}
(判断输赢是最难的一个小模块,但只要理清楚行列(i,j)之间的关系,从而找到相应的位置,就简单很多了。)
要判断输赢,首先我们要了解游戏规则,即只要连续的方格内的棋子相同,并且不是空格,则获胜。连续的方格可以是横的,竖的,斜的都行。所以下面我们就考虑这三种情况是否满足条件。(以5*5的棋盘为例,并且棋盘为正方形)
1.考虑每列(竖向)
需要注意,如果用 i 表示行, j 表示列,则在写循环的控制条件是要写
i<row-2;j<col (row表示棋盘有多少行,col表示棋盘有多少列) ,因为每一列的判断是连续判断三个数,若是惯性思维写i<row,j<col的话会有越界的问题,包括下面所有情况的越界问题都相同,只需注意 i , j 的取值范围即可,在下面的内容中不在赘述。( i , j 的初值为0,因为在这里,遍历从[0][0]开始)。
for (i = 0; i < col; i++)//列
{
for (j = 0; j < row - 2; j++)
{
if (board[j][i] == board[j + 1][i] && board[j][i] == board[j + 2][i] && board[j][i] != ' ')
return 0;
}
2.考虑每行
与考虑每列的情况同理,此情况也需要考虑越界问题。此处比较简单,读者自己写一写。
for (i = 0; i < row; i++)//行
{
for (j = 0; j < col - 2; j++)
{
if (board[i][j] == board[i][j + 1] && board[i][j] == board[i][j + 2] && board[i][j] != ' ')
return 0;
}
}
3.考虑斜的情况(较为复杂,需耐心)
由于这种情况较为复杂,所以在这里进行了分类讨论:
【1】主对角线
【2】副对角线
【3】主对角线上侧
【4】主对角线下侧
【5】副对角线上侧
【6】副对角线下侧
【1】主对角线情况
由上图所示,如果在主对角线上若有连续三个棋子相同且不是空格则获胜。
通过观察,主对角线上的下标 i = j 恒成立,根据 i , j的关系可找到主对角线上的所有位置。
(此处同样要考虑越界问题)
for (i = 0; i < row - 2; i++)//主对角线
{
for (j = i; j < col - 2; j++)
{
if (board[i][j] == board[i + 1][j + 1] && board[i][j] == board[i + 2][j + 2] && board[i][j] != ' ')
return 0;
}
}
【2】副对角线情况
由上图所示,如果在副对角线上若有连线三个棋子相同且不是空格则获胜。
通过分析,我们发现 i + j 是一个定值,这个定值为row(或col)。根据这个条件即可找到副对角线上所有位置。与判断主对角线不同的是,此处是从
[0][4]开始遍历(准确来说是从[0][col]开始,只是在这里以5*5的棋盘为例)。(考虑越界问题)
for (i = 0; i < row - 2; i++)//副对角线
{
for (j = col - i; j > 1; j--)
{
if (board[i][j] == board[i + 1][j - 1] && board[i][j] == board[i + 2][j - 2] && board[i][j] != ' ')
return 0;
}
}
【3】主对角线上侧情况
如果获胜出现在主对角线上侧该如何来判断呢?
i , j 有什么关系呢?
因为此获胜情况三个的斜的方向要与主对角线一致,所以棋子就不可能出现在[0][3]、[0][4]、[1][4]这三个位置,因为在此情况下这三个位置不能形成三子棋,所以棋子能下的位置为下图红色框区域里所包含的位置:
所以下面我们要做的就是找到红色框中的位置,只有找到了这些位置,才能对位置上的棋子进行判断。通过对二维数组下标的观察,不难发现,如果遍历从[0][1]开始,蓝色杠上的位置 j - i = 1,绿色杠上的位置 j - i = 2。所以我们可以创建一个变量k,赋它初值为1时,就找到了蓝色杠上所对应的位置,因为 j 的初值也为1,并且循环一次要加1,所以让k = j,判断 j - i 是否等于 k 就能把红色框中的所有位置都找到。(注意越界问题)
for (j = 1; j < col - 2; j++)//主对角线上部分
{
for (i = 0, k = j; i < row - 2; i++)
{
if (j - i == k)
{
if (board[i][j] == board[i + 1][j + 1] && board[i][j] == board[i + 2][j + 2] && board[i][j] != ' ')
return 0;
}
}
}
【4】主对角线下侧情况
此情况与【3】大致相同,要找到 i 和 j 的关系,再通过k找到所有符合条件的位置。只不过遍历的起点变了。(注意越界问题)
for (i = 1; i < row - 2; i++)//主对角线下部分
{
for (j = 0, k = i; j < col - 2; j++)
{
if (i - j == k)
{
if (board[i][j] == board[i + 1][j + 1] && board[i][j] == board[i + 2][j + 2] && board[i][j] != ' ')
return 0;
}
}
}
【5】副对角线上侧
考虑了主对角线的上下侧,那副对角线上下侧 i , j 又有什么规律呢?
不难看出,如要满足与副对角线斜的方向一致,并且要形成三子棋,则棋子只能落在橘框中的空格中。同样的,我们要找到橘框里的位置,通过分析橘框中数组下标,若从[0][3]开始遍历,绿色杠上的位置 i + j = 3(或col - 2),蓝色杠上的位置 i + j = 2(或col - 3)(注意越界问题)。同样,i + j 的值与col 有关,而col与 j 有关,所以创建 k = j ,判断 j + i 是否等于 k 就能把橘色框中的所有位置都找到。(注意越界问题)
for (i = 0; i < row - 2; i++)//副对角线上部分
{
for (j = col - 2, k = j; j > 1; j--)
{
if (i + j == k)
{
if (board[i][j] == board[i + 1][j - 1] && board[i][j] == board[i + 2][j - 2] && board[i][j] != ' ')
return 0;
}
}
}
【6】副对角线下侧
首先指出,我们从 [4][1]开始遍历。通过观察,不难发现每一行中,j 不断加1,并且随着行标的变小,j 的初值也再加1,所以我们可以创建变量 k,让 j = k ,并赋 k 初值为1,之后再通过循环改变 k 的值即可找到橘色框中的所有位置。(注意越界问题)
for (i = row - 1, k = 1; i > 1; i--, k++)//副对角线下部分
{
for (j = k; j < col - 2; j++)
{
if (board[i][j] == board[i - 1][j + 1] && board[i][j] == board[i - 2][j + 2] && board[i][j] != ' ')
return 0;
}
}
[3]game.h部分
这一部分主要用来声明一下我们所用到的函数,并且棋盘的大小也在此处定义。
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include <stdlib.h>
#include<time.h>
#define ROW 5
#define COL 5
void game();
//游戏声明
void initialboard(char board[ROW][COL]);
//棋盘初始化声明
void printboard(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 judgefull(char board[ROW][COL], int row, int col);
//判断棋盘是否满了
int judgewin(char board[ROW][COL], int row, int col);
//判断输赢
三.代码运行部分
笔者水平有限,若有错误或不足,还望指出,深表感谢!