目录
前言
三子棋,也称作井字棋,是一种简单但又充满策略的棋类游戏。它常常作为入门级的编程项目,因为其规则相对简单,但在实现过程中涵盖了许多编程概念。
一、游戏的背景和规则
三子棋,也称作井字棋,是一种简单但又充满策略的棋类游戏。它常常作为入门级的编程项目,因为其规则相对简单,但在实现过程中涵盖了许多编程概念。在这个游戏中,两名玩家轮流在一个3x3的棋盘上下棋,目标是先在一条横线、竖线或对角线上连成三个自己的棋子,即获得胜利。
游戏规则简要概括如下:
- 棋盘是一个3x3的网格,共有9个格子。
- 两名玩家轮流行动,其中一个玩家使用" * "标记,另一个使用" # "标记。
- 玩家轮流选择一个空格子,在空格子上放置自己的棋子。
- 第一个成功在横、竖、对角线上连成三个自己的棋子的玩家获得胜利。
- 如果棋盘填满且没有玩家获胜,则宣布平局
- 如下图所示:" * "方赢。
* | # | ---|---|--- | * | # ---|---|--- | | *
二、游戏提纲和思路
1.分析过程
分析过程 步骤 游戏思路 对应函数 1 创建菜单函数,选择 进入游戏 或者 退出游戏 menu() 菜单函数 2 初始化棋盘 initBoard() 初始棋盘函数 3 打印棋盘 showBoard() 打印棋盘函数 4 玩家下棋,并打印出棋盘(玩家输入行、列坐标方式进行落子,' * ' = 玩家落子) playerMove() 玩家下棋函数 5 判断玩家输赢,判断是否继续游戏 isWin() 判断输赢函数 6 电脑下棋(随机位置进行落子,' # ' = 电脑落子) computerMove() 电脑下棋函数 ; computerBerrer() 电脑下棋优化函数 7 同步骤5,判断4种情况,分别是:玩家赢、电脑赢、平局、继续游戏 isFull() 判断棋盘是否满 8 再回到步骤1,选择 进入游戏 或者 退出游戏 \
1.写代码之前的逻辑分析是非常锻炼逻辑思维的一个环节,建议大家在写代码之前先不用着急,可以先在纸上分析一下思路和提纲,可以有效解决写到一半不分东西南北的困境。实在想不出思路可以参考其他人的思路,但是一定不要跳过思考这一环节。刚开始思考可能绞尽脑汁也摸不着头脑,但是多训练几次就会发现自己头脑越来越灵光。
2.有些代码的具体实现的步骤和过程不是一下子全想出的,可以边想边敲代码边调试,路子都是勤劳人民群众摸索出来的。
2.编程方式
传统方式编程:所有的函数均放在main.c里,若使用的模块比较多,则一个文件内会有很多的代码,不利于代码的组织和管理,且不利于编程者思路的清晰。
模块化编程:把各个模块的代码放在不同的.c文件里,在.h文件里提供外部可调用函数的声明,其它.c文件想使用其中的代码时,只需要#include "XXX.h"文件即可。使用模块化编程有助于提高代码的可维护性、可重用性和可扩展性。
tips:当代码(中构造函数)比较多的时候,就可以采用模块化编程来完成这个程序。
三、化思路为代码
1.菜单函数 menu()
void menu()//打印菜单的函数 - 选择 进入游戏 或者 退出游戏
{
printf("******************************\n");
printf("****** 1. play *****\n");
printf("****** 0. exit *****\n");
printf("******************************\n");
}
打印效果如下所示 :
******************************
****** 1. play *****
****** 0. exit *****
******************************
2.初始化棋盘 initBoard()
//符号的定义 - 宏定义 - 方便程序的修改
#define ROW 3 //行
#define COL 3 //列
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] = ' ';//初始化为空格
}
}
}
void game()//存放游戏运行逻辑的函数
{
//存储数据 - 二维数组实现棋盘
char board[ROW][COL];
//初始化棋盘 - 初始化空格
initBoard(board, ROW, COL);
}
(1)使用 #define 宏定义在这里的好处:
a.代码可读性和易维护性: 宏定义可以用来创建有意义的常量或缩写,使代码更具可读性。通过使用宏名称,代码维护人员可以更容易理解和修改代码,因为所有用到宏的地方都会自动更新。
b.一致性: 使用宏定义可以确保在代码中使用相同的常量值,从而提供一致性。如果需要更改常量的值,只需修改一处宏定义,而无需在整个代码库中寻找和修改所有实例。
c.易于更改和调整: 如果代码中存在重复的数字,那么将来如果需要更改这些值会很麻烦,可能导致错误。通过使用宏定义,您只需要修改宏定义的值,而不必在代码中逐个查找和修改。
3.打印棋盘 showBoard()
脑海中要有棋盘(),知道自己想要,什么才能一步步去实现。
首先根据棋盘图,先写一个三子棋的棋盘,如下图代码所示。
打印棋盘,本质是打印数组的内容。
//打印三子棋的棋盘函数
void showBoard(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");
}
}
打印结果如下所示:
******************************
****** 1. play *****
****** 0. exit *****
******************************
请选择:>1
| |
---|---|---
| |
---|---|---
| |
输入第一个坐标记得空一格!
请玩家输入下棋的坐标:>
对代码进行优化,使之能实现多子棋的棋盘打印。
对棋盘构图进行分析:
我们发现棋盘由 " | | "和" ---|---|--- "这俩行交替组成,
我们需要改变row和col的值即可得到对应的结果,
故我们可以写出以下代码:
//优化后的打印棋盘的函数 - 改变row和col的值即可得到对应的结果
void showBoard(char board[ROW][COL], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf(" %c ", board[i][j]);
if (j < col - 1)
printf("|");
}
printf("\n");
//换行是在小for循环结束以后再换行,注意位置
//上面的for循环打印形如" %c | %c | %c \n"
if (i < row - 1)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf("---");
if (j < col - 1)
printf("|");
}
printf("\n");
//上面的for循环打印形如"---|---|---\n"
}
}
}
4.玩家下棋 playerMove()
tips:
(1)判断坐标合法性 - 坐标(x,y)满足 1<=x<=row && 1<=y<=col;
(2)判断坐标是否为空;
(3)如坐标(1,1) 玩家的角度是(1,1),但计算机的视角是(0,0) 故横纵坐标都需减1。
(4)玩家下棋=' * '。
void playerMove(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
printf("输入第一个坐标记得空一格!\n");
printf("请玩家输入下棋的坐标:>");
scanf("%d %d", &x, &y);
//判断坐标合法性
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
//下棋
//判断坐标是否为空
if (board[x - 1][y - 1] == ' ')
//如坐标(1,1) 玩家的角度是(1,1),但计算机的视角是(0,0) 故横纵坐标都减1
{
board[x - 1][y - 1] = '*';
break;
}
else
{
printf("已存在棋子, 请重新输入\n");
}
}
else
{
printf("坐标非法, 请重新输入\n");
}
}
}
虽然每下一次棋就要判断一次输赢,但为了对比理解玩家下棋和电脑下棋的函数,笔者把电脑下棋函数与玩家下棋函数放在相邻的模块进行介绍。
5.电脑下棋 computerMove()
tips:
(1)srand((unsigned int)time(NULL)); 在这里使用rand()函数生成随机数,来让电脑下棋。
#include <stdlib.h> 包含srand函数 #include <time.h> 包含time函数
(2)int x = rand() % row; //生成随机的0~2的横坐标,利用余数的性质控制取值范围
int y = rand() % col; //生成随机的0~2的纵坐标(3)电脑下棋=" # "。
//电脑下棋
void computerMove(char board[ROW][COL], int row, int col)
{
printf("电脑走:>\n");
while (1)
{
int x = rand() % row;//生成随机的0~2的横坐标
int y = rand() % col;//生成随机的0~2的纵坐标
//由于生成的随机数已限制了范围,故无需判断坐标是否合法
//判断占用
if (board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}
补充模块:
电脑下棋优化算法:
(1)若行、列、对角线、反对角线上已出现两个相同的棋子,则要进行拦截。
int computerBerrer(char board[ROW][COL], int row, int col)
{
int i = 0;
//行判断
for (i = 0; i < row; i++)
{
if (board[i][0] == board[i][1] && board[i][2] == ' ')
{
board[i][2] = '#';
return 1;
}
if (board[i][0] == board[i][2] && board[i][1] == ' ')
{
board[i][1] = '#';
return 1;
}
if (board[i][1] == board[i][2] && board[i][0] == ' ')
{
board[i][0] = '#';
return 1;
}
}
//列判断
for (i = 0; i < col; i++)
{
if (board[0][i] == board[1][i] && board[2][i] == ' ')
{
board[2][i] = '#';
return 1;
}
if (board[0][i] == board[2][i] && board[1][i] == ' ')
{
board[1][i] = '#';
return 1;
}
if (board[1][i] == board[2][i] && board[0][i] == ' ')
{
board[0][i] = '#';
return 1;
}
}
//对角线判断
if (board[1][1] == board[0][0] && board[2][2] == ' ')
{
board[2][2] = '#';
return 1;
}
if (board[0][0] == board[2][2] && board[1][1] == ' ')
{
board[1][1] = '#';
return 1;
}
if (board[1][1] == board[2][2] && board[0][0] == ' ')
{
board[0][0] = '#';
return 1;
}
//反对角线判断
if (board[1][2] == board[2][1] && board[3][0] == ' ')
{
board[3][i] = '#';
return 1;
}
if (board[1][2] == board[3][0] && board[2][1] == ' ')
{
board[2][1] = '#';
return 1;
}
if (board[2][1] == board[3][0] && board[1][2] == ' ')
{
board[1][2] = '#';
return 1;
}
return 0;
}
我们把这代码插入computerMove()的中,代码如下:
//电脑下棋
void computerMove(char board[ROW][COL], int row, int col)
{
printf("电脑走:>\n");
if (computerBerrer(board, row, col))//判断是否符合拦截条件
{
;
}
else
{
while (1)
{
int x = rand() % row;//生成随机的0~2的横坐标
int y = rand() % col;//生成随机的0~2的纵坐标
//由于生成的随机数已限制了范围,故无需判断坐标是否合法
//判断占用
if (board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}
}
6.判断输赢 isWin()
(1) 玩家赢了 - *
(2) 电脑赢了 - #
(3) 平局 - Q quit
(4) 游戏继续 - C continue
//判断游戏输赢
char isWin(char board[ROW][COL], int row, int col)
{
int i = 0;
//判断三行
for (i = 0; i < row; i++)
{
if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][1] != ' ')
{
return board[i][1];//
}
}
//判断三列
for (i = 0; i < col; i++)
{
if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[1][i] != ' ')
{
return board[1][i];
}
}
//判断对角线
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];
}
//判断平局
//isFull()函数 - 如果棋盘满了返回1, 不满返回0
int ret = isFull(board, row, col);
if (ret == 1)
{
return 'Q';
}
//继续
return 'C';
}
判断平局
isFull()函数 - 如果棋盘满了返回1, 不满返回0
//判断棋盘是否满
int isFull(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
//如果棋盘满了返回1,不满返回0
for (j = 0; j < col; j++)
{
if (board[i][j] == ' ')
{
return 0;//棋盘没满
}
}
}
return 1;//棋盘满了
}
7.main()函数
void game() - 存放游戏的构造
void game()//存放游戏运行逻辑的函数
{
//存储数据 - 二维数组
char board[ROW][COL];
//初始化棋盘 - 初始化空格
initBoard(board, ROW, COL);
//打印一下棋盘 - 本质是打印数组的内容
showBoard(board, ROW, COL);
char ret = 0;//接受游戏状态
while (1)
{
//玩家下棋
playerMove(board, ROW, COL);
showBoard(board, ROW, COL);
//判断玩家是否赢得游戏
ret = isWin(board, ROW, COL);
if (ret != 'C')
break;
//电脑下棋
computerMove(board, ROW, COL);
showBoard(board, ROW, COL);
//判断电脑是否赢得游戏
ret = isWin(board, ROW, COL);
if (ret != 'C')
break;
}
if (ret == '*')
{
printf("玩家赢了\n");
}
else if (ret == '#')
{
printf("电脑赢了\n");
}
else
{
printf("平局\n");
}
showBoard(board, ROW, COL);
}
int main() - 存放游戏的实现
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
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;
}
四、游戏的总代码
一共放在三个文件中,实现模块化编程。
1.头文件 - gameChess.h - 主要写了三子棋游戏的头文件的引用和函数的声明。
调用函数前一定要先声明。
#pragma once
//本文件主要写了三子棋游戏的头文件的引用和函数的声明
//头文件的包含
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
//符号的定义
#define ROW 3
#define COL 3
//函数的声明
//初始化棋盘的
void initBoard(char board[ROW][COL], int row, int col);
//打印棋盘的函数
void showBoard(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);
//电脑下棋优化
int computerBerrer(char board[ROW][COL], int row, int col);
//
//1. 玩家赢了 - *
//2. 电脑赢了 - #
//3. 平局 - Q
//4. 游戏继续 - C
//判断游戏是否有输赢
char isWin(char board[ROW][COL], int row, int col);
2.源文件 - gameChess.c - 主要写了三子棋游戏中各种函数的具体的构造方法和实现过程。
#define _CRT_SECURE_NO_WARNINGS 1
#include"gameChess.h"
//本文件主要写了三子棋游戏中各种函数的具体的构造方法和实现过程
//
//由于其他函数声明已写在gameChess.h文件中,故只引用gameChess.h即可。
//初始化棋盘
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] = ' ';//初始化为空格
}
}
}
//打印三子棋的棋盘函数
//void showBoard(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");
// }
//}
//优化后的打印棋盘的函数 - 改变row和col的值即可得到对应的结果
void showBoard(char board[ROW][COL], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf(" %c ", board[i][j]);
if (j < col - 1)
printf("|");//打印形如" %c | %c | %c \n"
}
printf("\n");//换行是在小for循环结束以后再换行,注意位置
if (i < row - 1)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf("---");
if (j < col - 1)
printf("|");
}
printf("\n"); //打印形如"---|---|---\n"
}
}
}
void playerMove(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
printf("输入第一个坐标记得空一格!\n");
printf("请玩家输入下棋的坐标:>");
scanf("%d %d", &x, &y);
//判断坐标合法性
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
//下棋
//判断坐标是否为空
if (board[x - 1][y - 1] == ' ')
//如坐标(1,1) 玩家的角度是(1,1),但计算机的视角是(0,0) 故横纵坐标都减1
{
board[x - 1][y - 1] = '*';
break;
}
else
{
printf("已存在棋子, 请重新输入\n");
}
}
else
{
printf("坐标非法, 请重新输入\n");
}
}
}
int computerBerrer(char board[ROW][COL], int row, int col)
{
int i = 0;
//行判断
for (i = 0; i < row; i++)
{
if (board[i][0] == board[i][1] && board[i][2] == ' ')
{
board[i][2] = '#';
return 1;
}
if (board[i][0] == board[i][2] && board[i][1] == ' ')
{
board[i][1] = '#';
return 1;
}
if (board[i][1] == board[i][2] && board[i][0] == ' ')
{
board[i][0] = '#';
return 1;
}
}
//列判断
for (i = 0; i < col; i++)
{
if (board[0][i] == board[1][i] && board[2][i] == ' ')
{
board[2][i] = '#';
return 1;
}
if (board[0][i] == board[2][i] && board[1][i] == ' ')
{
board[1][i] = '#';
return 1;
}
if (board[1][i] == board[2][i] && board[0][i] == ' ')
{
board[0][i] = '#';
return 1;
}
}
//对角线判断
if (board[1][1] == board[0][0] && board[2][2] == ' ')
{
board[2][2] = '#';
return 1;
}
if (board[0][0] == board[2][2] && board[1][1] == ' ')
{
board[1][1] = '#';
return 1;
}
if (board[1][1] == board[2][2] && board[0][0] == ' ')
{
board[0][0] = '#';
return 1;
}
//斜对角线判断
if (board[1][2] == board[2][1] && board[3][0] == ' ')
{
board[3][i] = '#';
return 1;
}
if (board[1][2] == board[3][0] && board[2][1] == ' ')
{
board[2][1] = '#';
return 1;
}
if (board[2][1] == board[3][0] && board[1][2] == ' ')
{
board[1][2] = '#';
return 1;
}
return 0;
}
//电脑下棋
void computerMove(char board[ROW][COL], int row, int col)
{
printf("电脑走:>\n");
if (computerBerrer(board, row, col))//判断是否符合拦截条件
{
;
}
else
{
while (1)
{
int x = rand() % row;//生成随机的0~2的横坐标
int y = rand() % col;//生成随机的0~2的纵坐标
//由于生成的随机数已限制了范围,故无需判断坐标是否合法
//判断占用
if (board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}
}
//判断棋盘是否满
int isFull(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
//如果棋盘满了返回1,不满返回0
for (j = 0; j < col; j++)
{
if (board[i][j] == ' ')
{
return 0;//棋盘没满
}
}
}
return 1;//棋盘满了
}
//1. 玩家赢了 - *
//2. 电脑赢了 - #
//3. 平局 - Q quit
//4. 游戏继续 - C continue
//判断游戏输赢
char isWin(char board[ROW][COL], int row, int col)
{
int i = 0;
//判断三行
for (i = 0; i < row; i++)
{
if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][1] != ' ')
{
return board[i][1];//
}
}
//判断三列
for (i = 0; i < col; i++)
{
if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[1][i] != ' ')
{
return board[1][i];
}
}
//判断对角线
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];
}
//判断平局
//isFull()函数 - 如果棋盘满了返回1, 不满返回0
int ret = isFull(board, row, col);
if (ret == 1)
{
return 'Q';
}
//继续
return 'C';
}
3.源文件 - test.c - 主要写了三子棋游戏运行的主要逻辑。
#define _CRT_SECURE_NO_WARNINGS 1
#include"gameChess.h"
//本文件主要写了三子棋游戏运行的主要逻辑
void menu()//打印菜单的函数
{
printf("******************************\n");
printf("****** 1. play *****\n");
printf("****** 0. exit *****\n");
printf("******************************\n");
}
void game()//存放游戏运行逻辑的函数
{
//存储数据 - 二维数组
char board[ROW][COL];
//初始化棋盘 - 初始化空格
initBoard(board, ROW, COL);
//打印一下棋盘 - 本质是打印数组的内容
showBoard(board, ROW, COL);
char ret = 0;//接受游戏状态
while (1)
{
//玩家下棋
playerMove(board, ROW, COL);
showBoard(board, ROW, COL);
//判断玩家是否赢得游戏
ret = isWin(board, ROW, COL);
if (ret != 'C')
break;
//电脑下棋
computerMove(board, ROW, COL);
showBoard(board, ROW, COL);
//判断电脑是否赢得游戏
ret = isWin(board, ROW, COL);
if (ret != 'C')
break;
}
if (ret == '*')
{
printf("玩家赢了\n");
}
else if (ret == '#')
{
printf("电脑赢了\n");
}
else
{
printf("平局\n");
}
showBoard(board, ROW, COL);
}
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
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;
}
五、运行结果
******************************
****** 1. play *****
****** 0. exit *****
******************************
请选择:>1
| |
---|---|---
| |
---|---|---
| |
输入第一个坐标记得空一格!
请玩家输入下棋的坐标:>1 1
* | |
---|---|---
| |
---|---|---
| |
电脑走:>
* | |
---|---|---
| | #
---|---|---
| |
输入第一个坐标记得空一格!
请玩家输入下棋的坐标:>2 2
* | |
---|---|---
| * | #
---|---|---
| |
电脑走:>
* | |
---|---|---
| * | #
---|---|---
| | #
输入第一个坐标记得空一格!
请玩家输入下棋的坐标:>1 2
* | * |
---|---|---
| * | #
---|---|---
| | #
电脑走:>
* | * | #
---|---|---
| * | #
---|---|---
| | #
电脑赢了
* | * | #
---|---|---
| * | #
---|---|---
| | #
******************************
****** 1. play *****
****** 0. exit *****
******************************
请选择:>0
退出游戏
如果对您有帮助请点个免费的👍支持一下吧,欢迎大家在讨论区交流讨论,提出珍贵的改进建议,这里是努力学习进步的小林子,我们下期再见~