文章目录
一. 三子棋游戏规则
任意一方的棋子先达成布满任意一行,布满任意一列,布满主对角线,布满副对角线这四项中任意一项即可结束游戏
二. 基本流程
- 创建菜单,玩家可选择进入或退出游戏
- 创建并初始化棋盘
- 打印棋盘
- 玩家下棋
- 电脑下棋
- 判断输赢
三. 具体步骤
代码模块划分
- test.c //测试游戏逻辑
- game.c //游戏代码的实现(函数定义)
- game.h //游戏代码的声明(函数声明,符号定义)
创建游戏菜单
在game.h头文件中声明函数menu()
,game.c源文件中定义函数menu()
//菜单函数的声明
void menu();
//菜单函数的定义
void menu()
{
printf("*************************************\n");
printf("************** 1. play **************\n");
printf("************** 0. exit **************\n");
printf("*************************************\n");
}
游戏逻辑是上来先显示一次菜单,玩家可选择进入或者退出游戏。此外,对于玩家的非法操作,程序需要做出适当的处理和响应。而除了选择退出游戏外,不论是玩家已经结束一把游戏还是非法输入需要重新选择,菜单都需再次显示。综上,在test.c源文件中使用循环执行语句do while和分支语句switch来实现
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");
break;
}
} while (input);
return 0;
}
创建一个整型变量input来存放玩家的输入,玩家输入0则退出游戏,循环判断为假则结束循环,不再显示菜单。输入1则进入游戏,结束当前游戏后,循环判断为真则再次进入循环显示菜单。而对于玩家的非法输入,输入非0的值,循环判断都为真,菜单会再次显示供玩家重新选择。
创建和打印棋盘
使用二维字符数组来表示棋盘,想到三子棋和五子棋甚至根据需求拓展为N子棋,它们的实现逻辑相同,只不过是棋盘的尺寸发生了变化,故可以在头文件game.h中用define定义标识符常量来表示棋盘的尺寸,方便以后拓展。
#define ROW 3
#define COL 3
一个棋盘的显示如下
//创建棋盘
char chess_board[ROW][COL] = { 0 };
//使用库函数memset初始化棋盘,即将二维数组chess_board[ROW][COL]中的每个元素置为空格字符' '
memset(chess_board, ' ', ROW * COL);
//打印棋盘
void Displayboard(char chess_board[][COL])
{
int i = 0;
int j = 0;
for (i = 0; i < ROW; i++)
{
for (j = 0; j < COL; j++)
{
//棋盘每一行最后一格不需要打印 '|'
j < COL - 1 ? printf(" %c |", chess_board[i][j]) : printf(" %c \n", chess_board[i][j]);
}
//棋盘最后一行打印完后不需要再打印 '-'
if (i < ROW - 1)
{
for (j = 0; j < COL; j++)
{
j < COL - 1 ? printf("---|") : printf("---\n");
}
}
}
}
玩家下棋
在game.c源文件中创建两个整型类型的全局变量来存放玩家落子的横纵坐标
int row = 0;//玩家棋子横坐标
int col = 0;//玩家棋子纵坐标
- 玩家下棋用字符 ’ * ’ 表示,所输入的坐标需要合法,横纵坐标的取值范围分别为[ 1, ROW ],[ 1, COL ]
- 坐标合法还应该扫描棋盘判断该坐标是否可以落子,即坐标对应二维数组中元素是否为空格
//扫描棋盘,判断是否可以落子
int scan_board(char chess_board[][COL], int row, int col)
{
if (chess_board[row][col] == ' ')
return 1;
else
return 0;
}
- 可以在game.c源文件中创建一个整型局部变量amount来记录棋盘上的棋子个数(玩家棋子数 + 电脑棋子数)
//玩家下棋
void PlayerMove(char chess_board[][COL], int* amount)
{
printf("玩家下棋:>\n");
again:
printf("请输入坐标:>");
scanf("%d %d", &row, &col);
//判断坐标是否合法
if ((row >= 1 && row <= ROW) && (col >= 1 && col <= COL))
{
if (scan_board(chess_board, row - 1, col - 1))
{
chess_board[row - 1][col - 1] = '*';
(*amount)++;//落子后棋盘上的棋子个数增加
}
else
{
printf("请重新输入\n");
goto again;
}
}
else
{
printf("请重新输入\n");
goto again;
}
}
电脑下棋
在game.c源文件中创建两个整型类型的全局变量来存放电脑落子的横纵坐标
int c_row = 0;//电脑棋子横坐标
int c_col = 0;//电脑棋子纵坐标
玩家下棋可以通过输入坐标来确定落子点,电脑下棋可以靠生成两个随机数分别表示落子点的横纵坐标,之后的逻辑跟玩家下棋相同
- 电脑下棋用字符 ’ # ’ 表示
- 设置随机数起点,生成两个随机数
srand((unsigned int)time(NULL));//设置随机数起点
//电脑下棋
void ComputerMove(char chess_board[][COL], int* amount)
{
printf("电脑下棋:>\n");
again_1:
//生成两个随机数,作为落子点坐标
c_row = rand() % ROW;
c_col = rand() % COL;
//判断该坐标是否可以落子
if (scan_board(chess_board, c_row, c_col))
{
chess_board[c_row][c_col] = '#';
(*amount)++;//落子后棋盘上的棋子个数增加
}
else
{
goto again_1;
}
}
判断输赢
行 / 列的判定
- 创建一个二维数组用来记录玩家和电脑在棋盘上每一行和每一列棋子的个数
- 该数组第一行表示横(纵)坐标号,比如第一行第一列的元素值0表示棋盘第0行,也可表示棋盘第0列,第一行第二列的元素值1表示棋盘第1行(列)
- 该数组第二行表示玩家在棋盘上每一行棋子的个数,第三行则表示每一列棋子的个数
- 第四行和第五行则分别用来表示电脑在棋盘上每一行,每一列棋子的个数
int row_col_count[5][COL] = { 0 };//存放玩家和电脑每一行(列)的棋子个数
int(*p)[COL] = row_col_count;//指向一个整型数组的指针
//将第一行设置为行(列)号
void Initcount(int row_col_count[][COL])
{
int j = 0;
for (j = 0; j < COL; j++)
{
row_col_count[0][j] = j;
}
}
0 | 1 | 2 |
---|---|---|
0 | 0 | 0 |
0 | 0 | 0 |
0 | 0 | 0 |
0 | 0 | 0 |
例如玩家落子坐标为(1,3),则玩家在棋盘上第0行和第2列的棋子个数增加
0 | 1 | 2 |
---|---|---|
1 | 0 | 0 |
0 | 0 | 1 |
0 | 0 | 0 |
0 | 0 | 0 |
//判断行/列是否布满同一方棋子
int r_or_c_judge(int* r_or_l_count)
{
//玩家或电脑每次落子,坐标所对应棋盘的行和列的棋子个数增加
(*r_or_l_count)++;
//判断落子坐标对应的当前行/列是否布满己方棋子
if ((*r_or_l_count) >= COL)
return 1;
else
return 0;
}
主对角线和副对角线判定
- 主对角线上的横纵坐标值是相等的
- 副对角线上的横纵坐标值关系为
玩家:row + col == COL + 1
电脑:c_row + c_col == COL - 1
- 玩家/电脑的棋子布满主/副对角线则游戏结束
int p_m_angle = 0;//玩家主对角棋子个数
int p_n_angle = 0;//玩家副对角棋子个数
int c_m_angle = 0;//电脑主对角棋子个数
int c_n_angle = 0;//电脑副对角棋子个数
- 平局判定
当双方棋子布满棋盘,而没有一方的棋子能布满任意行列或者两条对角线中的其中一条则平局,即amount == ROW * COL
//判断游戏输赢(返回'*'则玩家赢,'#'则玩家输,'B'则平局,'C'则游戏继续)
char IsWin(int(*s)[COL], char chess_board[][COL], int* m_angle, int* n_angle, int row, int col, int* amount)
{
//行/列的判定
if (r_or_c_judge(*s + row) || r_or_c_judge(*(s + 1) + col))
{
return chess_board[row][col];
}
//主对角线判定
if (row == col)
{
(*m_angle)++;
if (*m_angle >= ROW)
{
return chess_board[row][col];
}
}
//副对角线判定
if (col + row == COL - 1)
{
(*n_angle)++;
if (*n_angle >= ROW)
{
return chess_board[row][col];
}
}
//平局判定
if (*amount == ROW * COL)
{
return 'B';
}
return 'C';
}
游戏整体逻辑
void game()
{
char chess_board[ROW][COL] = { 0 };//棋盘
int row_col_count[5][COL] = { 0 };//存放玩家和电脑每一行(列)的棋子个数
int(*p)[COL] = row_col_count;//指向一个整型数组的指针
int p_m_angle = 0;//玩家主对角棋子个数
int p_n_angle = 0;//玩家副对角棋子个数
int c_m_angle = 0;//电脑主对角棋子个数
int c_n_angle = 0;//电脑副对角棋子个数
int amount = 0;//双方所下棋子的总数
//将棋盘置为空格并打印棋盘
memset(chess_board, ' ', ROW * COL);
Displayboard(chess_board);
//初始化存放行列棋子个数的数组
Initcount(row_col_count);
char ret = 0;
while (1)
{
//玩家下棋
PlayerMove(chess_board, &amount);
//判断输赢
ret = IsWin(p + 1, chess_board, &p_m_angle, &p_n_angle, row - 1, col - 1, &amount);
if (ret != 'C')
break;
Displayboard(chess_board);
//电脑下棋
ComputerMove(chess_board, &amount);
//判断输赢
ret = IsWin(p + 3, chess_board, &c_m_angle, &c_n_angle, c_row, c_col, &amount);
if (ret != 'C')
break;
Displayboard(chess_board);
}
Displayboard(chess_board);
switch (ret)
{
case '*':
printf("赢\n");
break;
case '#':
printf("输\n");
break;
case 'B':
printf("平局\n");
break;
}
}
四. 程序源码
game.h
#pragma once
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#include<string.h>
#define ROW 3
#define COL 3
//菜单
void menu();
//游戏实现
void game();
//打印棋盘
void Displayboard(char chess_board[][COL]);
//初始化存放所有行和列棋子个数的数组
void Initcount(int row_col_count[][COL]);
//扫描棋盘,判断是否可以落子
int scan_board(char chess_board[][COL], int row, int col);
//判断行/列相同棋子个数是否符合结束游戏条件
int r_or_c_judge(int* r_or_l_count);
//玩家下棋
void PlayerMove(char chess_board[][COL], int *amount);
//电脑下棋
void ComputerMove(char chess_board[][COL], int *amount);
//判断输赢
char IsWin(int(*s)[COL], char chess_board[][COL], int *m_angle, int *n_angle, int row, int col,int *amount);
game.c
#define _CRT_SECURE_NO_WARNINGS
#include "game.h"
int row = 0;//玩家棋子横坐标
int col = 0;//玩家棋子纵坐标
int c_row = 0;//电脑棋子横坐标
int c_col = 0;//电脑棋子纵坐标
//菜单
void menu()
{
printf("*************************************\n");
printf("************** 1. play **************\n");
printf("************** 0. exit **************\n");
printf("*************************************\n");
}
//打印棋盘
void Displayboard(char chess_board[][COL])
{
int i = 0;
int j = 0;
for (i = 0; i < ROW; i++)
{
for (j = 0; j < COL; j++)
{
//棋盘每一行最后一格不需要打印 '|'
j < COL - 1 ? printf(" %c |", chess_board[i][j]) : printf(" %c \n", chess_board[i][j]);
}
//棋盘最后一行打印完后不需要再打印 '-'
if (i < ROW - 1)
{
for (j = 0; j < COL; j++)
{
j < COL - 1 ? printf("---|") : printf("---\n");
}
}
}
}
//初始化存放行列棋子个数的数组
void Initcount(int row_col_count[][COL])
{
int j = 0;
for (j = 0; j < COL; j++)
{
row_col_count[0][j] = j;
}
}
//扫描棋盘,判断是否可以落子
int scan_board(char chess_board[][COL], int row, int col)
{
if (chess_board[row][col] == ' ')
return 1;
else
return 0;
}
//判断行/列
int r_or_c_judge(int* r_or_l_count)
{
(*r_or_l_count)++;
if ((*r_or_l_count) >= COL)
return 1;
else
return 0;
}
//玩家下棋
void PlayerMove(char chess_board[][COL], int* amount)
{
printf("玩家下棋:>\n");
again:
printf("请输入坐标:>");
scanf("%d %d", &row, &col);
//判断坐标是否合法
if ((row >= 1 && row <= ROW) && (col >= 1 && col <= COL))
{
if (scan_board(chess_board, row - 1, col - 1))
{
chess_board[row - 1][col - 1] = '*';
(*amount)++;
}
else
{
printf("请重新输入\n");
goto again;
}
}
else
{
printf("请重新输入\n");
goto again;
}
}
//电脑下棋
void ComputerMove(char chess_board[][COL], int* amount)
{
//电脑下棋,用 '#' 代表落子
printf("电脑下棋:>\n");
again_1:
//生成两个随机数,作为落子点坐标
c_row = rand() % ROW;
c_col = rand() % COL;
//判断是否可以落子
if (scan_board(chess_board, c_row, c_col))
{
chess_board[c_row][c_col] = '#';
(*amount)++;
}
else
{
goto again_1;
}
}
//判断游戏输赢
char IsWin(int(*s)[COL], char chess_board[][COL], int* m_angle, int* n_angle, int row, int col, int* amount)
{
//行/列的判定
if (r_or_c_judge(*s + row) || r_or_c_judge(*(s + 1) + col))
{
return chess_board[row][col];
}
//主对角线判定
if (row == col)
{
(*m_angle)++;
if (*m_angle >= ROW)
{
return chess_board[row][col];
}
}
//副对角线判定
if (col + row == COL - 1)
{
(*n_angle)++;
if (*n_angle >= ROW)
{
return chess_board[row][col];
}
}
//平局判定
if (*amount == ROW * COL)
{
return 'B';
}
return 'C';
}
//实现游戏
void game()
{
char chess_board[ROW][COL] = { 0 };//棋盘
int row_col_count[5][COL] = { 0 };//存放玩家和电脑每一行(列)的棋子个数
int(*p)[COL] = row_col_count;//指向一个整型数组的指针
int p_m_angle = 0;//玩家主对角棋子个数
int p_n_angle = 0;//玩家负对角棋子个数
int c_m_angle = 0;//电脑主对角棋子个数
int c_n_angle = 0;//电脑负对角棋子个数
int amount = 0;//双方所下棋子的总数
//将棋盘置为空格并打印棋盘
memset(chess_board, ' ', ROW * COL);
Displayboard(chess_board);
//初始化存放行列棋子个数的数组
Initcount(row_col_count);
char ret = 0;
while (1)
{
//玩家下棋
PlayerMove(chess_board, &amount);
//判断输赢
ret = IsWin(p + 1, chess_board, &p_m_angle, &p_n_angle, row - 1, col - 1, &amount);
if (ret != 'C')
break;
Displayboard(chess_board);
//电脑下棋
ComputerMove(chess_board, &amount);
//判断输赢
ret = IsWin(p + 3, chess_board, &c_m_angle, &c_n_angle, c_row, c_col, &amount);
if (ret != 'C')
break;
Displayboard(chess_board);
}
Displayboard(chess_board);
switch (ret)
{
case '*':
printf("赢\n");
break;
case '#':
printf("输\n");
break;
case 'B':
printf("平局\n");
break;
}
}
test.c
#define _CRT_SECURE_NO_WARNINGS
#include "game.h"
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;
}
五. 总结
三子棋游戏的简单实现算是完成了,我在写判断行或列是否符合结束游戏的条件的时候,当时的思路并没有直接想着通过遍历棋盘的某行/某列上的棋子是否都是玩家/电脑的棋子,而是通过创建一个二维数组来存放记录玩家和电脑的棋子在每一行,每一列的个数。由于数组可以随机存取,不需要通过遍历棋盘判断棋子在某行/列是否能连成一条线,只需要获取数组存放某行/列棋子个数的元素值即可。当根据需求需要拓展棋盘的时候,棋盘尺寸过大,遍历棋盘的时间开销相对数组的随机存取要大一些,不过在空间开销上,创建数组需要额外的内存空间,棋盘尺寸越大,空间开销就越大。还有一点就是,在判断行列和两条对角线的棋子是否能连成一条线,都是通过棋子的数量来进行判断的,导致了我在写判断输赢的函数Iswin()
,设置的参数过多,代码可读性降低。
博主是初学者,能力有限,程序还有不少需要改进之处,只能通过今后的学习来提升自己再来改进了。如有错误之处或改进意见,还望大佬们不吝赐教。