【c语言】保姆级教程实现小游戏 —— 三字棋
所用知识:函数、数组、选择结构语句,循环结构语句等。
一、前言
大家好!这篇博客要讲的是,用c语言实现一个简单游戏——三字棋。
这也许对初学者来说,是一个工程量较大的课题,也许拿到它时第一时间大脑一片空白,不知道从何入手。其实在开始构思整个代码如何开展之前,我们不妨先来想想三字棋是怎样一个游戏,然后再把有关三字棋的信息整理成一条清晰的流程,依照流程来开展课题。
-
所用知识:函数、数组、选择结构语句,循环结构语句等。
-
所用编译器:Dev c++ / VS 2019
二、大致思路
-
三字棋是大概是怎样一个游戏?
两个玩家在一个九宫格棋盘上下棋,谁先将三颗棋子连成一条线,谁就赢得游戏。
【小结】以上有几个重要的信息:
1.这个游戏由两个玩家参与;
2.他们在九宫格棋盘上玩这个游戏;
3.赢得游戏的方式是,先对方一步将三颗棋子连成一线,换言之,轮到我方下棋时,若此时落在棋盘上的我方棋子正好一共三颗且能连成一线,则我方胜利。
-
三字棋要怎么玩?
两个玩家轮流在九宫格棋盘上任意一格内下棋,其中一方先下,另一方后下,落下的棋子会占用棋盘的一格,且后续会保留其在棋盘上的位置,之后下的棋子也不会将其代替。若一方领先对方将三颗棋子连成一线,则该方胜利,游戏结束,双方停止下棋;若双方将九宫格棋盘填满也没有一方的棋子有三颗连成一线,则双方平局,游戏结束,双方停止下棋。
【小结】以上也有几个重要的信息:
-
九宫格棋盘九个格子的任意一格,都可让双方玩家选择落棋,但已落有棋子的格子双方都不可以再选;
-
双方下在棋盘上的棋子在游戏结束前不会消失;
-
一方胜利或双方平局时则游戏结束,且双方都不再下棋。
-
思路整理
我们将以上信息稍微整理一下:
-
想下棋就要有个九宫格棋盘;
-
一个玩家先下,之后另一个玩家再下;
-
任一玩家赢了或双方平局游戏就结束。
这样,我们就得到了一条大致的思路。但它还是初步的,不够完整。所以我们再看看,对于一款游戏,这条思路是否还缺些什么。
相信大家应该玩过4399或7k7k小游戏。在大部分游戏进入之初,要么是账号登录界面,要么是一栏游戏菜单。
(例如:童年回忆 - 森林冰火人)
显然,“游戏菜单”更适合我们要做的这款三字棋。
至此,我们得到了以下的实现流程:
-
进入游戏之初,打印一份游戏菜单;
-
进入游戏之后,打印一张棋盘;
-
实现双方玩家有序下棋;
-
判断玩家的输赢情况,若有一方胜出或平局,则游戏结束。
下面,我们一一来看。
三、具体过程
0. 准备工作
首先的首先,头文件、main函数一个也不能少。
#include <stdio.h>
int main()
{
return 0;
}
因为部分代码可能需要反复调用,例如打印菜单、双方下棋等,所以接下来,我们尽可能用函数去实现每个部分。
1. 进入游戏之初,打印一份游戏菜单
我们需要一段代码,让玩家能够进入游戏。
这里我们创建一个test函数来实现我们的目的,并在main函数中调用。
int main()
{
test();
return 0;
}
test函数内部应该怎么编辑呢?
不妨回忆一下,玩小游戏的时候,游戏菜单是什么样的,有什么功能。
首先,菜单上会有许多选项供玩家选择,其次,选择相应的选项后,例如“开始游戏”,玩家就可以进入下一个界面开始玩游戏了。
那我们也可以依葫芦画瓢,先给玩家看一个菜单,然后让ta选择菜单上的选项。选项的内容我们可以简化一些,实现“开始游戏“和“退出游戏”就足够了。
(像存档、读档之类的,对于三字棋来说没太多必要,更何况,我也不会/皮)
这里我们可以再创建一个menu函数来打印菜单,然后在test函数中调用它。
而菜单上的选项就可以用下图的方式来表示。
void menu()//无须返回值,故返回类型为void
{
printf("*************************\n");
printf("*********1.play**********\n");
printf("*********0.eixt**********\n");
printf("*************************\n");
}
接下来,我们需要提示玩家选择选项,然后在玩家做完选择后实现相应的选项内容。
这里可以使用switch语句:
输入“1”代表选择开始游戏,然后开始游戏;
选择“0”代表选择退出游戏,然后游戏结束;
当然,有可能玩家会抬杠输入其他数字,但其他数字又不会触发我们设定好的后续流程,那我们就公屏提示ta,让ta重新选择。而重新选择势必又会回到最初的菜单和选项,而玩家要是一直抬杠,就会一直回到最初的起点,所以,这里要用到一个循环语句。而在循环开始之前,玩家会先做一次选择,那么,我们就用do while语句,让选择开始游戏和退出游戏的流程先走一遍,如果玩家一直没按菜单提示输入能进入后续流程的数字,我们就一直提示ta重新选择,直到ta顿悟为止或ta关掉程序不响完辣。
void test()
{
int input = 0;//用来接收玩家输入的值
do
{
menu();//打印菜单
printf("请选择:>");//提示玩家进行操作
scanf("%d", &input);
switch (input)//输入“1”开始游戏,选择“0”游戏结束
{
case 1:
game();//关于此函数,详见下文
break;
case 0:
printf("退出游戏\n");//提示信息
break;
default:
printf("选择错误,请重新选择\n");//提示信息
break;
}
} while (input);
}
2. 进入游戏之后,打印一张棋盘;
玩家选择“1”后,即进入游戏。
这里我们编写一个game函数,在test函数内调用,来实现游戏内的内容。
首先,我们需要做一张棋盘,来让玩家下棋。
我们观察一下,一般三字棋棋盘的外形:
-
一共有九个空格;
-
九个空格被像“井”字一样的横杠和竖杠分隔开。
不难想到,可以用二维数组来实现这个棋盘。
这里我们编写两个函数:
init_board - 将二维数组(棋盘)初始化
#define ROW 3//宏定义ROW(行)为3
#define COL 3//宏定义COL(列)为3
void init_board(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]=' ';//将二维数组(棋盘)初始化成空格
}
}
}
print_board - 打印初始化后的二维数组(棋盘)
//预期效果:
//" %c | %c | %c "
//"--- |--- |---"
//" %c | %c | %c "
//"--- |--- |---"
//" %c | %c | %c "
#define ROW 3
#define COL 3
void printf_board(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]);
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("---|---|---");
for(j=0;j<row;j++)
{
printf("---");
if(j<col-1)
{
printf("|");
}
}
}
printf("\n");
}
}
实际运行后效果:
然后我们将init_board 函数、print_board函数依次放入game函数中,以便在之后调用来实现游戏内容。
void game()
{
char board[ROW][COL];//定义一个char类型的二维数组(棋盘)
char ret=0;//定义一个char类型的变量,用来接收判断输赢的函数的返回值
//初始化棋盘为全空格
init_board(board,ROW,COL);
//打印此时的棋盘
printf_board(board,ROW,COL);
}
3. 实现双方玩家有序下棋;
我们编写两个函数:
player_move - 玩家下棋
computer_move - 电脑下棋
方便起见,我们让玩家先下。
下棋的方式是从键盘输入一个落子的坐标。
我们用 * 代表玩家的棋子。
为了让棋子显示在棋盘上,我们需要先判断玩家输入的坐标是否在棋盘上(即是否越界)。这个函数我们势必是要重复调用的,第一回合时,棋盘上不会有棋子,但第二回合,玩家和电脑已经在棋盘上落过子了,根据井字棋的规则,此时落过子的地方是不可以再下棋的。所以,若玩家输入的坐标在棋盘之内,我们还需要判断坐标所在位置是否已经下过棋(即坐标所在位置是否为空格,或是否为*)。若坐标在棋盘内,坐标位置也为空格,就将棋子*落在坐标位置,否则提醒玩家,输入的坐标无效,或此坐标处已下过棋,需重新输入一个坐标。
void player_move(char board[ROW][COL],int row,int col)
{
printf("玩家下棋\n");
int x=0,y=0;
printf("请输入要下棋的坐标:>");
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");//提示玩家操作
}
}
}
玩家下棋后,就轮到电脑下棋了。
我们用 # 代表电脑的棋子 。
一般来说,电脑下棋是随机的。那么,如何来实现随机下棋呢?
这里我们引入rand函数。
rand函数 - 生成随机数
1.头文件引用:#include<stdlib.h>
2.返回类型为int,返回值介于0到RAND_MAX(至少为32767)之间。
3.参数类型为void。
4.返回值的模以范围跨度,可将随机值生成的范围控制至所需的范围中:
v1 = rand() % 100; // v1 in the range 0 to 99
v2 = rand() % 100 + 1; // v2 in the range 1 to 100
v3 = rand() % 30 + 1985; // v3 in the range 1985-2014
而使用rand函数时,还需要用srand函数来设置一个随机值生成的起点。
srand函数 - 初始化随机数生成器
1.头文件引用:#include<stdlib.h>
2.返回类型为void。
3.参数类型为unsigned int。
4.为了生成随机数,通常初始化为一些独特的运行时值,如函数返回的值时间. 这足以满足大多数琐碎的随机化需求。
同时,依照上面的第三点,我们再用一个time函数,来设置随机值生成的起点。
time函数 - 获取当前时间
1.头文件:#include <time.h>
2.返回类型为time_t,获取当前计算机系统的日历时间作为类型的值。
3.参数类型为time_t*。
4.[补]时间戳:(当前计算机系统的日历时间) - (计算机系统的起始时间(一般为1970年1月1日0时0分0秒)) = (x秒),(x秒)即为时间戳。time函数返回的就是这个时间戳。
下面,我们分别来编写实现。
int x=rand()%row;//随机生成棋盘横坐标
int y=rand()%col;//随机生成棋盘纵坐标
srand((unsigned int)time(NULL));//需放在一开始就调用的test函数中,设置随机值生成的起点
//具体代码展示见下文。
//[ps]
//1.time函数的参数是一个指针,而我们无需这个参数,所以传给它一个空指针即可
//2.time函数返回值是time_t型,而srand函数的参数是unsigned int类型,所以此处将time函数的返回值类型,强制类型转换为unsigned int。
然后,我们继续完善实现电脑下棋的computer_move函数。
#include<stdlib.h>
#include<time.h>
void computer_move(char board[ROW][COL],int row,int col)
{
printf("电脑下棋\n");
//随机生成坐标,只要坐标没有被占用,就下棋
while(1)
{
int x=rand()%row;//一个随机数%3求余,可将x的值控制在[0,2]内
int y=rand()%col;//同上
//判断该坐标处是否已经下过棋
if(board[x][y]==' ')
{
board[x][y]='#';//判断成功,则下棋
break;//下棋成功,跳出循环
}
//判断失败,循环继续
}
}
4. 判断玩家的输赢情况,若有一方胜出或平局,则游戏结束。
游戏结局有三种:玩家胜出、电脑胜出,和平局。
一般来说,棋盘已经被下满,但还没有谁的三颗棋子连成一线,则算平局。
谁的三颗棋子先连成一线谁就胜出。
而三颗棋子连成一线的情况又有四种:行连一线、列连一线、对角线连一线(对角线有两种方向,为左上到右下、右上到左下)。
因此,我们一共需要对五种情况做五次判断:
-
行元素是否相等;
-
列元素是否相等;
-
左上到右下的对角线元素是否相等;
-
右上到左下的对角线元素是否相等;
-
棋盘是否已满。
我们编写一个is_win函数来判断输赢,每一回合都在game函数中调用一次。
可能有人会想到用多个不同的函数来对三种结局情况分别判断,但我们可以通过一个函数来更简便地实现。
不论判断谁胜出,其实都是判断ta的棋子是否连成一线,即行、列、对角线上元素是否相等。那么,若某一方棋子在行、列、对角线上连成一线,只需要返回其中一颗棋子(字符),即可确定谁胜出。
玩家的棋子是*,函数返回*则玩家胜出。
电脑的棋子是#,函数返回#则电脑胜出。
这样就一下解决了四种情况的判断。
而平局的判断就简单许多了,对于棋盘是否已满,也可以判断棋盘上是否还有空位置,即是否还有空格。
我们再编写一个is_full函数来判断棋盘是否已满,并在is_win函数内部调用。
//判断棋盘是否已满
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][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];
}
//同理,
//判断平局,若平局则返回字符‘Q’
if(is_full(board,row,col)==1)
{
return 'Q';
}
//若没有玩家或电脑胜出也没有平局,则返回字符‘c',游戏继续
return 'c';
}
然后,我们继续将player_move函数、computer_move函数、is_win函数依次放入game函数中,以便在之后调用来实现游戏内容。
void game()
{
char board[ROW][COL];//定义一个char类型的二维数组(棋盘)
char ret=0;//定义一个char类型的变量,用来接收判断输赢的函数的返回值
//初始化棋盘为全空格
init_board(board,ROW,COL);
//打印此时的棋盘
printf_board(board,ROW,COL);
while(1)
{
//玩家先下棋
player_move(board,ROW,COL);
//打印此时下棋后的棋盘
printf_board(board,ROW,COL);
//判断此时的输赢情况
ret=is_win(board,ROW,COL);
if(ret!='c')
{
break;
}
//电脑后下棋
computer_move(board,ROW,COL);
//打印此时下棋后的棋盘
printf_board(board,ROW,COL);
//判断此时的输赢情况
ret=is_win(board,ROW,COL);
if(ret!='c')
{
break;
}
}
if(ret=='#')
{
printf("电脑赢了\n");
}
else if(ret=='*')
{
printf("玩家赢了\n");
}
else if(ret=='Q')
{
printf("平局\n");
}
}
至此,整个流程就编写完成啦。
四、代码汇总
此处附上全部代码。
-
dev c++代码
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define ROW 3
#define COL 3
void init_board(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 printf_board(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)
{
for(j=0;j<row;j++)
{
printf("---");
if(j<col-1)
{
printf("|");
}
}
}
printf("\n");
}
}
void player_move(char board[ROW][COL],int row,int col)
{
printf("玩家下棋\n");
int x=0,y=0;
printf("请输入要下棋的坐标:>");
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)
{
printf("电脑下棋\n");
while(1)
{
int x=rand()%row;
int 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][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(is_full(board,row,col)==1)
{
return 'Q';
}
return 'c';
}
void menu()
{
printf("***************************\n");
printf("******* 1.play *********\n");
printf("******* 0.exit *********\n");
printf("***************************\n");
}
void game()
{
char board[ROW][COL]
char ret=0;
init_board(board,ROW,COL);
printf_board(board,ROW,COL);
while(1)
{
player_move(board,ROW,COL);
printf_board(board,ROW,COL);
ret=is_win(board,ROW,COL);
if(ret!='c')
{
break;
}
computer_move(board,ROW,COL);
printf_board(board,ROW,COL);
ret=is_win(board,ROW,COL);
if(ret!='c')
{
break;
}
}
if(ret=='#')
{
printf("电脑赢了\n");
}
else if(ret=='*')
{
printf("玩家赢了\n");
}
else if(ret=='Q')
{
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;
}
-
vs 2019代码
test.c文件 - 存放游戏的主体代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"//game.h中已有全部所需头文件,故包含game.h文件即可
void menu()
{
printf("*************************\n");
printf("*********1.play**********\n");
printf("*********0.eixt**********\n");
printf("*************************\n");
}
void game()
{
char board[ROW][COL];
init_board(board, ROW, COL);
print_board(board, ROW, COL);
char ret = 0;
while (1)
{
player_move(board, ROW, COL);
print_board(board, ROW, COL);
ret = is_win(board, ROW, COL);
is_win(board, ROW, COL);
if(ret!='C')
{
break;
}
computer_move(board, ROW, COL);
print_board(board, ROW, COL);
if (ret != 'C')
{
break;
}
}
if(ret=='#')
{
printf("电脑赢了\n");
}
else if(ret=='*')
{
printf("玩家赢了\n");
}
else if(ret=='Q')
{
printf("平局\n");
}
}
void test()
{
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;
}
game.h文件 - 存放所有头文件和函数的声明
#include <stdio.h>
#define ROW 3
#define COL 3
//在game.h放入所有要用到的头文件
#include <stdlib.h>
#include <time.h>
//初始化棋盘
void init_board(char board[ROW][COL], int row, int col);
//打印棋盘
void print_board(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文件 - 存放所有函数的本体
#include "game.h"//同理
//初始化棋盘
void init_board(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 printf_board(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)
{
for(j=0;j<row;j++)
{
printf("---");
if(j<col-1)
{
printf("|");
}
}
}
printf("\n");
}
}
//玩家下棋
void player_move(char board[ROW][COL],int row,int col)
{
printf("玩家下棋\n");
int x=0,y=0;
printf("请输入要下棋的坐标:>");
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)
{
printf("电脑下棋\n");
while(1)
{
int x=rand()%row;//3
int y=rand()%col;//3
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][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(is_full(board,row,col)==1)
{
return 'Q';
}
return 'c';
}
五、游戏运行效果
下面一起来看一看代码运行的效果。
感谢大家看到这里,希望对大家的学习有所帮助,祝大家学有所成:)
以上。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐
所有评论(0)