前言
🎈大家好,我是何小侠🎈
🌀大家可以叫我**小何或者小侠🌀**
🔴我是一名普通的博客写作者🔴
💐希望能通过写博客加深自己对于学习内容的理解💐
🌸也能帮助更多人理解和学习🌸
🍃我的主页: 何小侠的主页🍃
这篇博客我们一起来学习如何用C语言所学知识来实现三子棋,另外我们还将推广到更大的棋盘。希望能帮助大家理解和学习
🍊
引子🍊
三子棋这个游戏大家都应该不陌生,大家应该在学校和小伙伴们玩过,这里就不介绍规则了,给大家介绍一下三子棋的历史(也可能是野史)。
关于《三子棋》的起源,年代之远古已经不可考究。但是严肃地讲,三子棋很可能是历史上最早使用类似“三消”原则作为棋理的棋种。古有记载,所谓三消,即以三为理,遇三则消失。
据说,明朝史上最会玩的正德皇帝–朱厚照经常会有一些出人意料的想法和举动,比如在宫中模拟开酒店、店铺和怡红院荒诞行为,甚至还经常偷跑出宫去玩。为了安抚这位不省心的皇帝,老臣李东阳曾将三子棋推荐给皇帝,奈何这个棋皇上玩了几次就没兴趣了,因为它有一个极大的BUG:熟悉规则的双方,理论上可以达到100%的平局。
于是古人在三连为赢的基础上逐渐升级难度并改进玩法,创造出更多种类的游戏。比如说五子棋,其实就是根据三子棋演变而来,甚至是如今流行的各种版本的消消乐其实也是在三连消的基础规则上演变而来的。
游戏模块介绍🍊
这是我们第一次接触模块化编程,下面我们解释一下模块化编程的好处 |
- 首先我们先介绍一下什么是模块化编程:
我们在写一个项目的时候,需要将这个项目分成用多个.c的源文件或者.h的头文件来实现,每个.c文件就相当于一个模块,比如计算器的实现,我们将加减乘除分成4个.c文件来实现,而.h文件则是用来放声明。
-模块化的实际应用:
比较传统的编程模式,即将所有文件全部集中于main函数,或者全部集中在一个.c文件。我们可以试想如果你写了一个很长的代码有1万行,当你想修改5500行的代码。在这个过程中,你会觉得很繁琐,冗杂,因为你的代码没有一个清晰的分类,把所有功能集中在一起不仅不利于你自己的观看,别人看起来也没有兴趣。而且在我们以后的工作中或者多人一起开发项目的时候,每个人只要分工明确,各种完成自己的模块就行,将你的.c和.h文件与其他人的一结合,这个项目就初步完成了,后续再经过调试等过程,就完成了整个工程。
- 模块化编程的优点
极大的提高代码的可阅读性、可维护性、可移植性并且它使得整个项目分工明确,条理清晰
游戏思路🍊
这张图片就是我们的整体思路,接下来我们会逐步实现。
main
void test()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请输入\n");
scanf("%d", &input);
switch (input)
{
case 1:
printf("三子棋\n");
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
还是是我们经典的do-while+Switch的结构srand和time函数如果看到这里不懂可以去看看我之前的关于猜数字游戏的博客 link这个链接里面是有srand rand 以及time函数的讲解的。
menu
void menu()
{
printf("*********************\n");
printf("*** 1.play *****\n");
printf("*** 0.eixt *****\n");
printf("*********************\n");
}
二维数组定义和初始化
char board[ROW][COL];
void intiboard(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++)
{
board[i][j] = ' ';
}
}
}
这里的ROW COL 是我们在头文件中用#define定义的,因为我们要玩三子棋所以我们设置为3,3。之后我们会拓展一下,例如,在5x5的棋盘里面玩三子棋。
打印棋盘
实际上我们打印棋盘有两种写法,一种是只适合3x3棋盘的,一种是可以随我们设定的ROW,COL随之改变的。我们先介绍第一种只适合3x3的
这是我们要打印的棋盘,我们仔细观察一下行,然后想一下该如何写代码。
通过观察我们知道第一行就是 这个两个竖线 | | ,第二行就是—|—|—
,然后第三行第五行都与第一行一样,第4行与第2行一样。这是我们初步的分析。但是我们知道我们要在这个棋盘上面下棋 × ○这样的符号 ,所以第一行其实实际上是space%cspace,为什么不只是一个%c呢?大家可以尝试在自己的电脑上对比一下 - 和 space,这两个实际上都占有一个字符。
然后我们尝试一下用代码写出一个棋盘。如果有问题可以与下面的代码作比较
void printboard(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");
}
}
上面就是我们没有优化的打印棋盘的代码,我们简单讲解一下,因为
%c | %c | %c 要打印三行就可以直接用for循环打印,但是—|—|—只需要打印两行就要控制一下,,i < row是三次,用 i < row -1就是两次。
我们在最开始就说过这个打印代码是不符合3x3以上棋盘的,所以我们要在词基础上加以改进。下面展示一下10x10情况下这个代码的打印情况让大家知道确实不能进行推广。
我们看到行确实没有出错但是我们更需要在列上下功夫。
下面我们再加深讨论,请看图。
我们将列进行拆分。
我们看蓝花这一行,发现space%cspace打印三次而 | 只打印两次。
我们看红花这一行,发现—打印三次 | 也只打印两次。
通过分析我们就能够就用代码实现,大家可以先动手实践一下。
下面的代码。
void printboard(char board[ROW][COL], int row, int col)
{
int j = 0;
int i = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
printf(" %c ", board[i][j]);
if (j < col - 1)
printf("|");
}
printf("\n");
for (j = 0; j < col; j++)
{
if (i < row - 1)
{
printf("---");
if (j < col - 1)
printf("|");
}
}
printf("\n");
}
}
我们还是和最初版本一样用 j < col -1来控制只出现两次的线条。用三个for循环,最外层控制有多少行,里面的两个for循环用来控制每行的列的打印。
玩家下棋
我们让玩家输入坐标,根据坐标来判断在哪个地方落子。
但是我们要注意两个细节,
第一,玩家不是程序员,不能用下标作为坐标
第二,我们要考虑到玩家输入的坐标是否合法,即是不是在坐标范围内,和是不是已经被落子了。
接下来我们给出代码。
void player(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
printf("请输入你要下的坐标");
scanf("%d %d", &x, &y);
//判断坐标合法性
if (1 <= x && x <= ROW && 1 <= y && y <= COL)
{
if (board[x - 1][y - 1] == ' ')
{
board[x - 1][y - 1] = '*';
break;
}
else
{
printf("坐标被占用请重新输入\n");
}
}
else
{
printf("输入错误请重新输入\n");
}
}
}
说明:我们既然要让玩家下棋就一定要有变量来接收,
我们就用x,y 来接收,再判断xy是不是在棋盘范围之内。
但是即使在棋盘之内也不能就直接落子,还有判断该位置是否已经被落子。落子的下标即坐标x-1,y-1。
电脑下棋
我们之前在学习猜数字游戏的时候了解过rand srand time函数的用法,这次我们同样也要用到这几个函数。
电脑下棋思路与玩家下棋差不了太多这里就直接给出代码
void computer(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
x = rand() % row;
y = rand() % col;
if (board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}
首先还是用x,y来接收,然后x = rand % row , y = rand % col ,这样做就是让生成的x,y永远也不会超出二维数组,当然我们也需要判断该区域是否被落子,如果落子就重新生成坐标,所以就是用循环来解决。
我们还要注意一点就是srand和time函数的位置
一定是要在do-while之前就调用,如果在do-while里面定义就不符合随机的概念了。
判断输赢
判断输赢是三子棋最为重要的一环,我们一定要好好理解。
我们知道游戏只有4种状态:玩家赢,电脑赢,平局和继续游戏,也就是说其实无论是电脑还是玩家下一次棋我们都需要判断是当中的哪种状态。我们需要在test.c中写出满足这个条件的代码,下面给出一个模板。
我们需要写出一个判断输赢的函数,下面是这个函数的返回值。
玩家赢 ———*
电脑赢 ———#
打平 ———e
继续 ———c
三子棋中输赢是什么样的?(3x3棋盘)
- 行
- 列
- 对角线
只要我们判断这3个方向是否有赢就行,有赢就会返回3子成线中的任意一个就行,当然没有人赢就返回c就好,如果棋盘满了就返回e。大家可以动手实践一下。
下面是参考代码。
char judge(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][0];
}
}
int j = 0;
//列
for (j = 0; j < col; j++)
{
if (board[0][j] == board[1][j] && board[1][j] == board[2][j]
&& board[0][j] != ' ')
{
return board[0][j];
}
}
//对角线
//3x3版本
if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[0][0]
!=' ')
{
return board[0][0];
}
if (board[2][0] == board[1][1] && board[1][1] == board[0][2]
&& board[1][1]!= ' ')
{
return board[1][1];
}
if (1 == is_full(board, row, col))
{
return 'e'; //满了 //平局
}
return 'c';
}
int is_full(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++)
{
if (board[i][j] == ' ')
{
return 0 ; //没有满
}
}
}
return 1; //满了
}
相信大家应该能理解这个代码,就是不要忘记在判断行列对角线时,忽略有空格的情况,另外我们为了实现判断输赢这个函数自己定义了一个判断棋盘是否已经满的函数is_full,这个函数并不需要在头文件添加声明,因为它是我们只是为了实现judge这个函数而生成的,并不属于我们思路的中的一类。
当然我们只实现这个函数是不行的,接下来我们来看看test.c中是如何来实现,人下一次判断一次,电脑下一次判断一次的。
void game()
{
//存储数据的二维数组
char board[ROW][COL];
//初始化为space
intiboard(board, ROW, COL);
//打印棋盘
printboard(board,ROW,COL);
char ret = 0;
while (1)
{
//玩家下棋
player(board, ROW, COL);
printboard(board, ROW, COL);
ret = judge(board, ROW, COL);
if (ret != 'c')
{
break;
}
//电脑下棋
computer(board, ROW, COL);
printboard(board, ROW, COL);
ret = judge(board, ROW, COL);
if (ret != 'c')
{
break;
}
}
if ('*' == ret) //单引号里面别加空格
{
printf("玩家赢了\n");
}
else if ('#' == ret)
{
printf("电脑赢了\n");
}
else
{
printf("平局\n");
}
}
我们看到我们需要在do-while循环外定义一个变量ret来接收judge的返回值。注意一个要在do_while外面定义,因为我们用if语句来判断时,是在do-while之外。
还有一个细节是我因为打代码时,因为个人习惯导致的bug我也提一下。注意在if判断的时候’'单引号里面不要加space,我因为手误调试浪费了时间,这是要注意的。
推广到nxn🍊
当然判断输赢还不仅仅只有这些,我的标题写的是推广到n x n,我们可以想一下如果推广到n x n ,我们判断输赢的代码哪里需要改变呢?我们以5x5来举例
对没错就是对角线的判断,我们不能再仅仅只判断对角线,我画个图来给大家看看。
可以看到()左斜连成3子,(/)右斜连成3子的情况变多了,这下就不是很好讨论了。
不过不要着急我们慢慢来。
我们知道三子棋只要三子连在一起就能获胜,我们就需要判断一个棋子的四面八方,也就是上下左右,和 西北到东南和 东北到西南。但是我们的方法是以一个坐标为基准,用下标不断加减的方法来判断是否三子连线,这与我们三子棋的方法是一致的。如果大家不是很懂我可以用一张图来说明。
假设我们以红色为基准,用二维数组表示假设是board[ i ] [ j ],那么紫色的就是board[i+1][j+1]和board[i+2][j+2].
同理可得,蓝色也为board[ i ][ j ],绿色的就是board[i-1][j-1]和board[i-2][j-2]。
大家明白我的意思吗?
我的意思其实就是我们在这个棋盘上,想让以 \ 的方式连成三子其实是有限制的,同理以 / 的方式连成三子也是有限制的。
总结:我们只能在有限的区域连成三子。
大家可以试试写出代码。
下面是我的代码,可以作为参考。
//5x5版本 / 与 \
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
if (i < row - 2 && j < col - 2)
{
if (board[i][j] == board[i + 1][j + 1] && board[i + 1][j + 1] == board[i + 2][j + 2] &&
board[i][j] != ' ')
return board[i][j];
}
if (j > 1 && i < row - 2)
{
if (board[i][j] == board[i - 1][j - 1] && board[i - 1][j - 1] == board[i - 2][j - 2]
&& board[i][j] != ' ')
return board[i][j];
}
}
}
如果大家不懂得话请看下面的图。
讲解:为什么是要这样画图呢?还记得我们说得我们是以下标加或者减的形式来判断三子连线吗?我们实际上能选做基准的元素只有那些空白区域,如果你不相信可以试一试,只要你在画x的区域选基准来实现左斜或者右斜都是不行的。
这也是这篇博客最有价值的地方。
源代码🍊
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
void menu()
{
printf("*********************\n");
printf("*** 1.play *****\n");
printf("*** 0.eixt *****\n");
printf("*********************\n");
}
void game()
{
//存储数据的二维数组
char board[ROW][COL];
//初始化为space
intiboard(board, ROW, COL);
//打印棋盘
printboard(board,ROW,COL);
char ret = 0;
while (1)
{
//玩家下棋
player(board, ROW, COL);
printboard(board, ROW, COL);
ret = judge(board, ROW, COL);
if (ret != 'c')
{
break;
}
//电脑下棋
computer(board, ROW, COL);
printboard(board, ROW, COL);
ret = judge(board, ROW, COL);
if (ret != 'c')
{
break;
}
}
if ('*' == ret) //单引号里面别加空格
{
printf("玩家赢了\n");
}
else if ('#' == ret)
{
printf("电脑赢了\n");
}
else
{
printf("平局\n");
}
}
//printboard(board, ROW, COL);
// 玩家赢 *
//电脑赢 #
//打平 e
//继续 c
void test()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请输入\n");
scanf("%d", &input);
switch (input)
{
case 1:
printf("三子棋\n");
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
game.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
void intiboard(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++)
{
board[i][j] = ' ';
}
}
}
//void printboard(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");
//
// }
//
//}
void printboard(char board[ROW][COL], int row, int col)
{
int j = 0;
int i = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
printf(" %c ", board[i][j]);
if (j < col - 1)
printf("|");
}
printf("\n");
for (j = 0; j < col; j++)
{
if (i < row - 1)
{
printf("---");
if (j < col - 1)
printf("|");
}
}
printf("\n");
}
}
void player(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
printf("请输入你要下的坐标");
scanf("%d %d", &x, &y);
//判断坐标合法性
if (1 <= x && x <= ROW && 1 <= y && y <= COL)
{
if (board[x - 1][y - 1] == ' ')
{
board[x - 1][y - 1] = '*';
break;
}
else
{
printf("坐标被占用请重新输入\n");
}
}
else
{
printf("输入错误请重新输入\n");
}
}
}
void computer(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
x = rand() % row;
y = rand() % col;
if (board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}
int is_full(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++)
{
if (board[i][j] == ' ')
{
return 0 ; //没有满
}
}
}
return 1; //满了
}
char judge(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][0];
}
}
int j = 0;
for (j = 0; j < col; j++)
{
if (board[0][j] == board[1][j] && board[1][j] == board[2][j]
&& board[0][j] != ' ')
{
return board[0][j];
}
}
//对角线
//3x3版本
/*if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[0][0]
!=' ')
{
return board[0][0];
}
if (board[2][0] == board[1][1] && board[1][1] == board[0][2]
&& board[1][1]!= ' ')
{
return board[1][1];
} */
//5x5版本 / 与 \
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
if (i < row - 2 && j < col - 2)
{
if (board[i][j] == board[i + 1][j + 1] && board[i + 1][j + 1] == board[i + 2][j + 2] &&
board[i][j] != ' ')
return board[i][j];
}
if (j > 1 && i < row - 2)
{
if (board[i][j] == board[i - 1][j - 1] && board[i - 1][j - 1] == board[i - 2][j - 2]
&& board[i][j] != ' ')
return board[i][j];
}
}
}
if (1 == is_full(board, row, col))
{
return 'e'; //满了 //平局
}
return 'c';
}
game.h
#pragma once
#define ROW 5
#define COL 5
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
//初始化
void intiboard (char board[ROW][COL],int row, int col);
//打印棋盘
void printboard(char board[ROW][COL], int row, int col);
//玩家下棋
void player(char board[ROW][COL], int row, int col);
//电脑下棋
void computer(char board[ROW][COL],int row, int col);
//判断输赢
char judge(char board[ROW][COL],int row, int col);
总结🍊
这篇博客我们系统的介绍了三子棋以及三子棋的推广,大家有没有感觉三子棋的推广有点熟悉呢?其实三子棋的推广就是五子棋的思路,如果有兴趣的小伙伴可以自己实现一下五子棋,虽然有些地方不一样,但是大体思路没有太大的改变。
最后如果这篇博客有帮助到你,欢迎点赞关注加收藏
如果本文有任何错误或者有疑点欢迎在评论区评论