博主CSDN主页:杭电码农-NEO

⏩专栏分类:C语言学习分享⏪

代码仓库:NEO的学习日记

关注我带你学习更多C语言知识



三子棋

  • 1. 前言
  • 2. 思路分析
    • 2.1 创建文件
    • 2.2 实现功能需要的函数
    • 2.3 main函数代码思考
  • 3. 代码实现
    • 3.1 main函数
    • 3.2 game.h源文件
    • 3.3 初始化函数
    • 3.4 打印棋盘
    • 3.5 玩家下棋
    • 3.6 电脑下棋
    • 3.7 game函数的设计
    • 3.8 判断棋盘是否满格
    • 3.9 判断输赢
  • 4. 所有代码

1. 前言

我们学习完数组之后,可以自己做一些小游戏来巩固我们的知识,
这里的三子棋就是其中一个偏简单的小游戏.详细的实现请看下面的分析


2. 思路分析

2.1 创建文件

  • 我们之前写代码的时候实现的功能很单一,就直接创建一个test.c文件,所有代码都在这个文件里面实现就可以了.

  • 但是当我们真正要做一个项目的时候,不管项目大或小,我们都要让我们的这个项目看起来更加规范,并且我们在实现完功能后我们希望自己能运行一下代码看看我们写的对不对.

  • 所以这里就引出了一个写项目常见的手段:创建多个文件夹分别实现不同的功能


这里在源文件中,我们需要自己定义三个文件,分别是:

  • game.c,用来实现游戏功能的文件
  • game.h,用来声明函数和包含头文件的文件
  • test.c,用来测试我们写的代码正确与否的文件

在本次游戏设计中,使用到了3个文件,为什么不能放在一个文件中?三子棋的实现需要多个模块的功能相互串联,多个文件可以分别处理各自模块的功能,能更好处理各个模块之间的逻辑并且便于后期调试,也使得代码的可读性提高。

我们可以将test.c文件与其他两个源文件区别开来,方便观察:

如何将不同源文件分开


2.2 实现功能需要的函数

众所周知啊,玩三子棋的时候是在九宫格中玩的,所以这里我们需要定义一个3×3的数组来充当我们的九宫格,这里数组中可能会存放我们下的棋子,所以我们在每次开始游戏之前都应该初始化一下数组内容

我们下棋当然要能看见棋盘了,并且要看见我们下的棋和电脑下的棋,所以这里我们需要一个打印功能

下棋分玩家下棋和电脑下棋,并且有先后顺序,所以这里我们需要单独完成两个功能:电脑随机下一个位置,玩家从键盘输入想要下的位置

根据上面的一些写法,我们现在可以暂时总结出我们需要的函数有:

  • InitBoard,初始化棋盘
  • DisplayBoard,打印棋盘
  • PlayerMove,玩家下棋
  • ComputerMove,电脑下棋

这名字你可以自己修改,但是应该要包含上面这些功能


2.3 main函数代码思考

  • 我们希望玩家自行选择想不想要玩游戏,假如有些人玩了一把不过瘾还想玩一把,亦或者是有人玩了一把腻了不想玩了,都是玩家自己决定的.

  • 所以我们可以在main函数中设计一个switch语句,用于判断玩家的输入,继续玩下去可以输入1,不想玩了可以输入0退出.

  • 并且main函数中应该创建3×3的数组来代表九宫格,以便被各个函数所调用并且我们下棋想要用*和#的话,应该创建一个二维的字符型数组

有了大致的思路后,接下来我们就一边写代码一边解释和思考


3. 代码实现

3.1 main函数

void menu()//菜单,选择1或0.{printf("*****************************\n");printf("********1. play *********\n");printf("********0. exit *********\n");printf("*****************************\n");}
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;}

在main函数中,我们想要实现刚刚想到的功能,所以这里我们使用do…while语句,保证语句至少执行一次,当我们输入1时,才会进入到我们的游戏中,这里我暂且把游戏的实现代码放在void game(void)中.


3.2 game.h源文件

我们在test.c和game.c文件中都使用了printf和scanf等库函数,所以我们将常见的头文件包含在game.h中,再在game.c和test.c中包含game.h的文件即可

#include#include//电脑下棋生成随机数需要引用的头文件#include#define row 3//将数组的行和列用define宏定义#define col 3//初始化棋盘void InitBoard(char board[row][col], int r, int c);//打印棋盘void DisplayBoard(char board[row][col], int r, int c);//玩家下棋void PlayerMove(char board[row][col], int r, int c);//电脑下棋void ComputerMove(char board[row][col], int r, int c);

引用了常见的头文件后,我们还需要在game.h文件中声明我们之前提到过的函数,这里的r和c代表二维数组的行数也列数


3.3 初始化函数

