C语言实现三子棋
程序运行效果
思路分析
-
打印菜单
根据程序预期的效果我们发现需要一个菜单界面,并且菜单必须在用户选择之前打印,因此无论用户选择开始游戏还是退出游戏,我们都需要先调用打印菜单函数,再根据用户输入判断是否继续打印菜单让用户继续选择,因此我们自然而然想到
do-while
语句。
-
根据用户输入实现不同逻辑功能
- 用户选择1——表示用户希望开始游戏
- 用户选择0——表示用户希望退出游戏
- 用户选择其他数字——表示用户非法输入,则需要提示用户重新选择
从上述情况中我们可以看出这是一个多分支选择语句,根据用户选择的不同执行不同的分支代码
并且分支判断的依据是接收用户输入的变量
choice
,所以switch-case
语句可以满足需求(注意不要忘了break
关键字哦~),循环退出的条件也可以使用choice
变量的值。 -
棋盘的设计与实现
-
棋盘的存储数据结构
首先我们观察这个棋盘是一个
3x3
的二维平面,物理结构为行列矩阵,需要三行三列进行存储,而在c语言中我们立马可以想到使用二维数组数据结构对应物理结构中的行列矩阵,所以我们可以使用int board[3][3]
创建一个棋盘存储数据。 -
棋盘的初始化
继续观察下图棋盘的格式,我们发现这个
3x3
的棋盘之间存放着看不见即空的数据,看不见的那些空间我们可以首先存放空格
这个字符进行占位,所以我们想到封装一个初始化棋盘的函数InitBoard()
-
棋盘的布局实现
棋盘的打印实现较为复杂,但是如果仔细观察,会发现效果图中第一行为:空格 -> 存放数据 -> 空格 ->竖杠(|)-> 空格 -> 存放数据 -> 空格 -> 竖杠( | ) -> 空格 -> 存放数据 -> 空格,其中有相同格式的部分,于是我们想到可以用循环实现,再来看第二行的格式:三个横杠(—)-> 竖杠(|)-> 三个横杠(—)-> 竖杠(|)-> 三个横杠(—),其中也存在大量相同格式的部分,也可以通过循环的方式进行进行格式处理,从第三行开始也与第一行、第二行格式重复,因此要实现棋盘的布局实现需要使用大量循环,那么就会有同学问了:为什么不直接格式化打印整个棋盘呢?答:这是为了程序的可扩展性,这是
3x3
的棋盘,但是如果以后换成5x5
呢,那么整个棋盘就需要重新构建,而若采用循环实现,我们只需要改动少量的数据代码,而不需要修改整个逻辑代码,程序的可维护性也就更强。
-
-
玩家下棋功能实现
玩家落子的功能逻辑我们可以独立封装成一个函数
PlayerMove()
,在这个函数中,我们可以创建两个变量x
、y
来让用户输入想要落子的坐标位置,但是需要注意下列两种非法情况- 假设棋盘为
3x3
的格式,那么若用户想要下到行为4 或者列为5 等等越出边界的情况,我们就需要判断变量x、y的值是否满足x >= 1 && x <= 3 && y >= 1 && y <= 3
条件,如果不满足就不落子并让用户重新输入落子位置 - 再满足落子位置不超出边界的情况时,还要注意到落子处可能已经有子存在,即我们需要判断当前坐标位置处的数据是否为我们初始化时的空值,如果当前有落子,就需要用户重新输入落子坐标重新判断
- 假设棋盘为
-
电脑下棋功能实现
电脑落子的功能逻辑我们也可以独立封装成一个函数
ComputerMove()
,电脑下棋在这里实际上是一个使用随机值坐标落子的简易实现,因此我们需要引入c语言中的rand()
函数进行随机值的选取,对于上述用户落子处理非法情况的第一种情况,我们可以使用取模%3
的操作强制数组行列下标值为0-2
区间内,对于非法情况的第二种情况,我们仍然需要做特殊处理,即判断当前坐标位置处是否已经有子存在,如果有就要重新选取随机值坐标进行判断,直到满足要求才给电脑落子。 -
判断输赢
在三子棋中,判断输赢实际上是比较简单的,我们可以独立封装成一个函数
isWin()
,而判断输赢函数中,总共只有三种情况,即用户胜利、电脑胜利、以及平局的情况。当某一行或者某一列或者一条对角线全是一种字符时且该字符不为空格,我们判断代表该字符的用户胜利,所以我们可以让玩家落子值为'*'
,电脑落子值为'#'
,如果两者都没有胜利,但是棋盘已经满了isFull()
,那么就是平局的情况,我们也可以使用字符's'
代表这种情况,除此以外,别的情况都还没有判断出结果,我们用字符'c'
表示这种结果。
代码实现细节
棋盘初始化
// 定义初始化棋盘函数
void InitBoard(int board[ROW][COL], int row, int col) {
// 初始化为空格
for (int i = 0; i < row; ++i) {
for (int j = 0; j < col; ++j) {
board[i][j] = ' ';
}
}
}
在上述代码中,InitBoard函数的参数为二维数组board, row为行数,col为列数,在函数体中,我们遍历整个二位数组并将值初始化为空格字符
棋盘打印函数
void DisplayBoard(int board[ROW][COL], int row, int col) {
for (int i = 0; i < row; ++i) {
// 打印数据
for (int j = 0; j < col; ++j) {
printf(" %c ", board[i][j]);
if (j < col - 1) {
printf("|");
}
}
printf("\n");
// 打印分隔行
if (i < row - 1) {
for (int j = 0; j < col; ++j) {
printf("---");
if (j < col -1) {
printf("|");
}
}
printf("\n");
}
}
}
为了实现可视化的效果,我们使用一些分隔符作为棋盘的边界,并使用大量的循环处理,在这里由于跟业务逻辑的实现无太大关系就省略了实现的细节。
玩家落子函数
// 定义玩家落子函数
void PlayerMove(int board[ROW][COL], int row, int col) {
printf("下面轮到玩家下棋\n");
int x = 0;
int y = 0;
while (1) {
printf("请玩家选择落子的坐标,用空格分隔>:");
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");
}
}
}
在这个函数中,我们让用户选择坐标并存入变量x、y中,并判断用户的落子位置是否合法,不合法的情况分如下两种:1、落子坐标超出边界 2、落子坐标处已经落子,并且当用户输入非法坐标值,我们应该提示用户选择错误,应当重新输入,因此可以设计为一个循环,只有当用户落子成功才退出循环。
电脑落子函数
// 定义电脑落子函数
void ComputerMove(int board[ROW][COL], int row, int col) {
// 电脑生成随机数
printf("现在轮到电脑下棋\n");
int x = 0;
int y = 0;
while (1) {
x = rand() % row;
y = rand() % col;
// 判断电脑落子处是否有子
if (board[x][y] == ' ') {
board[x][y] = '#';
break;
}
}
}
电脑落子实际上在此处不涉及过多的算法,只是简单的随机落子,可以使用c库函数提供的rand()
函数并取模的操作得到我们想要的落子区间0-row-1
与0-col-1
,但是此处也要判断落子是否合法即该坐标是否已经落子。
判断输赢函数
// 定义判断游戏状态函数
char isWin(int board[ROW][COL], int row, int col) {
// 赢状态
// 1.1 横向相连
for (int i = 0; i < row; ++i) {
// 取第一个数
char flagChar = board[i][0];
int flag = 0;
for (int j = 1; j < col; ++j) {
if (flagChar == ' ' || board[i][j] != flagChar) {
flag = 1;
break;
}
}
if (flag == 0) {
// 说明行全部相等即有人赢
return flagChar;
}
}
// 1.2 纵向相连
for (int i = 0; i < col; ++i) {
char flagChar = board[0][i];
int flag = 0;
for (int j = 1; j < row; ++j) {
if (flagChar == ' ' || board[j][i] != flagChar) {
flag = 1;
break;
}
}
if (flag == 0) {
// 说明有列一致,则有人赢了
return flagChar;
}
}
// 1.3 左斜对角线
char left_top = board[0][0];
int flag1 = 0;
for (int i = 1; i < row; ++i) {
if (left_top == ' ' || left_top != board[i][i]) {
flag1 = 1;
break;
}
}
if (flag1 == 0) {
// 说明左斜对角线一致
return left_top;
}
// 1.4 右斜对角线
char right_top = board[0][col - 1];
int flag2 = 0;
for (int i = 1; i < col; ++i) {
if (right_top == ' ' || right_top != board[i][col - i - 1]) {
flag2 = 1;
break;
}
}
if (flag2 == 0) {
// 说明右斜对角线一致
return right_top;
}
// 平局状态
// 此时如果棋盘已经满了又没人赢说明平局
if (isFull(board, row, col)) {
return 's';
}
// 游戏未结束
return 'c';
}
在这个函数中,要完成的就是判断游戏当前的状态,只有以下四种情况:1、玩家获胜 2、电脑获胜 3、平局 4、还未判断出输赢,游戏未结束。
玩家或者电脑获胜情况:
以判断横向相连情况为例,我们取当前行第一个元素作为比较元素flagChar
,置标记位flag
为0,并依次将其与当前行的其余元素一一比较,如果flagChar
变量为空格或者比较存在不同则置flag
为1代表当前行不满足胜利条件,由于每一行都需要如此进行比较,所以需要for循环遍历每一行,同理也要遍历每一列,而如果flag
标记值仍然为0,则说明当前行或者列元素相同并且不为空格,那么则可以说明玩家或者电脑获胜,就返回当前行或者当前列存储的字符即flagChar
以便后续判断胜利者。
平局情况:
当棋盘已经满了时并且不满足有胜利者的情况,就属于平局返回字符 ‘s’
游戏未结束:
当不存在平局和有人获胜时,此时表明游戏还未结束,就返回字符 ‘c’ 代表仍需继续游戏
完整源代码
-
test.c
#include "game.h" // 菜单函数 void menu() { printf("******************************\n"); printf("*******1. play 0. exit*******\n"); printf("******************************\n"); } // 游戏函数 void game() { // 创建棋盘(使用二维数组) int board[ROW][COL]; // 调用初始化棋盘函数 InitBoard(board, ROW, COL); // 打印棋盘函数 DisplayBoard(board, ROW, COL); // state代表游戏状态,为*则表示玩家赢,#表示电脑赢了,s代表平局,c代表继续 char state = 0; while (1) { // 玩家落子 PlayerMove(board, ROW, COL); DisplayBoard(board, ROW, COL); // 判断游戏状态 state = isWin(board, ROW, COL); if (state != 'c') { break; } // 电脑落子 ComputerMove(board, ROW, COL); DisplayBoard(board, ROW, COL); // 判断游戏状态 state = isWin(board, ROW, COL); if (state != 'c') { break; } } // 对state进行判断 if (state == '*') { printf("玩家获胜!\n"); } else if (state == '#') { printf("电脑获胜!\n"); } else { printf("双方平局!\n"); } } int main() { int choice = 0; srand((unsigned int)time(NULL)); do { menu(); printf("请输入选项>:"); scanf("%d", &choice); switch (choice) { case 1: printf("三子棋游戏现在开始\n"); game(); break; case 0: printf("游戏退出!\n"); break; default: printf("选择非法,请重新输入!\n"); break; } } while (choice); }
-
game.h
#define _CRT_SECURE_NO_WARNINGS #pragma once #include <stdio.h> #include <stdlib.h> #include <time.h> #define ROW 3 #define COL 3 // 声明初始化棋盘函数 void InitBoard(int board[ROW][COL], int row, int col); // 声明打印棋盘函数 void DisplayBoard(int board[ROW][COL], int row, int col); // 声明玩家落子函数 void PlayerMove(int board[ROW][COL], int row, int col); // 声明电脑落子函数 void ComputerMove(int board[ROW][COL], int row, int col); // 声明判断游戏状态函数 char isWin(int board[ROW][COL], int row, int col);
-
game.c
#include "game.h" // 定义初始化棋盘函数 void InitBoard(int board[ROW][COL], int row, int col) { // 初始化为空格 for (int i = 0; i < row; ++i) { for (int j = 0; j < col; ++j) { board[i][j] = ' '; } } } // 定义打印棋盘函数 void DisplayBoard(int board[ROW][COL], int row, int col) { for (int i = 0; i < row; ++i) { // 打印数据 for (int j = 0; j < col; ++j) { printf(" %c ", board[i][j]); if (j < col - 1) { printf("|"); } } printf("\n"); // 打印分隔行 if (i < row - 1) { for (int j = 0; j < col; ++j) { printf("---"); if (j < col -1) { printf("|"); } } printf("\n"); } } } // 定义玩家落子函数 void PlayerMove(int board[ROW][COL], int row, int col) { printf("下面轮到玩家下棋\n"); int x = 0; int y = 0; while (1) { printf("请玩家选择落子的坐标,用空格分隔>:"); 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"); } } } // 定义电脑落子函数 void ComputerMove(int board[ROW][COL], int row, int col) { // 电脑生成随机数 printf("现在轮到电脑下棋\n"); int x = 0; int y = 0; while (1) { x = rand() % row; y = rand() % col; // 判断电脑落子处是否有子 if (board[x][y] == ' ') { board[x][y] = '#'; break; } } } // 判断棋盘是否满函数 int isFull(int board[ROW][COL], int row, int col) { for (int i = 0; i < row; ++i) { for (int j = 0; j < col; ++j) { // 如果有空说明没满 if (board[i][j] == ' ') { return 0; } } } // 此时说明已经遍历完毕全部不为空 return 1; } // 定义判断游戏状态函数 char isWin(int board[ROW][COL], int row, int col) { // 赢状态 // 1.1 横向相连 for (int i = 0; i < row; ++i) { // 取第一个数 char flagChar = board[i][0]; int flag = 0; for (int j = 1; j < col; ++j) { if (flagChar == ' ' || board[i][j] != flagChar) { flag = 1; break; } } if (flag == 0) { // 说明行全部相等即有人赢 return flagChar; } } // 1.2 纵向相连 for (int i = 0; i < col; ++i) { char flagChar = board[0][i]; int flag = 0; for (int j = 1; j < row; ++j) { if (flagChar == ' ' || board[j][i] != flagChar) { flag = 1; break; } } if (flag == 0) { // 说明有列一致,则有人赢了 return flagChar; } } // 1.3 左斜对角线 char left_top = board[0][0]; int flag1 = 0; for (int i = 1; i < row; ++i) { if (left_top == ' ' || left_top != board[i][i]) { flag1 = 1; break; } } if (flag1 == 0) { // 说明左斜对角线一致 return left_top; } // 1.4 右斜对角线 char right_top = board[0][col - 1]; int flag2 = 0; for (int i = 1; i < col; ++i) { if (right_top == ' ' || right_top != board[i][col - i - 1]) { flag2 = 1; break; } } if (flag2 == 0) { // 说明右斜对角线一致 return right_top; } // 平局状态 // 此时如果棋盘已经满了又没人赢说明平局 if (isFull(board, row, col)) { return 's'; } // 游戏未结束 return 'c'; }