整体设计思路:
1. 程序一开始需要展示一个选择界面,根据玩家选择决定后续执行。这里假设1是开始游戏,0是退出程序,其他选择提示错误选项,即创建一个input变量接收玩家选项。此功能可以使用switch语句来实现。
除此之外,这个游戏可以重复玩,一旦结束又回到最开始的选择界面。所以将这个switch语句放到do while循环中。这样不仅可以实现一开始展示界面,还能反复玩。同样这个do while循环也使用玩家输入作为条件。
2. 玩家和电脑下棋意味落子需要被保存,所以创建一个3*3的数组( char board[ROW][COL] = { 0 }; )保存落子。玩家落子为*,电脑落子为#。为方便后续更改棋盘大小,这里在头文件game.h中使用#define来定义行和列。如果有改动,只需在game.h中修改即可。
3. 游戏开始,显示棋盘,这里需要一个显示棋盘的函数( DisplayBoard(board, ROW, COL); )。但因为还没有任何落子,所以棋盘是空的,需要用空格来暂时占位,由此引出初始化棋盘函数( InitBoard(board, ROW, COL); )。
4. 下棋时,玩家落子一次,判断输赢,显示棋盘;电脑落子一次,判断输赢,显示棋盘。一直循环,直到某一方有3个相同棋子连成线即获得胜利。否则平局。所以需要以下函数来实现:
玩家下棋函数:PlayerMove(board, ROW, COL);
电脑下棋函数:ComputerMove(board, ROW, COL);
判断输赢函数:IsWin(board, ROW, COL);
下方为test.c文件,用于测试游戏逻辑
#include "game.h"
//test.c-测试游戏逻辑
//game.c-游戏代码实现
//game.h-游戏代码的声明(函数声明,符号定义)
//此为test.c文件
void menu()
{
printf("*************************\n");
printf("***** 1. play *****\n");
printf("***** 0. exit *****\n");
printf("*************************\n");
}
void game()
{
char board[ROW][COL] = { 0 };//创建3*3的二维数组(即棋盘)
int ret = 0;//设置一个返回值,判断输赢
//本质是让空格来填充数组,在没有落子的时候也能保证棋盘对齐
InitBoard(board, ROW, COL);//初始化棋盘函数
DisplayBoard(board, ROW, COL);//显示棋盘
while (1)//下棋的过程,直到出结果(玩家赢或电脑赢或平局)停止循环
{
PlayerMove(board, ROW, COL);//玩家下棋后需要判断输赢
ret = IsWin(board, ROW, COL);
if (ret != 'C')//不等于C说明游戏不再继续,故跳出此循环
{
break;
}
DisplayBoard(board, ROW, COL);//打印棋盘也需要将数组的信息打印
ComputerMove(board, ROW, COL);//电脑下棋后需要判断输赢
ret = IsWin(board, ROW, COL);//判断输赢本质是查看字符
if (ret != 'C')
{
break;
}
DisplayBoard(board, ROW, COL);
}
//游戏不在继续后就要判断胜负或平局
if (ret == '*')
{
printf("玩家赢\n");
}
else if (ret == '#')
{
printf("电脑赢\n");
}
else
{
printf("平局\n");
}
DisplayBoard(board, ROW, COL);
}
int main()
{
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误,重新选择:>\n");
break;
}
} while (input);
return 0;
}
下方为game.h头文件,用于游戏代码的声明(函数声明,符号定义)
#include <stdio.h> //两个.c文件都需要这个头文件,并且两个.c文件都包含了这个game.h头文件
#include <stdlib.h>
#include <time.h>
//为了以后方便改成五子棋或其他,所以定义两个常量
//否则每次都要在多个地方修改参数
#define ROW 3
#define COL 3
//初始化棋盘
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);
//状态总共4种:
//玩家赢 - '*'
//电脑赢 - '#'
//平局 - 'Q'
//继续 - 'C'
char IsWin(char board[ROW][COL], int row, int col);
game.c中各函数实现
1. 初始化函数
就是利用嵌套循环遍历二维数组每个元素,并初始化他们为空格字符。
//先让落子的地方用空格占位,防止后期棋盘不对齐
void InitBoard(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] = ' ';
}
}
}
2. 显示棋盘函数
棋盘分为两部分,第一部分为落子的部分,第二部分为分割行。
第一个函数是较为简单的实现。只需要一个循环,每一次循环打印一行。但这种方法有个弊端,如果棋盘大小有改动,这个函数也需要改。
第二个函数是将棋盘的两个部分进一步细分,将每一行的内容再次划分,每一次循环只打印一个格子。
//void DisplayBoard(char board[ROW][COL], int row, int col)
//{
// int i = 0;
// for (i = 0; i < row; i++)
// {
// printf(" %c | %c | %c \n", board[i][0], board[i][1], board[i][2]);
// if (i < row - 1)
// printf("---|---|---\n");
// }
//}
void DisplayBoard(char board[ROW][COL], int row, int col)
{
//将棋盘分解为两部分
//第一部分:( | | ),这部分循环3次
//第二部分:(---|---|---),这部分循环2次,最后一行不打印
//所以总的(最外层循环)打印3行,内部循环通过条件控制少打印一行
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
//printf(" %c | %c | %c \n")
printf(" %c ", board[i][j]);
if (j < col - 1)
printf("|");
}
printf("\n");
if (i < row - 1)
{
int j = 0;
for (j = 0; j < col; j++)
{
//printf("---|---|---\n")
printf("---");
if (j < col - 1)
printf("|");
}
printf("\n");
}
}
}
3. 玩家下棋函数
首先,接收玩家输入的坐标,接着判断坐标是否合法(即是否在棋盘内),之后继续判断该位置是否下过棋。重复上去步骤,直到玩家正确落子才跳出循环。
void PlayerMove(char board[ROW][COL], int row, int col)
{
//首先判断坐标是否合法
//其次判断棋盘是否为空格
int x = 0;
int y = 0;
printf("玩家下棋\n");
while (1)//如果输入的坐标超过棋盘范围或被占用,需要重新输入直到正确为止
{
printf("请输入坐标:>\n");
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");
}
}
}
4. 电脑下棋函数
这里简单设置电脑随机下棋,所以需要一个随机值。在设置随机值之前需要借助时间戳设置随机数生成起点,这个起点只需要设置一次,所以放在main函数中。
将随机数模上行数和列数(值都为3)就能得到余数0~2。反复生成随机坐标,直到此坐标的位置是空格电脑才下棋。
void ComputerMove(char board[ROW][COL], int row, int col)
{
//电脑也需要横纵坐标,但是坐标是0~2随机值(0~2是数组下标)
//只有找到了空格才下棋
printf("电脑下棋\n");
int x = 0;
int y = 0;
while (1)//对生成的随机横纵坐标判断,直到找到空格才下棋
{
x = rand() % row;
y = rand() % col;
if (board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}
5. 判断输赢函数和判断棋盘是否被下满 函数
判断输赢即行、列、对角线都是同一种字符,这里不要明确是*(玩家落子)还是#(电脑落子),只需返回数组里保存的字符即可。
如果没有人获胜,那么就判断是否为平局,由此引出判断棋盘是否下满函数。判断是否下满,只需遍历每个数组的字符,如果没有空格即为满。
//满了返回1
//不满返回0
//此函数查看棋盘是否满了,即数组是否还有空格
int IsFull(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 IsWin(char board[ROW][COL], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
//判断行:第1格和第2格相等,并且第2格和第3格相等,并且任意一格不为空格
//不需再判断里面的字符是玩家落子还是电脑落子,直接返回里面的字符即可
if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][1] != ' ')
return board[i][1];
}
int j = 0;
for (j = 0; j < col; j++)
{
if (board[0][j] == board[1][j] && board[1][j] == board[2][j] && board[1][j] != ' ')
return board[1][j];
}
if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[1][1] != ' ')
return board[1][1];
if (board[0][2] == board[1][1] && board[1][1] == board[2][0] && board[1][1] != ' ')
return board[1][1];
//上方为出现了胜负,如果没有人赢,则进入下面平局代码
//平局:棋盘放满,但没有人赢
//还有空格返回0(棋盘不满),没有空格返回1(棋盘满了)
//返回0,if条件为假,就返回C,意味着继续
//返回1,if条件为真,就返回Q,意味着结束
if (IsFull(board, row, col))//此为调用棋盘是否为满函数,并将棋盘数组传参
{
return 'Q';
}
else
{
return 'C';
}
}