提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
首先介绍下三子的玩法,玩家下棋,电脑下棋,直到某一方能够三点连成一条直线即可
文章比较长,因为解释了许多我在学习时,思考的问题,文章中还简单了如如果想进阶一点,玩多子棋的设计思路
先上下自己完成的效果图:
一、设计思路
二、具体步骤
1.游戏的逻辑
首先说明本项目是结合头文件.h和源文件.c(新建项的时候改下后缀即可)
(1)在test,c中,先敲出游戏逻辑的框架
这里设想的是三种选择模式:
1、进入游戏
2、退出游戏
其他输入:选择错误
这里注意,main函数是要放在最底下的,test()函数放在上面
因为编译的时候是从上往下,只有编译器先读到你定义的函数才知道它的存在,否则会报错
void test()
{
int input = 0;
do{
menu();//游戏菜单
printf("选择游戏模式:");
scanf("%d", &input);
switch (input)
{
case 1:
game();//选择1;进入游戏的实现
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误\n");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
(2)同时可以设计出游戏一开始展示的菜单,同理,下面的menu(),game()要放在test()上面。
void menu()
{
printf("***********************\n");
printf("******* 1.play ********\n");
printf("******* 0.exit ********\n");
printf("***********************\n");
}
这里完成了,可以先运行一下,看看能不能正常的选择。
(3)接着就是game()函数
1)可以先定义个3x3的数组来存储棋盘的数组,char board[3][3] = { 0 }
2)下一步肯定是要初始化这个棋盘,我们可以设计个函数InitBoard,里面需要的参数首先要有我们的数组board,以及行3,列3
也就是InitBoard(board, 3, 3);
3)接着是要展示这个棋盘,我们也可以设计个函数DisplayBoard,传过来的参数自然还是board,3,3
4)关键部分就是下棋,要分玩家下棋,和电脑下棋,同理,player_move(board, 3, 3);computer_move(board, 3, 3)
void game()
{
//存储棋盘数据
char board[3][3] = { 0 };
//初始化棋盘
InitBoard(board, 3, 3);
//展示棋盘
DisplayBoard(board, 3, 3);
//玩家下棋
player_move(board, 3, 3);
DisplayBoard(board, 3, 3);//这里是考虑到下完棋后要把棋盘展示给玩家看
//电脑下棋
computer_move(board, 3, 3);
DisplayBoard(board, 3, 3);
}
此时,我们要注意一个问题,这时我们是要玩三子棋,那如果,想玩五子棋,十字棋,如何修改?
我们现在的进度修改起来还算简单,把所有的3,都改成5即可,那如果我们进程不断往前推进,使用这个3,3的次数越来越多,修改起来就会更加麻烦,你要在所有3,3的地方修改,所以这时可以作出一点优化
在game.h这个头文件里,宏定义两个变量,就是行和列的英文大写(可以选择小写,大写只是为了和普通变量区分开)
#define ROW 3
#define COL 3
这里可能有人要疑惑,为什么不使用全局变量?
解释一下:1)比如说,我在test.c里,定义了全局变量int row=3,想在game.c里使用row,就必须要加个外部变量声明
就是extern int row=3,这样才能扩展全局变量的作用域,而对于宏定义而言,只需要在game.c里引用下头文件#include "game.h"就行,宏定义不使用extern
2)其次,宏定义定义的是#define 宏名 字符,编译的时候就会使用,并会把所有出现ROW的地方替换成3,它是不需要开辟内存的,而全局变量需要
3)我们这里只是个简单的变量,使用宏定义更方便我们修改,宏定义缺点也不少,有兴趣的朋友可以多去了解下,这里就不过多介绍了
总之我们使用宏定义后,game()就变成
#inlcude "game.h"
void game()
{
//存储棋盘数据
//char board[3][3] = { 0 };
char board[ROW][COL] = { 0 };
//初始化棋盘
InitBoard(board, ROW, COL);
//展示棋盘
DisplayBoard(board, ROW, COL);
//玩家下棋
player_move(board, ROW, COL);
DisplayBoard(board, ROW, COL);
//电脑下棋
computer_move(board, ROW, COL);
DisplayBoard(board, ROW, COL);
}
当然代码还是有问题的,没考虑如何实时判断输赢,知道游戏结束,之后再介绍.
2.游戏的实现
1)头文件的使用
上面我们用了InitBoard,DisplayBoard等等,但都没有定义,这里我们就可以先去头文件里声明,再去game.c里定义
为什么要先声明呢?因为头文件里的内容是共享的,就像你.c文件开头都要#include <stdio.h>,和#define一样,都是预处理,相当于提前告诉编译器,有stdio这个头文件库,里面有printf,scanf等函数,同样的,你自己定义的函数
return_type function(parameter_type param1, ...)
{
.....
}
如果想要在多个文件使用,那你在文件开头肯定也要声明下,告诉这个编译器,有这个函数,已经定义好了
return_type function(parameter_type param1, ...);
那这样你每个要使用的文件里都要先声明再调用,未免太麻烦了,所以在头文件里声明,在源文件里定义以及调用,就会非常高效了
那么game.h就变成了
#define ROW 3
#define COL 3
//约定俗成用大写,以此与小写的普通变量区分开来
void InitBoard(char board[ROW][COL], int row, int col);
//这里对应着一开始调用的InitBoard(board,ROW,COL),这里你也可以换成其他的比如
//void InitBoard(char arr[ROW][COL], int r, int c);
void DisplayBoard(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);
2)InitBoard(初始化棋盘)
void InitBoard(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] = ' ';
}
}
}
3)DisplayBoard(展示棋盘)
就像一开始展示的那样,我们可以给棋盘用符号分割开来,更美观一点
这里加粗下颜色,看的更清楚些,格子是由三行两个“|”,以及两行“—|---|—”,结合一起
void DisplayBoard(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)//打印一个空白字符,划一条|,最后一个|不需要,那么就限制下j
printf("|");//如果col是3,那么j<2,就是0,1,只要划两个竖杠
}
printf("\n");//换行,给下一行“ 字符 | 字符 | 字符 ”
//---|---|---就只是单纯的符号,,只是为了美观
//基础写法:有两行---|---|---,就限制两次
//if (i < row - 1)
//{
// printf("---|---|---");
// printf("\n");
//}
//更通用的写法,如果变成五子棋,那么基础写法明显不适用了,这里就要酱---和|分开,就像上面那样,把---当作一个字符
//先有一个---(字符),再划一个|
if (i < row - 1)
{
for (j = 0; j < col; j++)
{
printf("---");
if (j < col - 1)
printf("|");
}
printf("\n");//换行,给下一行---|---|---
}
}
}
这里可以在test()去注释掉两个下棋函数,看一下,能不能正常展示。
4)player_move(玩家下棋)
玩家下棋,肯定要手动输入坐标(scanf),接着要判断这个坐标是否在格子内(if)以及这个坐标上是否已经有了字符(if)
这里用while,就是要玩家输入了正确的坐标才能break
补充一点,因为数组一开始是从0开始,board[3][3],竖着是x,对应row,实际上可选的数,是0,1,2,横着是y,可选的坐标也是0,1,2
但我们平常的习惯是从一开始,所以,我们在代码里可以简单运算下,1,1坐标,其实就是这个数组的0,0坐标,3,3坐标其实就是对应棋盘的2,2
void player_move(char board[ROW][COL], int row, int col)
{
int x, y = 0;
printf("玩家下棋\n");
while (1)
{
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");
}
}
这里可以把test()里和电脑下棋的相关函数注意,看一下能不能正常下棋
5)computer_move(电脑下棋)
这里实现与上面玩家下棋类似,注意这里用了个rand函数,着重说一下
因为是电脑下棋,所以就是要采用随机数的方法
首先要在一开始引用time库以及stdlib库(这里其实可以在game,h里引用,在game.c里只要引用game.h,那么也就引用了这两个库)
#include <time.h>
#include <stdlib.h>
初始化随机发生器,放在test(),main(),或者就在下面函数定义里初始化都可以,只要在用rand之前即可。
srand((unsigned int)time(NULL));//初始化随机数发生器
//time是时钟函数,是获取时间,这边是指的是随机数的仓库,我们这里是要整形,要强制转化下数据类型
//本来应该是先定义一个time_t类型的t变量,然后time(&t)
//但也可以直接传入一个空指针,因为程序不需要经过参数而去获得的数据
想要了解这个随机播种具体的解释,可以去蓝色链接,往下翻到最后有个前辈给了很详细的说明/
void computer_move(char board[ROW][COL], int row, int col)
{
int x, y = 0;
printf("电脑下棋:\n");
while (1)
{
x = rand() % row;//%row意思是从[0,row)随机选一个数字,也就是0,1,2
y = rand() % col;
if (board[x][y] == ' ')//这里同样是要等到这个随机坐标是合法的,才能跳出去
{
board[x][y] = '#';
break;
}
}
}
6)win(判断输赢)
游戏赢的条件是三点一条线,那么肯定实时判断(设计一个函数win()),并返回一个值(定义个字符char ret),是继续,是电脑赢,还是玩家赢,还是平局(if {} else if{} else{})
注意之前电脑下棋是‘#’,玩家下棋是‘*’。可以为我们少定义两个返回值,那么我们可以设定
返回‘c’(continue),就继续
返回‘#’,那就是电脑胜利
返回‘星号’(数字8那个符号,这里输入 星号会莫名和上面的星号形成注释,又取消不了,有解决的办法还希望评论区有大神能指点一下)就是玩家胜利
返回‘d’(dogfall),就是平局
这里的逻辑就要放在test.c里的test()里,至于实时判断的函数就要放在game.c里
while (1)
{
//玩家下棋
player_move(board, ROW, COL);
DisplayBoard(board, ROW, COL);
ret = win(board, ROW, COL);
if (ret != 'c')//如果返回的不是继续,那么就跳出这个while,去判断输赢还是平局
{
break;
}
//电脑下棋
computer_move(board, ROW, COL);
DisplayBoard(board, ROW, COL);
ret = win(board, ROW, COL);
if (ret != 'c')
{
break;
}
}
if (ret == '*')
{
printf("玩家胜利\n");
}
else if (ret == '#')
{
printf("电脑胜利\n");
}
else
{
printf("平局\n");
}
接下来就是实时判断输赢的函数win(),别忘了再头文件里声明
char win(char board[ROW][COL], int row, int col);
接着来到game.c里定义,最简单的方法就算判断三种类型,行(一个for循环,三行),列(一个for循环,三列),对角线(两条,直接验证)
那么平局呢?平局的话只有一种可能,玩家电脑把9个格子都填满了,而且没满足上面的三种类型,
这里可以再额外设计个函数full(),判断格子是否满了,满了返回1,游戏平局,没满返回0,游戏继续
static int full(char board[ROW][COL], int row, int col)//这里static是因为full函数只是为了下面的win函数服务,其他函数或者源文件并不需要他,static可加可不加
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
if (board[i][j] == ' ')//如果有一个格子是空的
{
return 0;//没满,返回0
}
}
}
return 1;//没有格子是空的,满了,返回1
}
char 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][0] == board[i][2] && board[i][1] != ' ')
//i=0的时候,如果(0,0)=(0,1)且(0,0)=(0,2)且(0,1)不为空白,那么返回(0,1)的字符,可能是#,可能是*
//下面行和对角线同理
return board[i][1];
}
//判断列是否城三个
for (i = 0; i < row; 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];
//判断平局
if (full(board, row, col) == 1)
{
return 'd';
}
return 'c';
}
我这里判断行列对角线的时候是采用比较直接的方式,但也限制了,这么比只能在三子棋中有效
如果想要更通用的方法,我这里提供一种思路,其他的可以自己去思考下
int i = 0;
int j = 0;
int count = 0;//变量count是为了限制这里只是为了比较行,
//判断行是否三个
for (i = 0; i < row; i++)
{
//原来的写法
// if (board[i][0] == board[i][1] && board[i][0] == board[i][2] && board[i][1] != ' ')
// return board[i][1];
for (j = 0; j < col - 1; j++)
{
//通用写法
if (board[i][j] == board[i][j + 1] && board[i][j] != ' ')
//i=0,j=0:比较(0,0)==(0,1)且(0,0)是否为空,满足count=1
//i=0,j=1:比较(0,1)==(0,2)且(0,j)是否为空,满足count=2
//i=0不满足那就比较i=1的情况,以此类推
{
count++;
}
if (count == col - 1)//一行三个,如果一个与另外两个都相等,那么这一行就相等,说明分出了胜负,游戏结束
{
return board[i][j];
}
}
}
三、整个代码
game.h
#include <time.h>
#include <stdlib.h>
#define ROW 3
#define COL 3
void InitBoard(char board[ROW][COL], int row, int col);
void DisplayBoard(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
#include "game.h"
void InitBoard(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 DisplayBoard(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)
//{
// 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, y = 0;
printf("玩家下棋\n");
while (1)
{
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");
}
}
void computer_move(char board[ROW][COL], int row, int col)
{
srand((unsigned int)time(NULL));
int x, y = 0;
printf("电脑下棋:\n");
while (1)
{
x = rand() % row;
y = rand() % col;
if (board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}
static int 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 is_win(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
int count = 0;
//判断行是否三个
for (i = 0; i < row; i++)
{
for (j = 0; j < col - 1; j++)
{
//原来的写法
// if (board[i][0] == board[i][1] && board[i][0] == board[i][2] && board[i][1] != ' ')
// return board[i][1];
//通用写法
if (board[i][j] == board[i][j + 1] && board[i][0] != ' ')
{
count++;
}
if (count == col - 1)
{
return board[i][j];
}
}
}
//判断列是否成三个
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];
int j = 0;
int count = 0;
for (j = 0; j < row - 1; j++)
{
//原来的写法
// if (board[i][0] == board[i][1] && board[i][0] == board[i][2] && board[i][1] != ' ')
// return board[i][1];
//通用写法
int count = 0;
if (board[j][i] == board[j + 1][i] && board[0][i] != ' ')
{
count++;
}
if (count == row - 1)
{
return board[j][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 (full(board, row, col) == 1)
{
return 'd';
}
return 'c';
}
test.c
#define _CRT_SECURE_NO_WARNINGS
#include "game.h"
void menu()//输入的时候不小心按到了fn+ins,导致输入模式变成了嵌入模式,无法修改当前字符
{
printf("***********************\n");
printf("******* 1.play ********\n");
printf("******* 0.exit ********\n");
printf("***********************\n");
}
void game()
{
char ret = 0;
//存储棋盘数据
//char board[3][3] = { 0 };
char board[ROW][COL] = { 0 };
//初始化棋盘
InitBoard(board, ROW, COL);
//展示棋盘
DisplayBoard(board, ROW, COL);
while (1)
{
//玩家下棋
player_move(board, ROW, COL);
DisplayBoard(board, ROW, COL);
ret = win(board, ROW, COL);
if (ret != 'c')
{
break;
}
//电脑下棋
computer_move(board, ROW, COL);
DisplayBoard(board, ROW, COL);
ret = win(board, ROW, COL);
if (ret != 'c')
{
break;
}
}
if (ret == '*')
{
printf("玩家胜利\n");
}
else if (ret == '#')
{
printf("电脑胜利\n");
}
else
{
printf("平局\n");
}
}
//测试菜单逻辑
void test()
{
/*srand((unsigned int) time(NULL));*/
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);
}
int main()
{
test();
return 0;
}
四、总结
一整个博客敲下来,其实还是花了不少时间的,但是发现很多一开始觉得自己理解不到位的或者没有注意的地方,文章有点啰嗦,因为掺杂了不少从我这个新手角度会去疑惑的问题,自己的能力还是有限,如果文章中有什么错误或者各位有什么建议,欢迎指出,不胜感激,下篇文章我会尽快写出来,希望能与大家共勉。