目录
第一步编写界面
1.构思
2.功能的实现
第二步编写游戏内容
1.游戏的框架
2.功能的实现
第三步编写电脑玩家
1.分类
2.输出顺序
总结
第一步编写界面
1.构思
我们平时玩游戏并非打开它就直接开始游戏,打开游戏后最先印入眼帘的往往是游戏菜单,而菜单中必不可少的则是游戏的 ‘开始’ 与 ‘退出’ ,而开始与退出的简易界面我们设一个函数来处理
//菜单 void menu() { printf("***********************************\n"); printf("*********** 1.Play ************\n"); printf("*********** 0.Exit ************\n"); printf("***********************************\n"); }这样我们就拥有了一个简易的菜单了!
2.功能的实现
有了菜单,接下来我们就要来实现菜单的功能 Play 和 Exit ,我在这里选择的是switch语句,switch(整型)恰好对应我们的1.Play和0.Exit,在这里我们需要思考,当玩家输入的不是1或0时,系统无法给出反馈,这时我们就要再给出一种情况default,使得在系统接收到一个其他内容时可以做出反馈。当然,如果玩家想要多次游戏,利用do while()语句就能够轻松解决。
小结:利用switch进行判断玩家的输入1 - play,0 - exit,default - 输入有误。
int input = 0; do { menu(); printf("请选择:>"); scanf("%d", &input); switch(input) { case 1: { printf("游戏开始!\n"); Game(); //游戏的实现 break; } case 0: { printf("退出游戏!\n"); break; } default: { printf("输入错误,请重新输入!\n\n"); } } } while (input);第二步编写游戏内容
1.游戏的框架
我们进入游戏之后,必然不能是空空荡荡的,下棋必然要有棋盘,有了棋盘再去落子,博弈必然也少不了对手,所以我们从这三个点构建框架。
首先我们要再创建两个文件,第一个是头文件(用于函数的声明),第二个是源文件(用于函数的实现)。在头文件中我们先定义行和列,方便后面的修改。
//头文件名为“五子棋声明” #include <stdio.h> #define ROM 3 //行 #define COL 3 //列#include "五子棋声明.h"//在源文件中对头文件进行声明1.打印棋盘
2.进行对局
3.分出胜负
由此,我们设一个Game函数
//放于第一个源文件中,主函数也在第一个源文件,源文件可以自行取名 void Game() { //储存数据 char Board[ROM][COL] = {0}; //打印棋盘 DisplayBoard(Board, ROM, COL); //对局 (包含玩家下棋与电脑下棋) Match(Board, ROM, COL); }2.功能的实现
一.打印棋盘
for (int i = 0; i < ROM; i++) { printf(" | | \n"); //里面为三个空格 if(i < ROM) { printf("---|---|---\n"); } }写到此处可能就会有疑问了,这样写不就把代码写死了吗?以后改成五子棋的多行和多列不就很复杂了吗?
是的,这样写确实弊端很大,正如我在大标题下写的,代码是改出来的,在改代码之前一定要把这个构思实现出来,修改时才会有清晰的思路,否则,让一个初学者写上上千行的代码想必很多人刚开始就懵了。通过不断的修改最基本的代码一步一步的扩展,逐渐你就会发现你慢慢的实现了一个自己感觉都不可能完成的事情。就像每天树立一个小目标实现起来并不困难,只要坚持便会收获一份大的成果。
接下来让我们对这个代码进行修改
打印完棋盘后我们发现,没有办法在棋盘上进行下棋,这时就要用到我们的二维数组了,通过二维数组在棋盘上进行坐标的输出。但我们的二维数组里面是空的,所以我们首先要做的是对数组进行初始化,将每个元素都初始化为空格。这时我们将再数组加进去,就达到了我们想要的效果。
//全部在头文件中进行声明 //初始化棋盘 - 初始化空格 void InitBoard(char Board[ROM][COL], int rom, int col); //打印棋盘 void DisplayBoard(char Board[ROM][COL], int rom, int col);//函数内容放在第二个源文件中 void InitBoard(char Board[ROM][COL], int rom, int col) { for (int i = 0; i < rom; i++) { for (int j = 0; j < col; j++) { Board[i][j] = ' '; } } }//函数内容放在第二个源文件中 void DisplayBoard(char Board[ROM][COL]) { for (int i = 0; i < ROM; i++) { printf(" %c | %c | %c \n", Board[i][0], Board[i][1], Board[i][2]); if(i < ROM) { printf("---|---|---\n"); } } }接下来对打印行与列的方式进行修改
通过对输出内容的拆分,寻找其中的规律,利用循环进行输出。这里将‘空格’ 与‘|’进行拆分,‘---’与‘|’进行拆分 ,‘|’每行的输出都少一个,从而建立循环,用if()语句进行判断,使得‘|’的输出比‘ ’和‘---’少一次。同理‘---|---|---’也是如此,用if()语句进行判断,使得他的输出比‘ | | ’少一次。这样我们通过修改ROM和COL的值就可以打印出19路五子棋了。
//函数内容放在第二个源文件中 void DisplayBoard(char Board[ROM][COL]) { //循环输出,通过更改ROM和COL可以输出自己需要的行数 for (int i = 0; i < ROM; i++) { //输出第一行 for (int j = 0; j < COL; j++) { printf(" %c ", Board[i][j]); if (j < COL - 1) //最后一个不要 { printf("|"); } } printf("\n"); //printf(" | | \n"); //输出第二行 if (i < ROM - 1) //最后一次输出不要 { for (int j = 0; j < COL; j++) { printf("---"); if (j < ROM - 1) //最后一个不要 { printf("|"); } } printf("\n"); } //printf("---|---|---\n"); } }发现问题解决问题
费劲千辛万苦终于把棋盘打印出来了,但打印出来之后你会发现,你在下棋的时候是需要通过输入坐标的输入来下棋的,而棋盘空空荡荡,下起棋来十分不方便,这时你就要再次对自己的代码进行修改。
下面的增加坐标的方式不唯一,我这肯定不是最好的方法,没有办法做到一劳永逸,此处仅供开阔思路吧!
//函数内容放在第二个源文件中 void DisplayBoard(char Board[ROM][COL], int rom, int col)// { //此处为横向坐标的添加 printf("坐标 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19\n"); for (int i = 0; i < rom; i++) { //此处为纵向坐标的添加 /*-------------------------------*/ if (i < 9) { printf(" %d ", i + 1); } else if (i >= 9) { printf(" %d", i + 1); } /*-------------------------------*/ for (int j = 0; j < col; j++) { printf(" %c ",Board[i][j]); if (j < col - 1) { printf("|"); } } printf("\n "); //此处做了改动 if (i < rom - 1) { for (int j = 0; j < col; j++) { printf("---"); if (j < col - 1) { printf("|"); } } printf("\n"); } } printf("\n"); }二.进行对局
1.玩家下棋
//函数内容放在第二个源文件中 void PlayerMove(char Board[ROM][COL]) { printf("玩家的坐标:>"); scanf("%d%d", &x, &y); printf("\n"); Board[x - 1][y - 1] = '*'; }2.电脑下棋
这里我们先制作一个会落子的电脑玩家
//在头文件中引入函数,用于产生随机数,在此先不细讲了 #include <stdlib.h> #include <time.h>srand((int)time(NULL));//放于main函数里的开头位置,用于产生随机数void ComputerMoves(char Board[ROM][COL]) { x = rand()%ROM;//产生纵向随机数0 ~ (ROM - 1) y = rand()%COL;//产生横向随机数0 ~ (COL - 1) Board[x][y] = '#'; printf("电脑的坐标:>%d %d\n\n", x, y); }发现问题
运行后你会发现什么都没有打印,这时你就要再次数将原先的打印棋盘的函数家在此处。
你再次运行程序后会发现,你的棋子可以覆盖掉电脑的棋子,反之电脑也可以覆盖你的棋子,这时我们就要针对这个问题再进行完善。
解决问题
在这里我们需要再加一个判断条件和一个循环语句,当判断此处已经落子后,可以让我们从新输入。而电脑玩家此处只需要加上一个判断就可以了。完善后的代码如下
//玩家下棋 void PlayerMove(char Board[ROM][COL]) { int x, y; printf("玩家的回合!\n"); while (1) { printf("玩家的坐标:>"); scanf("%d%d", &x, &y); printf("\n"); if (Board[y - 1][x - 1] == ' ') // 判断此处是否已经落子 { Board[y - 1][x - 1] = '*'; break; } else { printf("该坐标已落子!\n"); printf("请重新输入!\n"); } } DisplayBoard(Board, ROM, COL); // 输入成功后,打印棋盘 } //电脑玩家下棋 void ComputerMove(char Board[ROM][COL]) { int x, y; while (1) { x = rand()%ROM; y = rand()%COL; if (Board[x][y] == ' ') { Board[x][y] = '#'; printf("电脑的回合!\n"); printf("电脑的坐标:>%d %d\n\n", y, x); // 这里输出时不要搞反 break; } } DisplayBoard(Board, ROM, COL); }3.对局
我们将前面的内容整合一下就可以得到下面的代码
void Match(char Board[ROM][COL], int rom, int col) { while (1) { //玩家下棋 PlayerMove(Board); count++; if (count == rom*col) // 判断棋盘是否已经占满,棋盘格子的数目为(ROM * COL)为单数所以放在中间 { printf("平局!"); break; } //电脑下棋 ComputerMove(Board); count++; } }三.分出胜负
五子棋判断输赢无非就是某一方的棋在一条直线上连出5个棋子,由此我们可以分成以下四种情况
1.纵向连成5个棋子
2.横向连成5个棋子
3.斜率 == 1时,连成五个棋子
4.斜率 == -1时,连成五个棋子
1.纵向判断
在这里我使用的是纵向遍历的方法,最开始纵向的五个值,沿着纵向进行遍历。
void Win1(char Board[ROM][COL], int rom, int col, char *Ret) { int x = 0; int y = 0; for (y = 0; y <= col; y++ && *Ret != 'W') { for (x = 0; x <= rom - 5; x++) //(rom - 5)是指后面的四个数不需要在遍历,Board[x + 4]已经将其覆盖了 { if (Board[x][y] == Board[x + 1][y] && Board[x + 1][y] == Board[x + 2][y] && Board[x + 2][y] == Board[x + 3][y] && Board[x + 3][y] == Board[x + 4][y] && Board[x][y] != ' ') // 这里的方法很笨,直接判断5个值知否相等 { *Ret = 'W'; //这里的指针用于直接改变结果,不需要再返回值 break; } } } }2.横向判断
与纵向判断一样,最开始横向的五个值,沿着横向进行遍历。
void Win2(char Board[ROM][COL], int rom, int col, char *Ret) { int x = 0; int y = 0; for (x = 0; x <= rom; x++ && *Ret != 'W') { for (y = 0; y <= col - 5; y++) { if (Board[x][y] == Board[x][y + 1] && Board[x][y + 1] == Board[x][y + 2] && Board[x][y + 2] == Board[x][y + 3] && Board[x][y + 3] == Board[x][y + 4] && Board[x][y] != ‘ ’) { *Ret = 'W'; break; } } } }3.斜率 == 1方向
判断方法与前面一样,只不过变成了倾斜的方向,起始点为左上角(1,1)
从红线开始遍历,沿着斜率 == -1的白线到方向,一条路向左下遍历到5个红点的位置,另一条路向右上遍历到仅容纳5个点的右上角。每向 (白线)- 1方向(上/下)挪动时,都会少遍历一格,直至仅有五格的时候停止遍历,因此在循环的时候ROM和COL均减去5。
void Win3(char Board[ROM][COL], int rom, int col, char *Ret) { int x = 0; int y = 0; int xs = 0; int ys = 0; for (xs = 0; xs <= rom - 5 && *Ret != 'W'; xs++)//沿-1方向向下遍历 { x = xs; for (y = 0; y <= col - 5 && x <= rom - 5; y++, x++)// { if (Board[x][y] == Board[x + 1][y + 1] && Board[x + 1][y + 1] == Board[x + 2][y + 2] && Board[x + 2][y + 2] == Board[x + 3][y + 3] && Board[x + 3][y + 3] == Board[x + 4][y + 4] && Board[x][y] != ' ') { *Ret = 'W'; break; } } } for (ys = 0; ys <= col - 5 && *Ret != 'W'; ys++)//沿-1方向向上遍历 { y = ys; for (x = 0; y <= col - 5 && x <= rom - 5; y++, x++) { if (Board[x][y] == Board[x + 1][y + 1] && Board[x + 1][y + 1] == Board[x + 2][y + 2] && Board[x + 2][y + 2] == Board[x + 3][y + 3] && Board[x + 3][y + 3] == Board[x + 4][y + 4] && Board[x][y] != ' ') { *Ret = 'W'; break; } } } }4.斜率 == -1方向
方法与斜率 == 1相同,不同点在于改变了起始点为(COL - 1)
这次是从白线开始遍历,沿着斜率 == 1红线的方向分为两条路,第一条为左上,第二条为右下,遍历到仅容纳5个点的位置停止遍历。
//赢法四(判断斜率 == -1时) void Win4(char Board[ROM][COL], int rom, int col, char *Ret) { int x = 0; int y = 0; int xs = 0; int ys = col - 1; for (xs = 0; xs <= rom - 5 && *Ret != 'W'; xs++) { x = xs; for (y = col - 1; y >= 4 && x <= rom - 5; y--, x++) { if (Board[x][y] == Board[x + 1][y - 1] && Board[x + 1][y - 1] == Board[x + 2][y - 2] && Board[x + 2][y - 2] == Board[x + 3][y - 3] && Board[x + 3][y - 3] == Board[x + 4][y - 4] && (Board[x][y] == '#' || Board[x][y] == '*')) { *Ret = 'W'; break; } } } for (ys = col - 1;ys >= 4 && *Ret != 'W'; ys--) { y = ys; for (x = 0; y >= 4 && x <= rom - 5; y--, x++) { if (Board[x][y] == Board[x + 1][y - 1] && Board[x + 1][y - 1] == Board[x + 2][y - 2] && Board[x + 2][y - 2] == Board[x + 3][y - 3] && Board[x + 3][y - 3] == Board[x + 4][y - 4] && (Board[x][y] == '#' || Board[x][y] == '*')) { *Ret = 'W'; break; } } } }根据以上四种判断方法,我们就可以判断谁胜谁负了,但在此之前还是要把他们整理到一起
//判断赢得对局 char WinMatch(char Board[ROM][COL]) { char Ret = 'F'; //Ret = 'W';判断出了结果的同时,也不会再跑完其他的函数 Win1(Board, ROM, COL, &Ret); Win2(Board, ROM, COL, &Ret); Win3(Board, ROM, COL, &Ret); Win4(Board, ROM, COL, &Ret); return Ret; }优化和完善一下我们的对局
//对局 void Match(char Board[ROM][COL], int rom, int col) { int OrWin = 'F'; // 接收返回的Ret结果 int count = 0; while (1) { //玩家下棋 printf("第 %d 回合!\n",count + 1); PlayerMove(Board); OrWin = WinMatch(Board);//判断对局是否赢了 count++; //记录回合数 if (OrWin == 'W') { printf("恭喜你赢了!\n"); printf("玩家胜利!\n\n"); break; } //判断回合是否已满 else if (count == rom * col) { printf("平局!\n\n"); break; } //电脑下棋 printf("第 %d 回合!\n",count + 1); ComputerMoves(Board); OrWin = WinMatch(Board);//判断对局是否赢了 count++; //记录回合数 if (OrWin == 'W') { printf("很遗憾你输了!\n"); printf("电脑胜利!\n"); break; } } }头文件整理
#include <stdio.h> #include <stdlib.h> #include <time.h> #define ROM 19 #define COL 19 //菜单 void menu(); //五子棋主函数 void Game(); //主函数(内部) //初始化棋盘 - 初始化空格 void InitBoard(char Board[ROM][COL], int rom, int col); //打印棋盘 void DisplayBoard(char Board[ROM][COL], int rom, int col); //玩家下棋 void PlayerMove(char Board[ROM][COL]); //电脑(简单)算法下棋 void ComputerMoves(char Board[ROM][COL]); //对局 void Match(char Board[ROM][COL], int rom, int col); //判断赢得对局 char WinMatch(char Board[ROM][COL]); //判断赢得对局(内部) //赢法一(判断列) void Win1(char Board[ROM][COL], int rom, int col, char *Ret); //赢法二(判断行) void Win2(char Board[ROM][COL], int rom, int col, char *Ret); //赢法三(判断斜率 == 1时) void Win3(char Board[ROM][COL], int rom, int col, char *Ret); //赢法四(判断斜率 == -1时) void Win4(char Board[ROM][COL], int rom, int col, char *Ret);Game函数
void Game() { //储存数据 - 二维数组 char Board[ROM][COL] = {0}; //初始化棋盘 - 初始化空格 InitBoard(Board, ROM, COL); //打印棋盘 DisplayBoard(Board, ROM, COL); //对局 Match(Board, ROM, COL); }除Game函数以外,我将其他的函数都放到了源文件(2),并将其命名为Game
这样我们就成功完成了我们的五子棋小游戏!
第三步编写电脑玩家
由于我水平确实很有限,就电脑玩家写了就有1000行,真就是技术不够代码来凑啊...所以在这里我只给大家分享一下思路也欢迎各位的指点交流!
1.分类
//电脑算法头文件 #include <stdio.h> #include <stdlib.h> #include <time.h> #define ROM 19 #define COL 19 //电脑(简单)算法下棋(内部) void ComputerMain (char Board[ROM][COL], int *x, int *y, int *stop); //算法(初始点设定) void ComputerC1(char Board[ROM][COL], int *px, int *py, int *stop); //算法(随机点产生) void ComputerC2(char Board[ROM][COL], int *px, int *py, int *stop); //连珠算法常规 //算法一(纵向) void ComputerM1(char Board[ROM][COL],int *px, int *py, int *stop, int N); //算法二(横向) void ComputerM2(char Board[ROM][COL], int *px, int *py, int *stop, int N); //算法三(斜率 == 1方向) void ComputerM3(char Board[ROM][COL], int *px, int *py, int *stop, int N); //算法四(斜率 == -1方向) void ComputerM4(char Board[ROM][COL], int *px, int *py, int *stop, int N); //算法一选择 void ComputerM1_1(char Board[ROM][COL],int x, int y, int *px, int *py, int *stop, int N); //算法二选择 void ComputerM2_1(char Board[ROM][COL],int x, int y, int *px, int *py, int *stop, int N); //算法三选择 void ComputerM3_1(char Board[ROM][COL],int x, int y, int *px, int *py, int *stop, int N); //算法四选择 void ComputerM4_1(char Board[ROM][COL],int x, int y, int *px, int *py, int *stop, int N); //一字缺口算法 //缺口算法一(纵向) void ComputerG1(char Board[ROM][COL], int *px, int *py, int *stop); //缺口算法二(横向) void ComputerG2(char Board[ROM][COL], int *px, int *py, int *stop); //缺口算法三(斜率 == 1方向) void ComputerG3(char Board[ROM][COL], int *px, int *py, int *stop); //缺口算法四(斜率 == -1方向) void ComputerG4(char Board[ROM][COL], int *px, int *py, int *stop); //缺口算法五(纵向) void ComputerG5(char Board[ROM][COL], int *px, int *py, int *stop); //缺口算法六(横向) void ComputerG6(char Board[ROM][COL], int *px, int *py, int *stop); //缺口算法七(斜率 == 1方向) void ComputerG7(char Board[ROM][COL], int *px, int *py, int *stop); //缺口算法八(斜率 == -1方向) void ComputerG8(char Board[ROM][COL], int *px, int *py, int *stop); //回字缺口算法 //缺口算法九(空心三乘三) void ComputerG9(char Board[ROM][COL], int rom, int col, int *px, int *py, int *stop); //缺口算法十(空心七乘七) void ComputerG10(char Board[ROM][COL], int rom, int col, int *px, int *py, int *stop); //缺口算法十一(空心九乘九) void ComputerG11(char Board[ROM][COL], int rom, int col, int *px, int *py, int *stop); //缺口算法(回字三乘三)四向 void ComputerG9_1(char Board[ROM][COL], int x, int y, int *ret1, int *ret2); //缺口算法(回字七乘七)纵向 void ComputerG10_1(char Board[ROM][COL], int x, int y, int *ret1, int *ret2); //缺口算法(回字七乘七)横向 void ComputerG10_2(char Board[ROM][COL], int x, int y, int *ret1, int *ret2); //缺口算法(回字七乘七)斜率 == 1方向 void ComputerG10_3(char Board[ROM][COL], int x, int y, int *ret1, int *ret2); //缺口算法(回字七乘七)斜率 == -1方向 void ComputerG10_4(char Board[ROM][COL], int x, int y, int *ret1, int *ret2); //缺口算法(回字九乘九)纵向 void ComputerG11_1(char Board[ROM][COL], int x, int y, int *ret1, int *ret2); //缺口算法(回字九乘九)横向 void ComputerG11_2(char Board[ROM][COL], int x, int y, int *ret1, int *ret2); //缺口算法(回字九乘九)斜率 == 1方向 void ComputerG11_3(char Board[ROM][COL], int x, int y, int *ret1, int *ret2); //缺口算法(回字九乘九)斜率 == -1方向 void ComputerG11_4(char Board[ROM][COL], int x, int y, int *ret1, int *ret2); //缺口算法(选择输出) void Cret(char Board[ROM][COL], int x, int y, int *ret1, int *ret2);2.输出顺序
下面的注释是对一个标题的整体解释,四个为一组的标题依然是按照 纵向、横向、k == 1、k == -1 来编写的。我方永远在敌方前面判断,如果我方不符合,在判断敌方是否构成威胁。
void ComputerMain (char Board[ROM][COL],int *x, int *y, int *stop) { //五子缺一子 ComputerG8(Board, x, y, stop); //五子缺一子,说白了就是连成了4个棋子 ComputerG7(Board, x, y, stop); //我方:下到这一步直接胜利 ComputerG6(Board, x, y, stop); //敌方:必须封住着一颗棋子否则输 ComputerG5(Board, x, y, stop); //缺少的是中间三颗棋子的其中一颗 //四子连珠(一空/两空) ComputerM4(Board, x, y, stop,2); //四颗棋子连在一起,两边有空,或者一边有空 ComputerM3(Board, x, y, stop,2); //我方:下到这一步直接胜利 ComputerM2(Board, x, y, stop,2); //敌方:必须封住着一颗棋子否则输,也有可能是必输 ComputerM1(Board, x, y, stop,2); //四子缺一子 ComputerG4(Board, x, y, stop); //判断四颗棋子中间两个子棋缺少任意一个 ComputerG3(Board, x, y, stop); ComputerG2(Board, x, y, stop); ComputerG1(Board, x, y, stop); //回字缺口(九乘九) ComputerG11(Board, ROM, COL, x, y, stop); //用于判断九成九的格子内的交叉棋,并封堵 //建议11*11更全面,我这里有一种情况会误判,正在改ing //三子连珠(一空/两空)(敌方两空) ComputerM4(Board, x, y, stop,1); //三个棋子连在一起,两边都有空,或一边有空 ComputerM3(Board, x, y, stop,1); //我方:直接下,一空拖一手,两空有机会赢 ComputerM2(Board, x, y, stop,1); //敌方:一空就不用管了,两空肯定要封 ComputerM1(Board, x, y, stop,1); //回字缺口(七乘七) ComputerG10(Board, ROM, COL, x, y, stop); //这里要注意,这是两子和三子的交叉棋 //一个方向两子加三子的情况之和就有5种 //交叉方式更多,所以要格外注意,目前我这里没发现漏洞 //回字缺口(三乘三) ComputerG9(Board, ROM, COL, x, y, stop); //这里就是十字棋的情况了,优先级比较低,但是也够用 //电脑二子连珠(两空) ComputerM4(Board, x, y, stop,0); //这里就是我方的下棋方法了,我偏向于斜着下 ComputerM3(Board, x, y, stop,0); //这里面是很值得研究的,但是我下棋本就没啥章法 ComputerM2(Board, x, y, stop,0); //所以这块我也没有什么路数,等Bug修完,打算再研究 ComputerM1(Board, x, y, stop,0); //如果是敌方:我直接就没管他 //电脑的初始点 ComputerC1(Board, x, y, stop); //初始点我设置了几个我自认为毕竟好的开局点位,我防止电脑开局乱下 //电脑的随机点 ComputerC2(Board, x, y, stop); //我设置了两个随机点 //第一个,我以中心为范围的7*7随机点,注意不要死循环 //第二个,我设的全局随机点,防止7*7范围下满 }我的思路可能不全,打算等在学一段时间之后,又能好的方法可以将我原先的代码更替掉,然后研究一下大佬们的思路。也慢慢的去学习如何做到高内聚低耦合,现在感觉还是多少又一些困难再接再厉吧!