目录
一、前提说明:
涉及相关知识:函数、数组、循环
游戏可以放在多个文件中:
test.c ——> 游戏的测试逻辑
game.c ——> 游戏的实现逻辑
game.h ——> 游戏实现函数的声明
game.c文件和game.h文件都被test.c调用;game.c和game.h这两个文件合起来是实现游戏的代码。在VS2019中添加这3个文件。
函数声明和函数实现的第一行代码必须一模一样,否则如少一个void会报错重新定义函数基类型。
二、代码实现:
①test.c文件中:
#define _CRT_SECURE_NO_WARNINGS 1
#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)
{
player_move(board, ROW, COL);
DisplayBoard(board, ROW, COL);
ret = is_win(board, ROW, COL);
if (ret != 'C')
{
break;
}
computer_move(board, ROW, COL);
DisplayBoard(board, ROW, COL);
ret = is_win(board, ROW, COL);
if (ret != 'C')
{
break;
}
}
if (ret == '*')
{
printf("玩家赢了\n");
}
else if (ret == '#')
{
printf("电脑赢了\n");
}
else
{
printf("平局\n");
}
}
void test()
{
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);
}
int main()
{
test();
return 0;
}
②game.h文件中:
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include <stdio.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 1
#include "game.h"
void InitBoard(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 DisplayBoard(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 < col - 1)
{
printf("|");
}
}
printf("\n");
if (i < row - 1)
{
//printf("---|---|---\n");
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)
{
printf("玩家下棋:>");
int x = 0;
int y = 0;
while (1)
{
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;
printf("电脑下棋:>\n");
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 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];
}
if (1 == is_full(board, row, col))
{
return 'Q';
}
return 'C';
}
三、代码讲解:
①test.c文件中:
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
//主函数调用test()函数,test()函数用来进行相关的测试
void menu()
{
printf("**************************\n");
printf("****** 1.play ******\n");
printf("****** 0.exit ******\n");
printf("**************************\n");
}
void game()
{
//数据存储到一个字符的二维数组(9*9)中,玩家下棋是'*',电脑下棋是'#'
//char board[3][3] = { 0 };//棋盘名字(字符二维数组名)是board,先对其初始化为0
//刚开始数组的内容应该全部是空格(棋盘上还没有下棋):用一个初始化函数InitBoard()来初始化这个数组(初始化棋盘)
//InitBoard(board, 3, 3);//这个函数专门用来初始化棋盘的,所以传的参数是棋盘board,再传过去这个棋盘(数组)的行和列即3和3
//像这里的行3和列3在后来代码中可能会频繁大量使用,而且也可能会被修改,以防到处被修改,所以在game.h头文件中用#define定义一个符号ROW(行)值是3,再用#define定义一个符号COL(列)值是3,如果想要用这两个符号,则在本文件中需要包含这个头文件game.h:#include "game.h"
//这样分开文件写的好处:凡是修改涉及ROW和COL的可以在#define中只改它定义时的值即可——>方便,写出的代码不需要没必要的经常调整
char board[ROW][COL] = { 0 };//注意:对二维数组的初始化一般用的是大括号{},对一维数组初始化一般用双引号""
InitBoard(board, ROW, COL);//初始化棋盘
//接下来要实现(定义)这个函数
//函数的声明在头文件中,所以在game.h文件中声明一下InitBoard()函数
//函数的实现在game.c中
//初步初始化好棋盘后,打印棋盘看效果
DisplayBoard(board, ROW, COL);//用这个函数打印二维数组的内容,这里实参传的是数组名board和行ROW和列COL
//这个是打印函数,要想实现先声明,则在game.h头文件中声明
//在game.c文件中进行实现
//棋盘布置好后,开始玩游戏:玩家下棋,电脑下棋,玩家下棋,电脑下棋……是个循环
//则实现下棋的逻辑:
char ret = 0;
while (1)
{
//player_move();玩家下棋
player_move(board, ROW, COL);
//玩家下棋,因为整个过程都在操纵棋盘,所以需要把棋盘传进去,表示玩家下棋下的是ROW行COL列的board二维数组,
//这个函数需要在头文件game.h中声明,在文件game.c中实现
//这里打印一下棋盘
DisplayBoard(board, ROW, COL);
ret = is_win(board, ROW, COL);
if (ret!='C')
{
break;
}
//computer_move();//电脑下棋
//computer_move(board[ROW][COL], ROW, COL);//写法错误
computer_move(board, ROW, COL);
//电脑下棋是随机下,哪里用空格就在哪里随机下,电脑下棋也需要把数据(琪)下到棋盘中,所以也传进入棋盘和ROW行和COL列
//这个函数同样需要在头文件game.h中声明,在文件game.c中实现
//再打印一下棋盘
DisplayBoard(board, ROW, COL);
ret = is_win(board, ROW, COL);//int ret=is_win(board,ROW,COL)错误,不用再创建ret变量了
if (ret!='C')
{
break;
}
//玩家下棋,电脑下棋,需要判断输赢才能结束游戏,所以在上面代码中判断完善
//判断输赢:
//⑴玩家赢
//⑵电脑赢
//⑶平局(玩家和电脑都没赢的时候棋盘满了)
//⑷继续(游戏未结束,既不是玩家赢,不是电脑赢,也不是平局)
//所以有这4种状态,创建is_win()函数表示这4种状态
//所以:
//⑴玩家赢-----'*'//玩家赢了,函数的返回值
//⑵电脑赢-----'#'//电脑赢了,函数的返回值
//⑶平局-------'Q'//平局赢了的函数返回值
//⑷继续-------'C'//继续的函数返回值
//用返回值来确定游戏的状态
//is_win()函数是用来检测棋盘中是否有3行,3列或者对角线3个相连并且相同,所以需要传进去棋盘和ROW和COL知道几行几列
//⑴⑵⑶都表示的是游戏结束;只有⑷表明还可以玩游戏
}
//这里是在循环外判断的
if (ret == '*')
{
printf("玩家赢了\n");
}
else if (ret == '#')
{
printf("电脑赢了\n");
}
//else(ret == 'Q')
else
{
printf("平局\n");
}
}
void test()
{
int input = 0;
srand((unsigned int)time(NULL));//主函数中调用srand()函数,time()函数的返回值作为时间戳,参数是空指针,返回值强转类型为unsigned int
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;
}
②game.h文件中:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ROW 3
#define COL 3
//#define定义的符号通常习惯上全部大写
//这里ROW和COL专门是用来指定行和列的大小的
//1、初始化棋盘的函数声明(即声明InitBoard()这个函数)
void InitBoard(char board[ROW][COL], int row, int col);//参数是:数组、行、列,注意形参变量的行和列不能和#define定义的符号值冲突
//2、打印棋盘的函数声明
void DisplayBoard(char board[ROW][COL], int row, int col);
//注意:row和col是形参,是传进来的几行几列,是形参变量;
//ROW和COL是用来指定数组大小的符号,是确切的定义的值,只要ROW和COL的值变了则经它们传出,传进的row和col也相应发生变化
//3、玩家下棋的函数声明
void player_move(char board[ROW][COL], int row, int col);
//4、电脑下棋的函数声明
void computer_move(char board[ROW][COL], int row, int col);
//5、判断输赢的函数的声明
//char is_win();//返回类型是4种字符,是char
char is_win(char board[ROW][COL], int row, int col);
③game.c文件中:
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
//1、实现初始化棋盘的函数:
//实现函数:实现InitBoard()这个函数
void InitBoard(char board[ROW][COL], int row, int col)//这里用到了ROW和COL这两个符号,这两个符号来自game.h,所以在本文件中game.h头文件依然被包含
{
//1、首先初始化二维数组(棋盘)为全部空格
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
board[i][j] = ' ';
}
}
//已经初步初始化好
}
//2、实现打印棋盘的函数
//void DisplayBoard(char board[ROW][COL], int row, int col)
//{
// //打印二维数组里的所有内容
// //先打印二维数组的9个位置
// int i = 0;
// int j = 0;
// for (i = 0; i < row; i++)
// {
// for (j = 0; j < col; j++)
// {
// printf(" %c ", board[i][j]);//打印的是i行j列的元素,格式是:空格+元素+空格
// }
// printf("\n");
// }
//
//}
//错误,看不到棋盘,只是空出来3行
//需要的是行之间用3个“—”分隔开,即———;列之间用“|”分隔开
//所以调整代码为:
//void DisplayBoard(char board[ROW][COL], int row, int col)
//{
// int i = 0;
// int j = 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");
// }
// }
//}
//这个棋盘被写死了,行可以变动但只能写3列,不便于代码的扩展
//再调整:
void DisplayBoard(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
//打印数据:
//printf(" %c | %c | %c \n", board[i][0], board[i][1], board[i][2]);
//把“ %c |”看成一组数据,打印col组这样的数据,只不过最后一组没有打印“|”
//printf(" %c |")//这样写又写死了,因为这样写每次打印都必须打印“|”了
for (j = 0; j < col; j++)
{
printf(" %c ", board[i][j]);//这是数据的打印
if (j < col - 1)
{
printf("|");
}
}
printf("\n");//数据打印完后要换行
//打印分割行也是,把“---|”看成一组,打印col组,最后一组不打印“|”
//打印分割行
if (i < row - 1)
{
//printf("---|---|---\n");
for (j = 0; j < col; j++)
{
printf("---");
if (j < col - 1)
{
printf("|");
}
}
}
printf("\n");
}
}
//即以上代码实现了DisplayBoard()函数,即已经完成了正确地打印出棋盘(二维数组)
//3、实现玩家下棋的函数
void player_move(char board[ROW][COL], int row, int col)
{
//提示:
printf("玩家下棋:>");
//输入坐标
int x = 0;
int y = 0;
while (1)
{
scanf("%d %d", &x, &y);
//判断玩家输入数的范围
if (x >= 1 && x <= row && y >= 1 && y <= col)//输入坐标合法
{
//判断所下的琪的位置是否已经有之前下过的琪,因为值能在空格处下棋,不能悔棋
//if (board[x][y] == ' ')//因为对于二维数组来说行和列都是从0开始的,而玩家会认为行和列都是从1开始的
if (board[x - 1][y - 1] == ' ')
{
//是空格则可以下棋
//board[x][y] = "*";//错误
//board[x][y]="*"//错误,若玩家下的棋是星号*则应该用单引号引起来,如果用说双引号则是p
board[x - 1][y - 1] = '*';
break;//下棋成功,break,否则代码会死循环
}
else
{
printf("该坐标被占用,请重新输入!\n");
}
}
else
{
printf("坐标非法,请重新输入!\n");//坐标非法需要重新输入,则再上去重新输入,是个循环
}
}
}
//综上有坐标合法的判断,有坐标是否被占用的判断,然后再下棋(这里是星号标记)
//4、实现电脑下棋的函数
void computer_move(char board[ROW][COL], int row, int col)
{
//生成行坐标和纵坐标
int x = 0;
int y = 0;
printf("电脑下棋:>\n");
while (1)
{
//生成随机数,并确定生成随机数的范围
x = rand() % ROW;//生成行,这样写的范围是0~(ROW-1)
y = rand() % COL;//生成列,这样写的范围是0~(COL-1)
//所以坐标的合法性不用判断了
//判断坐标是否被占用
if (board[x][y] == ' ')//这里不需要进行-1操作,因为已经设置好范围了且不是玩家输入的
{
board[x][y] = '#';
break;//保证下棋成功后不再下棋
}
//如果坐标被占用也不需要提示,因为坐标被占用电脑上去重新生成即可,即写成一个循环
}
}
//5、实现判断输赢的函数
//检测棋盘中是否有3行,3列或者对角线3个相连并且相同
int is_full(char board[ROW][COL],int row,int col)
{
//遍历一下数组看有没有空格,写2次循环,二维数组创建了2个变量
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)
{
//首先检查一行一行的是否有相同的,每行都要检查,循环row次
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];//返回#或*,随意返回相同的一个值即能说明赢的状态(前面定义的)
}
}
//再检查一列一列的是否有相同的,每列都要检查,循环col次
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];
}
//判断是否平局:棋盘满了,没有空格——>用函数is_full(),判断的是board是否满了,传进去数组,行,列
if (1 == is_full(board, row, col))//这里用的是小写,因为row和col已经传给上面的row和col了(is_win函数中包含这个函数,是函数中调用了函数)
{
return 'Q';
}
//实现is_full函数,因为is_full()函数只是为了支持is_win()函数使用,没有必要在头文件中声明
//继续:
return 'C';
}
④注意:
1、游戏总体的打印效果:
2、疑问:打印棋盘时代码的实现过程:
由此写代码:
(被遮挡的代码是:printf(" %c | %c | %c \n", board[i][0], board[i][1], board[i][2]);)
但由于第三行并没有分割行所以调整代码:
列写死了,拓宽代码调整为:
分割列:
把“ %c |”看成一组数据,打印col组,因为这是列与列之间的分割,但最后一次没有“|”。所以先打印“ %c ”,再打印“|”,打印“|”时要判断一下是不是最后一次,是最后一次就不打印了。
同样,分割行:
把“---|”看成一组数据,打印row组,因为这是行与行之间的分割,但最后一次没有“|”。所以先打印“---”,再打印“|”,打印“|”时要判断一下是不是最后一次,是最后一次就不打印了。
注意:
两次嵌套循环:外围的for循环是确定打印row行(即循环row次),
在row行里面,又要循环打印“ %c ”和“|”,打印col列(即循环col次),还要确保打印最后一列时没有打印“|”;
代码中间有一行if (row - 1)作为判断条件是否继续打印下面的代码,如果是最后一行就不用打印“---和“|”了,所以就不用走下面打印的代码;
在row行里面,又要循环打印“---”和“|”,打印col列(即循环col次),还要确保打印最后一列时没有打印“|”。
3、注意:先判断坐标的输入是否合法再判断是否是空格。
4、
while (1)
{
player_move(board, ROW, COL);
DisplayBoard(board, ROW, COL);
ret = is_win(board, ROW, COL);
if (ret!='C')
{
break;
}
computer_move(board, ROW, COL);
DisplayBoard(board, ROW, COL);
ret = is_win(board, ROW, COL);
if (ret!='C')
{
break;
}
}
理解:
判断输赢的时候,这里是个循环,而循序终止的条件(break)是不等于'C',即不是继续。