一. 主要思路
游戏的主要思路如下,一方面是我们整个的游戏思路,另一方面则是我们实现功能的游戏主体了。(写完博客才发现图上多了一道竖线,大家原谅我把==)
二.思路梳理
第一,同我们之前实现的扫雷一样,整个游戏里,棋盘的布置,用来布置棋盘的元素都是和我们后面息息相关的,一开始得先想好。
三子棋,棋盘是三行三列,但我们在判断输赢时会碰到元素越界的问题,仅仅三行三列够不够呢?
对于三子棋,甚至五子棋,再高一点,我们其实都可以用判断所有行所有列,对角线来判断输赢,越界问题也就不存在了。
第二, 我们用空格 ’ ’ 初始化棋盘是没问题的,但是下棋的元素会不会影响我们判断呢,上面说了可以用所有行列对角线来判断输赢,也就不需要字符相加减来计算,因此下棋的元素没有影响。
三.主体思路实现
在梳理完大概的问题后,就可以开始写代码了。最先的肯定是我们的大致结构了。如扫雷一般,函数声明以及一些常用变量放在一起,大致结构放在一个源文件里,游戏功能实现放在另一个源文件里。
大致结构就没什么好说的了,具体代码如下:
void menu()
{
printf("*****************\n");
printf("*****1.play******\n");
printf("*****0.exit******\n");
printf("*****************\n");
}
int main()
{
int input = 0;
printf("游戏开始\n");
srand((unsigned int)time(NULL));
do
{
menu();
printf("请输入:>\n");
scanf("%d", &input);
switch (input)
{
case 1:
printf("游戏开始\n");
//最开始可以先删除
game();
break;
case 0:
printf("退出游戏\n");
break;
default :
printf("输入错误\n");
break;
}
} while (input);
return 0;
}
四.初始化棋盘及布置
之前已经对棋盘初始化和布置的问题进行了说明,就不再赘述。这里布置包含了展示棋盘,毕竟布置完肯定得给玩家看,由于棋盘都是空格,所以可以加上一些横竖线对棋盘布置一下。 笔者定义了两个常量ROW,COL,都为3代表行列,*表示玩家下棋,#表示电脑下棋。具体代码如下:
void init_board(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 show_board(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
int n = 1;
for (i = 0; i < row; i++)
{
if (i > 0)
{
for (n = 0; n < col; n++)
{
printf("----");
}
printf("\n");
}
for (j = 0; j < col; j++)
{
printf("%c", board[i][j]);
if (j < col)
{
printf(" | ");
}
}
printf("\n");
}
printf("\n");
}
这里都没有什么要注意的地方,注意换行,对棋盘展示的结果不满意的也可以自己改改。
五.下棋
下棋分为两部分,一部分是玩家下棋,另一部分是电脑下棋,咱们默认玩家先下。
1 玩家下棋
玩家下棋就比较简单了,要注意是否越界,另一个就是坐标是不是已经被人下过了,具体代码如下:
void player1(char board[ROW][COL], int row, int col)
{
printf("玩家回合请输入\n");
int x = 0;
int y = 0;
int flag = 1;
while (flag)
{
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] = '*';
flag = 0;
}
}
else
{
printf("坐标越界,请重新输入\n");
}
}
}
这里我们判断的思路其实是有点取巧的,只要是空格那就能下,也有玩家故意在已经下了的地方下棋,碰到这种情况上述代码运行起来是没有提示的,但这也属于极端情况,这里只是作此说明。另外,我们看到棋盘自然想到3行3列,输入也是3行3列,实际是0到2,这个要注意一下。
2. 电脑下棋
在描述电脑下棋之前,我想告诉大家,电脑或者说计算机在执行命令时是缺少我们人类的常识的,我们认为该理所当然的东西在计算机这就犯了难,所以对一些判断条件要写的严格一点!
电脑下棋,需要随机值,生成随机值的方法以前说了,不再赘述,重点在于随机值要放进循环里,不然很可能只生成一个随机值,且不满足条件,代码就会卡住。具体代码如下:
void player2(char board[ROW][COL], int row, int col)
{
int flag = 1;
printf("电脑回合\n");
//注意电脑的判断范围和玩家有一点不一样,
while (flag)
{
//随机数的生成放在循环内,不然只生成了一个不满足条件的随机数就会卡在这
int x = rand() % 3;
int y = rand() % 3;
//对于是否能落子的条件判断一定写成等于' '就好,
if (x >= 0 && x <= row - 1 && y >= 0 && y <= col - 1)
{
if (board[x][y] == ' ')
{
board[x][y] = '#';
flag = 0;
}
}
}
}
电脑下棋的行列范围是0到2,要注意和玩家下棋的不同。
六.判断输赢
在判断输赢前我们先梳理一下思路。对于输赢,有三种结果,玩家赢,电脑赢,棋盘下满平局,那该先判断平局吗?
实际上会出现棋盘下满了但是有人获胜的可能,所以我们先判断输赢,再判断是否平局。
对于判断,首先是判断的方法,开头讲过,我们可以直接判断所有行和列及两条对角线。整个判断的流程就应该是玩家游戏,展示棋盘,接着判断,判断是否有人获胜,再判断是否和棋,电脑下棋也是这么判断。
按照这样的流程顺序走下去,那么我们每一方下棋都要判断两次,所以我们这里改变一下,在下棋的循环中只判断棋盘是否满了,如果满了,就跳出循环判断是否有人获胜,没人获胜再判断是否和棋,这样整个流程就简单一些。到此,整个游戏就完成了,源码如下:
game.h
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<string.h>
#include<time.h>
#include<stdlib.h>
#define ROW 3
#define COL 3
void init_board(char board[ROW][COL], int row, int col);
void show_board(char board[ROW][COL], int row, int col);
void player1(char board[ROW][COL], int row, int col);
void player2(char board[ROW][COL], int row, int col);
char is_win(char board[ROW][COL], int row, int col);
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"game1.h"
void menu()
{
printf("*****************\n");
printf("*****1.play******\n");
printf("*****0.exit******\n");
printf("*****************\n");
}
void game()
{
char board[ROW][COL] = { 0 };
char win = 0;
init_board(board, ROW, COL);
show_board(board, ROW, COL);
while (1)
{
//玩家下棋
player1(board, ROW, COL);
show_board(board, ROW, COL);
win = is_win(board, ROW, COL);
if (win != 'N')
{
//不等于n说明游戏无法进行下去了,就判断输赢
break;
}
//电脑下棋
player2(board, ROW, COL);
show_board(board, ROW, COL);
win = is_win(board, ROW, COL);
if (win != 'N')
{
break;
}
}
if (win == '*')
{
printf("玩家胜利\n");
}
if (win == '#')
{
printf("电脑胜利\n");
}
if (win == 'Y')
{
printf("平局\n");
}
}
int main()
{
int input = 0;
printf("游戏开始\n");
srand((unsigned int)time(NULL));
do
{
menu();
printf("请输入:>\n");
scanf("%d", &input);
switch (input)
{
case 1:
printf("游戏开始\n");
//最开始可以先删除
game();
break;
case 0:
printf("退出游戏\n");
break;
default :
printf("输入错误\n");
break;
}
} while (input);
return 0;
}
game.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"game1.h"
void init_board(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 show_board(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
int n = 1;
for (i = 0; i < row; i++)
{
if (i > 0)
{
for (n = 0; n < col; n++)
{
printf("----");
}
printf("\n");
}
for (j = 0; j < col; j++)
{
printf("%c", board[i][j]);
if (j < col)
{
printf(" | ");
}
}
printf("\n");
}
printf("\n");
}
void player1(char board[ROW][COL], int row, int col)
{
printf("玩家回合请输入\n");
int x = 0;
int y = 0;
int flag = 1;
while (flag)
{
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] = '*';
flag = 0;
}
}
else
{
printf("坐标越界,请重新输入\n");
}
}
}
void player2(char board[ROW][COL], int row, int col)
{
int flag = 1;
printf("电脑回合\n");
//注意电脑的判断范围和玩家有一点不一样,
while (flag)
{
//随机数的生成放在循环内,不然只生成了一个不满足条件的随机数就会卡在这
int x = rand() % 3;
int y = rand() % 3;
//对于是否能落子的条件判断一定写成等于' '就好,
if (x >= 0 && x <= row - 1 && y >= 0 && y <= col - 1)
{
if (board[x][y] == ' ')
{
board[x][y] = '#';
flag = 0;
}
}
}
}
int is_full(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 is_win(char board[ROW][COL], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
//判断条件写严格一点,不然会出现一些bug!!!
if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][0] != ' ')
{
return board[i][0];
}
/*if (board[i][0] == board[i][1] == board[i][2] && board[i][0] != ' ')
{
return board[i][0];
}*/
}
for (i = 0; i < col; i++)
{
if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[0][i] != ' ')
{
return board[0][i];
}
/* if(board[0][i] == board[1][i] == board[2][i] && board[0][i] != ' ')
{
return board[0][i];
}*/
}
if ((board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[1][1] != ' ') ||
(board[2][0] == board[1][1] && board[1][1] == board[0][2] && board[1][1] != ' '))
{
return board[1][1];
}
/*if ((board[0][0] == board[1][1] == board[2][2] && board[1][1] != ' ') ||
(board[2][0] == board[1][1] == board[0][2] && board[1][1] != ' '))
{
return board[1][1];
}*/
int n = is_full(board, ROW, COL);
if (n == 1)
{
//棋盘满了,返回y,没满返回n,不等于n说明游戏无法进行下去了,接着判断输赢
return 'Y';
}
else
{
return 'N';
}
}
七.代码优化
对于上述实现的三子棋,其实有很多可以再优化的地方。
第一, 玩家和电脑随先下棋的问题,实际上可以利用随机数来判断谁先下棋,也还算好实现。
第二,棋盘的扩展问题,因为我们定义的是全局变量,玩家如果有意愿的话,实际上也可以做到4子棋,5子棋等等,而我们后续的判断条件上仅仅是考虑了三子棋,没有考虑到ROW, COL的变化,涉及到这一点再来写判断条件就会比较困难了,各位有兴趣的可以试试。
最后还是要提一嘴,对于这样有计算机参与的游戏,与玩家不同,计算机进行游戏时代码的判断一定要非常严格,非常具体,不然就很容易出现一些因为语法的问题而发生的错误,正例如笔者在判断输赢is_win函数内写了两个判断条件,被注释掉的判断条件就是不太严谨的,容易因为语法出现bug。这也算是笔者最近写三子棋和扫雷最深刻的体会了(因为这个花了好多时间==)。