自学习C语言来第一次面对"大工程",首先我们得好好的进行规划。
一.菜单
作为一个游戏,玩家进去后首先应该有一个简易的界面,对玩家进行选项提示,这里简单的编写一个菜单函数,代码实现如下:
void menu()
{
printf("************三子棋小游戏************\n");
printf("************ 1.play ************\n");
printf("************ 0.exit ************\n");
printf("************ V1.0.0 ************\n");
}
四行打印,简单的提示了相关信息,并且选项为输入1进入游戏,输入0结束游戏
二.菜单选项基本运行逻辑
即打印界面后,玩家输入1开始游戏,且游戏结束后不应直接退出,可让玩家再次进行选择,输入0直接结束游戏,若输入其他数字,则玩家输入错误,应提示重新输入,同样可让玩家再次进行选择。
因此,代码实现,首先定义变量接收输入值,然后用switch语句进行分支选择,在最外层套一个循环以达到使玩家重复输入的目的,代码实现如下:
int input = 0;
do
{
printf("请输入选项>:\n");
scanf("%d", &input);
switch (input)
{
case 1:
printf("对局开始 玩家方:* 电脑方: #\n");
game();//游戏实现
break;
case 0:
printf("游戏结束,欢迎下次游玩\n");//exit
break;
default:
printf("输入错误,请重新输入\n");
break;
}
}
while (input);
玩家输入1,则提示开始游戏,进入game()函数,这也是随后应重点实现的内容。玩家输入0,则while条件为假,游戏结束。至此基本运行逻辑已经实现,值得一提的是,此处比较容易犯的错误是将scanf语句放在循环外面,使得不能达到再次输入的目的,形成死循环
三.game()函数-游戏的实现
第三部分需实现游戏的运行,较为复杂,是程序的重中之重,为此,我们最好创建新的源文件game.c和对应头文件game.h进行编写
来到game()函数内部,经过简单的思考,游戏的实现大概分为以下几个部分:
1.棋盘 2.玩家/电脑输入 3.判断正负
1.棋盘实现
1.三子棋的棋盘,可抽象为二维的坐标,因此可用二维数组实现。
#define ROW 4
#define COL 4
char board[ROW][COL] = { 0 };
简单的一行代码,就是之后我们处处都需要使用到的三子棋棋盘。这里ROW,COL为标识符常量,应放在头文件里用#define定义,这里为便于理解将其放在一起。这样做的好处是,对于后面处处需要使用的棋盘,当我们想要更改棋盘大小时,只需更改ROW,COL的定义,而非一个个的进行修改。虽然三子棋限定了3*3棋盘,但不妨想想五子棋和扫雷,能自由方便的修改棋盘大小自然是更好的。
2.接着我们对棋盘进行初始化,在一开始棋盘上并未有人落子,每个点位在棋盘打印时最好打印的是' ',因此我们通过遍历将二维数组元素全部初始化为' '。
void Init_board(char board[ROW][COL])
{
int i = 0;
int j = 0;
for (i = 0; i < ROW; i++)
{
for (j = 0; j < COL; j++)
{
board[i][j] = ' ';
}
}
}
比较简单的实现了一个初始化函数,只需进行调用即可。
3.接下来着手实现棋盘的打印,棋盘由数组元素和分割线组成,分割线可以使让棋盘看起来更美观,下面先给出代码实现:
void print_board(char board[ROW][COL])
{
int i = 0;
int j = 0;
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");
}
}
}
效仿二维数组的打印,棋盘的打印由两个变量控制,外层循环变量i控制行,内层变量j控制列
图1.
图2.
行的打印,包含棋盘列和分割线,可将其分成i-1行的图1打印和第i行的图2打印,因此在最外层循环的内部,用两个for循环,第一个实现棋盘列的打印,第二个实现分割线的打印,又因当最后一行时不需打印分割线,因此第二个for循环用if语句控制少打印一行
图1.
图2.
列的打印,则两个for循环内部进行实现。首先是棋盘列,行的打印思路一致,可将其分成j-1行列图1打印和第j列的图2打印。
for (j=0;j<COL;j++)
{
printf(" %c ", board[i][j]);
if (j<COL-1)
printf("|");
}
printf("\n");
即可,记住打印完后进行换行
分割线同理
for (j=0;j<COL;j++)
{
printf("---");
if (j < COL - 1)
printf("|");
}
至此,棋盘的打印已然实现,第一部分的棋盘实现结束
2.玩家/电脑落子 玩家:'*' 电脑:'#'
思路很简单,就是玩家输入坐标,然后对数组相应元素进行修改,接着打印修改之后的棋盘即可。需要注意的是,在玩家看来的1-3列,实际上对应着数组下标的0-2。
对于玩家输入,需要判断输入的合法性。即 1.输入值越过棋盘范围,此时为无效输入,应让玩家重新输入。 2.输入坐标已有棋子,此时也为无效输入,需重新输入。直到输入正确,执行相应操作,基本逻辑与菜单选项逻辑相似,下面为代码实现:
void player_input(char board[ROW][COL])
{
int x = 0;
int y = 0;
printf("请玩家落子>: (输入坐标:x,y):\n");
while (1)
{
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");
}
}
对于电脑输入,则用rand()函数产生随机数,然后判断产生坐标是否已有棋子即可,注意需用srand函数进行随机数发生器的初始化,可放在程序开始位置,注意前文代码中没有写出
有人可能就疑惑了:电脑随机落子,那这不成了人工智障了吗?
没办法,目前水平有限,简易实现只能想到随机数生成的方法
void computer_input(char board[ROW][COL])
{
printf("电脑落子>: \n");
while (1)
{
int x = rand() % ROW + 1;
int y = rand() % COL + 1;
if (x >= 1 && x <= ROW && y >= 1 && y <= COL)
{
if (board[x - 1][y - 1] == ' ')
{
board[x - 1][y - 1] = '#';
break;
}
}
}
}
至此玩家输入/输出已经实现,第二部分结束
3.输赢判断
第三部分也是最复杂的一部分。首先应明白,当每一次玩家/电脑输入后,只有以下几种结果:
1.玩家赢/电脑赢,即满足胜利条件(存在横行or竖列or斜对角全为*/#),游戏结束
2.平局,即无任何一方满足胜利条件,但棋盘已满,游戏结束
3.不属于前两种情况,只是单纯的落子,游戏继续
下面先给出我的代码实现:
char is_win(char board[ROW][COL], char ch)
{
int i = 0;
int j = 0;
for (i = 0; i < ROW; i++)//横三行
{
int flag = 0;
for (j = 0; j < COL-1; j++)
{
if (board[i][j] != board[i][j + 1] || board[i][j] != ch)
{
flag = 1;
break;
}
}
if (flag == 0)
return ch;
}
for (j = 0; j < COL; j++)//竖三列
{
int flag = 0;
for (i = 0; i < ROW - 1; i++)
{
if (board[i][j] != board[i + 1][j] || board[i][j] != ch)
{
flag = 1;
break;
}
}
if (flag == 0)
return ch;
}
int flag = 0;
for (i = 0; i < ROW - 1; i++)//右斜
{
for (j = 0; j < COL - 1; j++)
{
if (i == j)
{
if (board[i][j] != board[i + 1][j + 1] || board[i][j] != ch)
{
flag = 1;
break;
}
}
}
}
if (flag == 0)
return ch;
flag = 0;
for (i = 1; i < ROW; i++)//左斜
{
for (j = 0; j < COL-1; j++)
{
if (i + j == ROW - 1)
{
if (board[i][j] != board[i - 1][j + 1] || board[i][j] != ch)
{
flag = 1;
break;
}
}
}
}
if (flag == 0)
return ch;
else if (is_full(board) == ' ')
return 'C';
else
return 'E';
}
函数参数部分除了必要的棋盘传参,还定义了ch接收字符,这是为了让玩家和电脑都能通过调用这个函数判断输赢,返回类型定义字符类型,针对不同情况返回不同信息
可以看到,我把它分为了: 1.行的判断 2.列的判断 3.右下对角线的判断 4.左下对角线的判断 5.平局的判断 6.不满足前面任意一种,游戏继续
这里没有简单的用3行/3列/对角线3个元素进行判断,而是通过循环变量实现,主要是为了当棋盘大小改变时也能应用三子棋的规则游戏,即每行,每列....(虽然说完全不对吧,就算是五子棋也不应该是这个下法啊,虽然姑且想实现一下,但是中间写出bug调试的时候简直要命啊!!!)
横行的判断
for (i = 0; i < ROW; i++)//横三行
{
int flag = 0;
for (j = 0; j < COL-1; j++)
{
if (board[i][j] != board[i][j + 1] || board[i][j] != ch)
{
flag = 1;
break;
}
}
if (flag == 0)
return ch;
}
首先是最基本的两层循环,我的思路是,对每一行的元素两两之间进行判断如果1.存在两元素不相等,胜利条件不成立 2.即便两元素相等,但存在元素不等于'*',可能全为' '或是其他情况,总之胜利条件不成立
遍历每行,如果条件不满足直接跳出,定义一个flag变量判断跳出后的情况,如果上述1,2全部不成立,即能确保这一行全部都为*,玩家胜,返回字符ch
同理,列的判断
for (j = 0; j < COL; j++)//竖三列
{
int flag = 0;
for (i = 0; i < ROW - 1; i++)
{
if (board[i][j] != board[i + 1][j] || board[i][j] != ch)
{
flag = 1;
break;
}
}
if (flag == 0)
return ch;
}
接下来是对角线的判断:
斜右下对角线 ,我的思路是遍历数组,然后判断(行数==列数)的元素是否满足条件,即(1,1),(2,2).....(n,n),这些元素便是斜右下对角线元素,其余思路与行的判断一致
int flag = 0;
for (i = 0; i < ROW - 1; i++)//右斜
{
for (j = 0; j < COL - 1; j++)
{
if (i == j)
{
if (board[i][j] != board[i + 1][j + 1] || board[i][j] != ch)
{
flag = 1;
break;
}
}
}
if (flag == 1)
break;
}
if (flag == 0)
return ch;
此处flag定义位置必须放在循环外部,因为循环必须遍历完对角线后进行if判断
斜左下对角线同理,依旧遍历数组,假如给n*n的二维坐标,容易发现,左斜下对角线的坐标满足(i+j==n+1),但由于是数组,故最终条件为(i+j==ROW-1)
flag = 0;
for (i = 1; i < ROW; i++)//左斜
{
for (j = 0; j < COL-1; j++)
{
if (i + j == ROW - 1)
{
if (board[i][j] != board[i - 1][j + 1] || board[i][j] != ch)
{
flag = 1;
break;
}
}
}
if (flag == 1)
break;
}
需要注意,对角线遍历时变量并不是从0开始循环到第n个元素,而是经过了相应的处理,这是为了防止数组越界。如果斜右下对角线循环时循环到第n个元素,那么n+1显然越界
胜利条件判断完毕,接下来是平局条件
if (is_full(board) == ' ')
return 'C';
用is_full函数判断棋盘是否已满,若未满则设定返回字符'C'
下面实现is_full函数,遍历即可
char is_full(char board[ROW][COL])
{
int i = 0;
int j = 0;
for (i = 0; i < ROW; i++)
{
for (j = 0; j < COL; j++)
{
if (board[i][j] == ' ')
return ' ';
}
}
return 'E';
}
若未满返回字符' ',此时上文条件成立,返回'C'
否则上述情况均不满足,设定返回字符'E',表示已满,即平局
胜负判断至此已完成,现在有了返回值之后,我们需要在game()函数内部进行情况判断,首先如果继续进行,则重复输入,循环实现,否则跳出循环,和菜单逻辑实现依然类似
while (1)
{
player_input(board);
//玩家输入
print_board(board);
ret = is_win(board, '*');
if (ret != 'C')
break;
//电脑输入
computer_input(board);
print_board(board);
ret = is_win(board, '#');
if (ret != 'C')
break;
}
若上文函数返回值为C,游戏继续,则进行下一次循环,否则跳出后,依靠胜负情况判断输出对应结果
//3.判断正负 1.棋盘未满,胜负未分,继续! 2.胜负已分 - 玩家或电脑 3.棋盘满,平局 return game
//输出结果
if (ret == 'E')
printf("平局!\n");
else if (ret == '*')
printf("玩家胜!\n");
else
printf("电脑胜!\n");
至此胜负判断实现,第三部分结束
将这些部分组合起来,你便得到了一个极其简单的三子棋小程序(虽然你可以也能玩规则离谱的四子棋)
void game()//三子棋游戏实现 1.棋盘 2.玩家/电脑输入 3.判断正负
{
char ret = 0;
//1.棋盘实现-二维数组
char board[ROW][COL] = { 0 };
//初始化棋盘
Init_board(board);
//打印棋盘
print_board(board);
//2.玩家/电脑输入
while (1)
{
player_input(board);
print_board(board);
ret = is_win(board, '*');
if (ret != 'C')
break;
//电脑输入
computer_input(board);
print_board(board);
ret = is_win(board, '#');
if (ret != 'C')
break;
}
//3.判断正负 1.棋盘未满,胜负未分,继续! 2.胜负已分 - 玩家或电脑 3.棋盘满,平局 return game
//输出结果
if (ret == 'E')
printf("平局!\n");
else if (ret == '*')
printf("玩家胜!\n");
else
printf("电脑胜!\n");
}
以下是最后对菜单打印进行的一点的修改,可以打开为n子棋小程序,虽然毫无意义,但是胜负判断里实现都实现了,总得全面一点吧
char* tran()
{
switch (ROW)
{
case 1:
return "一";
case 2:
return "二";
case 3:
return "三";
case 4:
return "四";
case 5:
return "五";
case 6:
return "六";
case 7:
return "七";
case 8:
return "八";
case 9:
return "九";
default:
return "&&&";
}
}
void menu()
{
printf("************%s子棋小游戏************\n", tran());
printf("************ 1.play ************\n");
printf("************ 0.exit ************\n");
printf("************ V1.0.0 ************\n");
}
最后不得不吐槽,这种"大型程序"要是写出来bug,找bug真是让人头皮发麻啊啊啊
最后附上头文件以便观察需实现的函数:
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#define ROW 3 //行
#define COL 3 //列
void game();
void Init_board(char board[ROW][COL]);
void print_board(char board[ROW][COL]);
void player_input(char board[ROW][COL]);
void computer_input(char board[ROW][COL]);
char is_win(char[ROW][COL],char ch);
char is_full(char board[ROW][COL]);
效果图展示: