1. 前言
三子棋,也称井字棋,该游戏进行起来十分方便,只需要一支笔和一张纸就可以进行游戏,是上课摸鱼的好游戏(bushi),也是我的童年回忆。
本篇博客将采用C语言来模拟实现简单的三子棋游戏。
人机大战,谁能更胜一筹?
2. 游戏分工
该项目分为三个文件:
test.c
:游戏的逻辑game.h
:函数声明,符号的定义game.c
:游戏的实现
其中game.h,game.c为游戏模块,test.c为测试区域。
3. 游戏菜单
在进入游戏前,我们需要让玩家通过菜单
选择开始游戏或退出游戏。
游戏菜单应该实现三个功能:
- 进入游戏
- 退出游戏
- 非法输入的返回提示和说明
而对于游戏,我们至少需要玩一次,应该用do...while
循环来完成游戏的重复游玩功能。对于游戏选项,则可以用switch
语句完成。
对应代码:
void menu() { printf("**********************************\n"); printf("************* 1.play *************\n"); printf("************* 0.exit *************\n"); printf("**********************************\n"); } void test() { int input = 0; do { menu(); printf("请选择:>"); scanf("%d", &input); switch (input) { case 1: printf("三子棋\n"); break; case 0: printf("退出游戏\n"); break; default: printf("选择错误,请重新选择\n"); break; } } while (input); } int main() { test(); return 0; }
运行结果:
4. 游戏过程
友情提示:
以下模块均用于实现游戏功能,对应的功能均在test.c的game()函数中调用、game.h中进行声明和相关常量的定义,函数功能在game.c中实现。
在.c文件中均需引自定义的头文件#include"game.h"。
对于函数的声明我会省略,以下模块展示的内容主要为在game.c文件中实现的游戏功能。
4.1 前提准备
为了提高代码的专业性和方便对代码进行修改,我们将对于行和列进行定义(game.h).
对应代码:
#pragma once #include<stdio.h> #define ROW 3 #define COL 3
4.2 棋盘的初始化
既然是三子棋
,那棋盘并不可少。在进行游戏前,我们需要将棋盘中的元素都置为空白,以便后续进行下棋的操作。
棋盘,无非就是一个3 * 3的二维数组,我们在game()
函数中定义:
void game() { //棋盘的创建 char board[ROW][COL] = { 0 }; //初始化棋盘 init_board(board, ROW, COL); }
对于棋盘的初始化,我们直接利用嵌套循环即可:
void init_board(char board[ROW][COL], int row, int col) { int i = 0; for (i = 0; i < col; i++) { int j = 0; for (j = 0; j < col; j++) { board[i][j] = ' ';//置为空白 } } }
4.3 打印棋盘
在完成棋盘的初始化后,我们需要将初始化后的棋盘展示给玩家看,棋盘如图所示:
棋盘分为三块:
- 数据,一个数据表示为
空格数据空格
的形式. - 分隔符
|
,分隔符的个数每行均为两个,边界不需要分隔符。 - 分隔线
---
,该分割线只存在于第一、二行数据的下方。
对于分隔符可以用列来进行限制,对于分隔线则是用行来限制:
void display_board(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"); if (i < row - 1)//最后一行没有 { int j = 0; for (j = 0; j < col; j++) { printf("---"); if (j < col - 1) printf("|"); } printf("\n"); } } }
4.4 对弈过程
对弈过程不止一次,直到分出胜负为止,因此在game()
函数中应用循环表示。
玩家下棋
思路:
玩家下棋的输入我们用坐标表示,但是需要明确一点:并不是所有玩家都了解C语言数组下标,因此玩家输入的坐标需要进行转化。
输入坐标的取值范围为:1~3,若不在范围内应提示输入错误。
玩家棋子用*表示,若选择坐标为空白,则将坐标置为*,并退出,让电脑进行后续操作,若选择坐标不是空白,则操作为悔棋操作,应提示重新输入。
对应代码:
void player_move(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] == ' ')//坐标转换和下棋 { board[x - 1][y - 1] = '*'; break;//注意退出 } else { printf("该坐标已有棋子,请重新选择\n"); } } else { printf("选择错误,请重新选择\n"); } } }
电脑下棋(简版,智能化下棋中会优化)
思路:
电脑为自动下棋,并不需要手动输入坐标。
自动随机值的提供需要利用时间戳来完成,和上次的猜数字小游戏相同,利用rand、srand、time函数来完成相关操作,srand函数在test中调用,rand函数在电脑下棋中用临时变量接收。
随机值取值范围应在0~2之间,由行和列的值都为3,直接让rand()%3即可(数值1模数值2结果为0~数值2-1)
电脑棋子用#表示,由于自动下棋并不需要人为操作,只要条件判断即可,不需要输入提示。
对应代码:
void computer_move(char board[ROW][COL], int row, int col) { int x = 0; int y = 0; printf("电脑下棋\n"); while (1) { x = rand() % row;//0-2 y = rand() % col;//0-2 if (board[x][y] == ' ') { board[x][y] = '#'; break;//注意退出 } } }
4.5 判断输赢
我们用以下方式判定游戏情况:
玩家赢 - *
电脑赢 - #
平局 - Q
继续 - c
思路:
赢取游戏,分为四种情况,行、列、主对角线、副对角线所占元素均相同且不为空。
在判断输赢后未得出结果,则说明游戏平局,我们可以封装一个函数判断棋盘是否满了,用返回值为1则说明平局,且该函数仅为判断输赢做辅助作用,并不需要声明,且不需要被其他文件所发现。
若输赢和平局并不满足,则说明游戏继续。
在game()中应对相应的返回结果做出判断,且在玩家和电脑下棋后均应该判断棋盘是否下满,下满则直接进入判断部分。
结合对弈过程和判断在game()函数的对应代码:
void game() { while (1) { //玩家下棋 player_move(board, ROW, COL); //玩家下棋后展示棋盘 display_board(board, ROW, COL); //判断输赢 ret = is_win(board, ROW, COL); if (ret != 'C') { break; } //电脑下棋 computer_move(board, ROW, COL); //电脑下棋后棋盘展示 display_board(board, ROW, COL); ret = is_win(board, ROW, COL); if (ret != 'C') { break; } } if (ret == '*') { printf("玩家赢\n"); } else if (ret == '#') { printf("电脑赢了\n"); } else { printf("平局\n"); } }
game.c
对应代码:
static int is_full(char board[ROW][COL], int row, int col)//static修饰为静态函数,只为is_win起辅助作用 { int i = 0; for (i = 0; i < row; i++) { int j = 0; for (j = 0; j < col; j++) { if (board[i][j] == ' ') { return 0; } } } return 1;//下满了 } char is_win(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][i] != ' ') { 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[2][0] == board[1][1] && board[1][1] == board[0][2] && board[1][1] != ' ') { return board[1][1]; } if (is_full(board, row, col) == 1) { return 'Q'; } return 'C'; }
4.6 智能化下棋(※)
对于电脑下棋,原先的代码使得在下棋过程中,电脑的操作过于呆板,没有挑战性。所以我们可以对电脑下棋进行升级。
智能化下棋分为两部分:
电脑判断是否能赢并下棋
电脑阻拦玩家下棋
思路:
我们封装一个函数,被computer_move函数所调用,函数中实现这两个功能。
这两个操作都是对于行、列、主副对角线进行判断,它们的实现逻辑是相同的,所以我们可以把它们在同一函数中实现,电脑赢棋的取决于一条路线上是否有两个#,阻拦玩家下棋则取决于一条路线上是否有两个*。
所以我们在传参时,只需要把元素传入,这样就可以将两部分在一个函数中实现。
对应代码:
static int intelligent_computer(char board[ROW][COL], int row, int col, char ch)//ch为'#'进行下棋,ch为'*'进行阻拦 { //检查电脑能否获得胜利/阻拦玩家 int i = 0; //行 for (i = 0; i < row; i++) { int j = 0; for (j = 0; j < col; j++) { //判断第一个和第二个的元素电脑是否已下棋,对第三个元素进行下棋/阻拦 if (board[i][0] == board[i][1] && board[i][0] == ch && board[i][2] == ' ') { board[i][2] = '#'; return 1; } //判断第一个和第三个的元素电脑是否已下棋,对第二个元素进行下棋/阻拦 if (board[i][0] == board[i][2] && board[i][0] == ch && board[i][1] == ' ') { board[i][1] = '#'; return 1; } //判断第二个和第三个的元素电脑是否已下棋,对第一个元素进行下棋/阻拦 if (board[i][1] == board[i][2] && board[i][1] == ch && board[i][0] == ' ') { board[i][0] = '#'; return 1; } } } //列 for (i = 0; i < row; i++) { int j = 0; for (j = 0; j < col; j++) { //第一和第二个已下,下/阻拦第三个 if (board[0][j] == board[1][j] && board[0][j] == ch && board[2][j] == ' ') { board[2][j] = '#'; return 1; } //第一和第三个已下,下/阻拦第二个 if (board[0][j] == board[2][j] && board[0][j] == ch && board[1][j] == ' ') { board[1][j] = '#'; return 1; } //第二和第三个已下,下/阻拦第一个 if (board[1][j] == board[2][j] && board[1][j] == ch && board[0][j] == ' ') { board[0][j] = '#'; return 1; } } } //主对角线 if (board[0][0] == board[1][1] && board[1][1] == ch && board[2][2] == ' ')//落子/阻拦主对角线第三个元素 { board[2][2] = '#'; return 1; } if (board[0][0] == board[2][2] && board[0][0] == ch && board[1][1] == ' ')//落子/阻拦主对角线第二个元素 { board[1][1] = '#'; return 1; } if (board[1][1] == board[2][2] && board[1][1] == ch && board[0][0] == ' ')//落子/阻拦主对角线第一个元素 { board[0][0] = '#'; return 1; } //副对角线 if (board[0][2] == board[1][1] && board[0][2] == ch && board[2][0] == ' ')//落子/阻拦副对角线第三个元素 { board[2][0] = '#'; return 1; } if (board[0][2] == board[2][0] && board[0][2] == ch && board[1][1] == ' ')//落子/阻拦副对角线第二个元素 { board[1][1] = '#'; return 1; } if (board[1][1] == board[2][0] && board[1][1] == ch && board[0][2] == ' ')//落子/阻拦副对角线第一个元素 { board[0][2] = '#'; return 1; } return 0;//若无法赢棋或无法阻拦,返回0 }
注:对于原先的computer_move函数也需要做出相应改进,改进内容在完整展示部分。
5. 完整展示
game.h
#pragma once #include<stdio.h> #include<stdlib.h> #include<time.h> #include<windows.h> #define ROW 3 #define COL 3 //初始化棋盘 void init_board(char board[ROW][COL], int row, int col); //打印棋盘 void display_board(char board[ROW][COL], int row, int col); //玩家下棋 void player_move(char board[ROW][COL], int row, int col); //电脑下棋 void computer_move(char board[ROW][COL], int row, int col); //判断输赢 char is_win(char board[ROW][COL], int row, int col);
game.c
#define _CRT_SECURE_NO_WARNINGS 1 #include"game.h" void init_board(char board[ROW][COL], int row, int col) { int i = 0; for (i = 0; i < col; i++) { int j = 0; for (j = 0; j < col; j++) { board[i][j] = ' '; } } } void display_board(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"); if (i < row - 1) { int j = 0; for (j = 0; j < col; j++) { printf("---"); if (j < col - 1) printf("|"); } printf("\n"); } } } void player_move(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] == ' ') { board[x - 1][y - 1] = '*'; break; } else { printf("该坐标已有棋子,请重新选择\n"); } } else { printf("选择错误,请重新选择\n"); } } } static int intelligent_computer(char board[ROW][COL], int row, int col, char ch)//ch为'#'进行下棋,ch为'*'进行阻拦 { //检查电脑能否获得胜利/阻拦玩家 int i = 0; //行 for (i = 0; i < row; i++) { int j = 0; for (j = 0; j < col; j++) { //判断第一个和第二个的元素电脑是否已下棋,对第三个元素进行下棋/阻拦 if (board[i][0] == board[i][1] && board[i][0] == ch && board[i][2] == ' ') { board[i][2] = '#'; return 1; } //判断第一个和第三个的元素电脑是否已下棋,对第二个元素进行下棋/阻拦 if (board[i][0] == board[i][2] && board[i][0] == ch && board[i][1] == ' ') { board[i][1] = '#'; return 1; } //判断第二个和第三个的元素电脑是否已下棋,对第一个元素进行下棋/阻拦 if (board[i][1] == board[i][2] && board[i][1] == ch && board[i][0] == ' ') { board[i][0] = '#'; return 1; } } } //列 for (i = 0; i < row; i++) { int j = 0; for (j = 0; j < col; j++) { //第一和第二个已下,下/阻拦第三个 if (board[0][j] == board[1][j] && board[0][j] == ch && board[2][j] == ' ') { board[2][j] = '#'; return 1; } //第一和第三个已下,下/阻拦第二个 if (board[0][j] == board[2][j] && board[0][j] == ch && board[1][j] == ' ') { board[1][j] = '#'; return 1; } //第二和第三个已下,下/阻拦第一个 if (board[1][j] == board[2][j] && board[1][j] == ch && board[0][j] == ' ') { board[0][j] = '#'; return 1; } } } //主对角线 if (board[0][0] == board[1][1] && board[1][1] == ch && board[2][2] == ' ')//落子/阻拦主对角线第三个元素 { board[2][2] = '#'; return 1; } if (board[0][0] == board[2][2] && board[0][0] == ch && board[1][1] == ' ')//落子/阻拦主对角线第二个元素 { board[1][1] = '#'; return 1; } if (board[1][1] == board[2][2] && board[1][1] == ch && board[0][0] == ' ')//落子/阻拦主对角线第一个元素 { board[0][0] = '#'; return 1; } //副对角线 if (board[0][2] == board[1][1] && board[0][2] == ch && board[2][0] == ' ')//落子/阻拦副对角线第三个元素 { board[2][0] = '#'; return 1; } if (board[0][2] == board[2][0] && board[0][2] == ch && board[1][1] == ' ')//落子/阻拦副对角线第二个元素 { board[1][1] = '#'; return 1; } if (board[1][1] == board[2][0] && board[1][1] == ch && board[0][2] == ' ')//落子/阻拦副对角线第一个元素 { board[0][2] = '#'; return 1; } return 0;//若无法赢棋或无法阻拦,返回0 } void computer_move(char board[ROW][COL], int row, int col) { printf("电脑下棋\n"); int sign1 = 0; int sign2 = 0; sign1 = intelligent_computer(board, ROW, COL, '#');//电脑智能赢棋 if (sign1 == 0)//若电脑无法赢棋,则进行阻拦 { sign2 = intelligent_computer(board, ROW, COL, '*');//阻拦玩家下棋 if (sign2 == 0)//若赢棋和阻拦都不满足,电脑自行下棋 { int x = 0; int y = 0; while (1) { x = rand() % row; y = rand() % col; if (board[x][y] == ' ') { board[x][y] = '#'; break; } } } } } static int is_full(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++) { if (board[i][j] == ' ') { return 0; } } } return 1; } char is_win(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[2][0] == board[1][1] && board[1][1] == board[0][2] && board[1][1] != ' ') { return board[1][1]; } if (is_full(board, row, col) == 1) { return 'Q'; } return 'C'; }
test.c
6. 动画展示
7. 结语
到这里,一个简易三子棋游戏也就实现成功了!
我们发现经过智能化的电脑下棋还是很厉害的嘛,我都输了好几次,不过经我挑战后,我赢的次数还是比电脑多的,所以本次人机大战,我宣布,由anduin我,获得胜利!