游戏简介>>>
三子棋的游戏如下图,游戏规则是 两个玩家分别在 3x3 的棋盘上下棋,只要有一个玩家下的3个棋子先连成一条了直线,那么他就获胜。如果棋盘满了还没有分出胜负那么就定为平局。
接下来我们使用C语言写一个玩家与电脑PK的三子棋游戏。
注意: 我们在写一些重要的代码时,最好把自定义函数的声明、定义以及整个程序的实现,分别放在不同的文件里面。这样写不仅会让你的逻辑更清晰、更容易调试代码还便于后续对整个代码的保密操作。
关于三子棋代码,我按照上文分为 game.h \ game.c \ test.c 三个文件,分别写自定义函数的声明、定义以及整个程序的实现。然后记得一边写一边测试,这样有错误的话改起来不费力。
三子棋代码的实现思路>>>
先来分析一下三子棋游戏的组成成分和动作:游戏开始和结束的提示、棋盘、两种棋子、玩家下棋、判断输赢和电脑下棋。
游戏开始和结束的提示>>>
游戏的第一步一般都是让玩家选择“开始游戏”或者“退出游戏”,因为有人可能只是手滑而点进了这个游戏。还得注意为了方便以后写代码不繁琐且逻辑清晰,我们尽量都自定义函数来实现这些需要重复的操作,包括接下来的各种功能。 所以这里需要写一个函数 menu 让玩家选择是否开始游戏。
//像这样自定义一个功能单一的函数menu只负责打印选项单
void menu()
{
printf("********************\n");
printf("****** 1.play ******\n");
printf("****** 0.exit ******\n");
printf("********************\n");
}
//当玩家选择1后开始游戏,进入 game函数
void test()
{
int a = 0;
do
{
menu();
scanf("%d", &a);
if (1 == a)
{
game();
break;
}
else
{
printf("输入错误,请重新输入:\n");
}
} while (a);
printf("退出游戏。\n");
}
//当玩家不想玩后选择0,即结束游戏
这里使用 do while 循环是因为一开始就先要让玩家判断是否开始游戏,如果玩家输入错误需要重新输入(先执行一次,后判断是否循环),所以输入这个操作也要放在循环里。
把menu函数也放在这个循环中,等一局游戏结束后再次出现menu选项的具体数值方便玩家继续选择。
先把最开始的逻辑写好(是否开始)。如果玩家选择了1.开始游戏,将进入 game 函数,我将三子棋游戏的相关函数都在这个函数调用。
棋盘、两种棋子>>>
根据我们对游戏的了解,开始前要打印一次设计好的棋盘,上面没有棋子。后续每次下棋后都要同时打印棋盘和棋子。
下棋的数据都要存储起来并对应到棋子上。让玩家下棋的方法可以试试让玩家输入行数和列数 这个操作就像输入二维数组的行和列,而且直观上对应着棋盘我们也可能会想到使用二维数组来实现,二维数组的行和列刚好对应了棋盘的行和列。所以刚开始要把这个二维数组初始化为全空格(这样比较简洁),后面下棋时在赋值(棋子)。我选择了 * 作为玩家棋子, # 作为电脑棋子。
玩家看到的棋盘应该是由一些分隔的线和能够下棋的空位置(上文的二维数组)组成。这些线方便玩家看出棋盘的行列,二维数组要存放棋子。也可以理解为分隔线是视觉上的棋盘,二维数组才是真的棋盘(放置棋子)。所以它们是一体,设计函数时得注意。
让二维数组刚开始看起来是能够下棋的空位置,需要把每个元素都初始化为 ‘ ’(空格)。
//初始化下棋的位置为全空格
void chessboard(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 print_chessboard(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++)
{
//注意|比空格的数量少一
printf(" %c ", board[i][j]);
if (j < row - 1)
printf("|");
}
printf("\n");
//打印分割每一行的---
if (i < row - 1)
{
for (j = 0; j < col; j++)
{
printf("---");
if (j < col - 1)
printf("|");
}
printf("\n");
}
}
}
我们传递三个参数是因为:函数都需要对棋盘进行操作,所以有第一个参数。而棋盘的行数和列数或许会改变而且二维数组和函数里的循环需要这两个变量 ,很重要,于是也需要这些参数。 至于循环里的i、j变量对于我们操作棋盘只起到辅助作用,使用的时候在定义就好,而且尽量减少不必要的参数,否则参数过多不好记忆。
函数的返回类型是 void 是因为我们直接对char board[ROW][COL] 这个二维数组操作了,既传址调用(char a[ ] 本质上是 char *a 类型),不需要返回值。
在这里我使用了#define 定义标识符常量。因为如果将来你想要扩大棋盘,直接修改这里会很方便。
( 我们要注意代码的灵活性、可重用性等,所以在设计代码时就得对这些进行思考,尽量把能想到的、可能出错的问题都在代码的设计之除想好。这样写出来的代码才是好的代码。)
#define ROW 3
#define COL 3
玩家下棋>>>
先提示玩家下棋、输入行数和列数,然后打印出玩家下好棋后的棋盘。在之前我们把这个二维数组的每个元素都赋值为 ‘ ’,这时候只要把选中的‘ ’改为棋子,再次调用上文写好的打印棋盘的函数。
但值得注意的是“ 把选中的‘ ’改为棋子 ” ,我们都知道没有棋子的位置才可以下棋。所以在每次 下棋 这个动作开始之前都要让电脑判断 这个位置是否可以下棋。我们得把玩家想成很会手滑的小白(doge)。
当玩家下棋时,他输入的应该是直接看到的1/2/3行,而数组是通过下标访问的,实际的电脑操作是应该把玩家输入的坐标减一。还要注意数组不可以越界访问,应该对手滑小白玩家输入的行数和列数进行判断,在1~3范围内才可以给他下棋,否则请提示他请重新输入。
//玩家下棋
void player_move(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
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");
}
}
}
判断输赢>>>
下棋后一共会出现四种结果:玩家赢、电脑赢、平局和继续。写函数时对应这四种情况分别返回 ‘ * ’、'#'、‘Q’、‘C’ 。
在每次下棋后都应该做一次判断。
先判断输赢,这里得对每种可能赢的结果都要判断!没赢才关心平局,不平局才继续。
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[0][2] == board[1][1] && board[1][1] == board[2][0] && board[1][1] != ' ')
{
return board[1][1];
}
}
如果赢了那么返回棋子,通过判断是哪种棋子就知道是谁赢。所以这里的函数设计不用分别对玩家和电脑进行判断,代码不冗余。
//判断棋盘是否满了,没赢就满是平局
static int if_full(char board[ROW][COL], int row, int col)
{
//这个函数只为 is_win 服务,所以可以限制他的使用范围只在本文件中
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 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[0][2] == board[1][1] && board[1][1] == board[2][0] && board[1][1] != ' ')
{
return board[1][1];
}
//这里的return是按我们分析的判断顺序执行的,
//如果上面输赢判断出来了那么该函数会直接跳出去的
//判断平局
if (if_full(board, row, col) == 1)
{
return 'Q';
}
//继续
return 'C';
}
判断平局这个函数只为下面这个函数服务,我们可以用 static 修饰它,让它只能在这个文件中使用,不暴露它。这种行为也是对代码的一种保护。
电脑下棋>>>
电脑下棋和玩家差不多,电脑直接下棋不用给提示。电脑是随机下棋,这里需要使用随机数。(关于这个函数先不在这里细讲了,不然篇幅太大,我先鸽一下......)
//电脑下棋
void computer_move(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
x = rand() % row;//row=3时x的结果范围: 0~2
y = rand() % col;
if ( x>=0 && x <= row && y >= 0 && y <= col && board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}
总代码>>>
先来这个game函数的逻辑。如果下棋后没有 “ 继续 ” 命令,那么将跳出循环输出结果。
void game()
{
char ch = 0;
//创建二维数组存放下棋数据
char board[ROW][COL] = { 0 };
//初始化棋盘
chessboard(board, ROW, COL);
//打印棋盘
print_chessboard(board, ROW, COL);
//随机数
srand((unsigned int)time(NULL));
while (1)
{
printf(" 玩家下棋 \n");
//玩家下棋(接着打印下棋结果)
player_move(board, ROW, COL);
print_chessboard(board, ROW, COL);
//判断输赢
if ((ch = is_win(board, ROW, COL)) != 'C')
break;
//电脑下棋(接着打印下棋结果)
printf(" 电脑下棋 \n");
Sleep(1000);//让玩家的有点感觉,不要太慌
computer_move(board, ROW, COL);
print_chessboard(board, ROW, COL);
//判断输赢
if ((ch = is_win(board, ROW, COL)) != 'C')
break;
}
if ('*' == ch)
printf("玩家赢。\n ");
if ('#' == ch)
printf("电脑赢。\n ");
if ('Q' == ch)
printf("平局。\n ");
}
注意中间使用了 Sleep 函数,使程序休息,单位毫秒(1000毫秒=1秒)。该函数的头文件是windows.h 。Sleep函数是windows环境下的一个头文件,所以只能在windows环境下使用。使用时直接在需要休息的地方写(S 开头是大写的) Sleep(1000); // 表示程序休息1秒。(感觉上达到状态进行中的效果)
下面附上整个代码: (注意有三个文件)
// game.h
#define _CRT_SECURE_NO_WARNINGS 1
#define ROW 3
#define COL 3
#include<stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
//注意三子棋游戏实现的逻辑
//相关自定义函数的声明
//打印 功能选项单
void menu();
//初始化棋盘
void chessboard(char board[ROW][COL], int row, int col);
//打印棋盘
void print_chessboard(char board[ROW][COL], int row, int col);
void game();
//玩家下棋(
void player_move(char board[ROW][COL], int row, int col);
//判断输赢
char is_win(char board[ROW][COL], int row, int col);
//电脑下棋
void computer_move(char board[ROW][COL], int row, int col);
// game.c
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include"game.h"
//三子棋游戏的相关函数定义如下
void menu()
{
printf("********************\n");
printf("****** 1.play ******\n");
printf("****** 0.exit ******\n");
printf("********************\n");
}
//初始化棋盘
void chessboard(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 print_chessboard(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++)
{
//注意|比空格的数量少一
printf(" %c ", board[i][j]);
if (j < row - 1)
printf("|");
}
printf("\n");
//打印分割每一行的---
if (i < row - 1)
{
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");//得给个提示,告诉玩家可以开始下棋了
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 computer_move(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
x = rand() % row;//row=3时x的结果范围: 0~2
y = rand() % col;
if ( x>=0 && x <= row && y >= 0 && y <= col && board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}
//判断输赢 //这里思考下判断顺序,先判断谁是否赢了?没赢就看是否平局?没有平局才可以继续下棋
//判断棋盘是否满了,没赢就满是平局
static int if_full(char board[ROW][COL], int row, int col)
{
//这个函数只为 is_win 服务,所以可以限制他的使用范围只在本文件中
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 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[0][2] == board[1][1] && board[1][1] == board[2][0] && board[1][1] != ' ')
{
return board[1][1];
}
//这里的return是按我们分析的判断顺序执行的,
//如果上面输赢判断出来了那么该函数会直接跳出去的
//判断平局
if (if_full(board, row, col) == 1)
{
return 'Q';
}
//继续
return 'C';
}
// test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
//测试三子棋游戏
void test()
{
int a = 0;
do
{
menu();
scanf("%d", &a);
if (1 == a)
{
game();
break;
}
else
{
printf("输入错误,请重新输入:\n");
}
} while (a);
printf("退出游戏。\n");
}
void game()
{
char ch = 0;
//创建二维数组存放下棋数据
char board[ROW][COL] = { 0 };
//初始化棋盘
chessboard(board, ROW, COL);
//打印棋盘
print_chessboard(board, ROW, COL);
//随机数
srand((unsigned int)time(NULL));
while (1)
{
printf(" 玩家下棋 \n");
//玩家下棋(接着打印下棋结果)
player_move(board, ROW, COL);
print_chessboard(board, ROW, COL);
//判断输赢
if ((ch = is_win(board, ROW, COL)) != 'C')
break;
//电脑下棋(接着打印下棋结果)
printf(" 电脑下棋 \n");
Sleep(1000);//让玩家的有点感觉,不要太慌
computer_move(board, ROW, COL);
print_chessboard(board, ROW, COL);
//判断输赢
if ((ch = is_win(board, ROW, COL)) != 'C')
break;
}
if ('*' == ch)
printf("玩家赢。\n ");
if ('#' == ch)
printf("电脑赢。\n ");
if ('Q' == ch)
printf("平局。\n ");
}
int main()
{
test();
return 0;
}
最终效果图>>>
写完的思考>>>
使用 C语言这个面向过程的程序设计语言,就是要把过程细化细化在细化。把相同的操作可以一起做,稍有差别的操作记得用 if 语句写判断是否执行的条件,不同的操作需要仔细的分开来写,慢慢、细致的写好每一步,因为每一步都很重要。 (如上文打印棋盘的那段代码)
在写之前需要好好想一想可以会出现的各种情况,如上文写道的很会手滑的小白玩家的离谱操作。这时候得注意着数组下标越界,输入的数据不在适用范围,输入错误等这些情况并细致写好。 还要注意代码的灵活性、可重用性等。(如上文玩家下棋的那段代码)
最重要的话>>>
不理解的记得私信我哦 ^ ^。
(直接评论区留言也可以^ ^)