我们想要在每次开始游戏之前将棋盘上所有数据清除,就是将棋盘初始化,将数组所有元素初始化为空格:

void InitBoard(char board[row][col], int r, int c){for (int i = 0; i < r; i++){for (int j = 0; j < c; j++){board[i][j] = ' ';}}}

3.4 打印棋盘

如果我们直接将二维数组中所有元素都打印出来,它看起来并不是很美观,所以这里我们加上一些分割线和换行,使我们的九宫格更加立体.如下

void DisplayBoard(char board[row][col], int r, int c){int i = 0;for (i = 0; i < row; i++){//1. 打印数据int j = 0;for (j = 0; j < col; j++){printf(" %c ", board[i][j]);if (j < col - 1)printf("|");}printf("\n");//2. 打印分割线if (i < row - 1){int j = 0;for (j = 0; j < col; j++){printf("---");if (j < col - 1)printf("|");}printf("\n");}}}

当然 ! 如果你是自己玩,并且不是颜控,直接把数组打印出来也是欸有任何问题的


3.5 玩家下棋

玩家下棋时需要注意,玩家有可能输入的坐标位置已经有棋子了,或者输入的坐标位置在九宫格之外,所以这里当玩家键盘输入后,我们应该判断下棋的位置是否正确,如果输入的位置不正确,我们将一直重复这个过程直到输入正确的位置,所以这里我们给一个while(1)语句,恒为真,当输入坐标合法时才跳出while语句

void PlayerMove(char board[row][col], int r, int c){while (1){printf("请选择下棋的坐标:");int x, y;scanf("%d %d", &x, &y);if (x > row || y > col){printf("坐标选择越界\n");}else if (board[x - 1][y - 1] == ' '){board[x - 1][y - 1] = '*';//将玩家输入的正确坐标赋值'*'后,break跳出while循环break;//如果这里不跳出循环,玩家会一直下棋}else{printf("此位置已有棋子\n");}}}

并且我们知道,数组的下标是从0开始的,但是不是每一个玩三子棋的人都是程序员,所以我们应该将玩家输入的横纵坐标减一得到它在数组中的位置,比如:玩家输入 2 3,就是下在第二行第三列,但是数组的第二行第三列是board [ 1 ] [ 2 ];


3.6 电脑下棋

这里的电脑下棋是生成随机位置下棋,是不具有智能化手段的,所以这里我们首先要生成两个随机数来作为下子的横纵坐标,这里我们用到库函数rand(),rand是生成随机数的函数,它生成的数可大可小,所以这里生成的随机数有可能超出我们棋盘的范围,这里我们的棋盘是九宫格,3×3的棋盘,所以我们希望它生成的坐标在0~2之间来满足要求所以我们这样写:

int x=rand()%3;//横坐标int y=rand()%3;//纵坐标

因为一个数对三取余一定比三要小,它只能为0 1 2,这正和我意.所以我们常常在rand()函数后面加一个取余操作来确定我们生成的随机数在一个范围内

void ComputerMove(char board[row][col], int r, int c){while (1){int x, y;printf("电脑下棋:\n");x = rand() % row;y = rand() % col;if (board[x - 1][y - 1] == ' ')//判断生成的随机位置是是不是没有下过数据{board[x - 1][y - 1] = '#';break;}}}

这里和玩家下棋一样,生成了3以内的两个数字后,要判断这个位置是否已经下过棋子了,只有生成了正确的位置,才会跳出while循环


3.7 game函数的设计

我们的思路是.先初始化棋盘,然后再将棋盘打印出来后,让玩家开始下棋,玩家下了一个子后立马又打印棋盘,并且判断输赢,如果没赢,那么电脑再继续下棋,下子后马上又打印棋盘,并且判断电脑是否赢了,.所以这个地方我们还需要设计一个判断输赢的函数,并且还有一个问题就是当棋盘下满棋子了还没有人赢,那么这就是平局,所以我们还需要写一个函数来判断棋盘是否已满

但是不管怎么样我们先把游戏的大致过程给写出来:

void game(){char board[row][col] = { 0 };//定义一个二维数组//初始化棋盘InitBoard(board, row, col);//打印棋盘DisplayBoard(board, row, col);//下棋char ret = 0;//ret用来判断对局是否结束while (1){//玩家下棋PlayerMove(board, row, col);DisplayBoard(board, row, col);//下子后立马打印棋盘//判断输赢ret = IsWin(board, row, col);//判断输赢函数返回的是C,证明没有人赢,游戏继续if (ret != 'C')break;//电脑下棋ComputerMove(board, row, col);DisplayBoard(board, row, col);//判断输赢ret = IsWin(board, row, col);if (ret != 'C')break;}if (ret == '*')//如果判断输赢的函数返回的是'*'玩家赢printf("玩家赢\n");else if (ret == '#')//返回'#'电脑赢printf("电脑赢\n");elseprintf("平局\n");}

3.8 判断棋盘是否满格

int IsFull(char board[row][col], int r, int c){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;//棋盘满了}

只有棋盘中有空格就证明棋盘没有被下满,如果九宫格遍历完都没有空格,证明棋盘满了.这里的返回值你可以自行设定,你甚至可以写成布尔值true或false.


3.9 判断输赢

我们首先要知道什么情况下算赢了:


一共有八种情况,一个正方形的四条边加中间一个”米”字,都是赢.所以我们这样来设计代码:

char IsWin(char board[row][col], int r, int c){//赢//行int i = 0;for (i = 0; i < row; i++){if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][0] != ' ')//如果一行中三个元素都相同 //且不等于空格就赢了{return board[i][0];//我们返回这三个元素中任意一个元素,如果玩家赢那么game函数就接受到'*',如果电脑赢就接受到'#'}}//列for (i = 0; i < col; i++){if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[0][i] != ' ')//列的判断和行一样{return board[0][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];//平局if (IsFull(board, row, col) == 1)//如果IsFull返回1,证明棋盘满了,平局{return 'Q';}//继续return 'C';//返回C代表没有人赢并且棋盘没有满,就继续下子}

这里我们将判断输赢函数的返回值设计为 ’ * ’ ,‘#’ ,‘Q’ ,和’C’,其实还是有目的的,game函数的ret如果接受到判断输赢函数的返回值如果不是’C’了,证明有输赢产生,但是我并不知道是玩家赢还是电脑赢,这里玩家下子是’ * ‘,电脑下子是’ # ‘,所以最后我们只需要判断返回值是什么就可以判断谁赢了.


接下来我们运行看看:

三子棋下棋

4. 所有代码

  • test.c
#define _CRT_SECURE_NO_WARNINGS#pragma warning(disable:6031)#include"game.h"void menu(){printf("*****************************\n");printf("********1. play *********\n");printf("********0. exit *********\n");printf("*****************************\n");}void game(){char board[row][col] = { 0 };InitBoard(board, row, col);//打印棋盘DisplayBoard(board, row, col);//下棋char ret = 0;while (1){//玩家下棋PlayerMove(board, row, col);DisplayBoard(board, row, col);//判断输赢ret = IsWin(board, row, col);if (ret != 'C')break;//电脑下棋ComputerMove(board, row, col);DisplayBoard(board, row, col);//判断输赢ret = IsWin(board, row, col);if (ret != 'C')break;}if (ret == '*')printf("玩家赢\n");else if (ret == '#')printf("电脑赢\n");elseprintf("平局\n");}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;}

  • game.c
#define _CRT_SECURE_NO_WARNINGS#pragma warning(disable:6031)#include"game.h"void InitBoard(char board[row][col], int r, int c){for (int i = 0; i < r; i++){for (int j = 0; j < c; j++){board[i][j] = ' ';}}}void DisplayBoard(char board[row][col], int r, int c){int i = 0;for (i = 0; i < row; i++){//1. 打印数据int j = 0;for (j = 0; j < col; j++){printf(" %c ", board[i][j]);if (j < col - 1)printf("|");}printf("\n");//2. 打印分割线if (i < row - 1){//printf("---|---|---\n");int j = 0;for (j = 0; j < col; j++){printf("---");if (j < col - 1)printf("|");}printf("\n");}}}void PlayerMove(char board[row][col], int r, int c){while (1){printf("请选择下棋的坐标:");int x, y;scanf("%d %d", &x, &y);if (x > row || y > col){printf("坐标选择越界\n");}else if (board[x - 1][y - 1] == ' '){board[x - 1][y - 1] = '*';break;}else{printf("此位置已有棋子\n");}}}void ComputerMove(char board[row][col], int r, int c){while (1){int x, y;printf("电脑下棋:\n");x = rand() % row;y = rand() % col;if (board[x][y] == ' '){board[x][y] = '#';break;}}}int IsFull(char board[row][col], int r, int c){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 IsWin(char board[row][col], int r, int c){//赢//行int i = 0;for (i = 0; i < row; i++){if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][0] != ' '){return board[i][0];}}//列for (i = 0; i < col; i++){if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[0][i] != ' '){return board[0][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];//平局if (IsFull(board, row, col) == 1){return 'Q';}//继续return 'C';}

  • game.h
#pragma once#include#include#include#define row 3#define col 3//初始化棋盘void InitBoard(char board[row][col], int r, int c);//打印棋盘void DisplayBoard(char board[row][col], int r, int c);//玩家下棋void PlayerMove(char board[row][col], int r, int c);//电脑下棋void ComputerMove(char board[row][col], int r, int c);char IsWin(char board[row][col], int r, int c);int IsFull(char board[row][col], int r, int c);