目录
前言:
一、文件建立:
1.头文件game.h:
2.函数定义文件game.c:
3.工程测试文件test.c:
二、编写井字棋游戏:
1.程序整体执行思路:
3.game游戏函数逻辑:
4.game函数中各功能的实现:
①.头文件game.h内容:
②.棋盘的初始化init_board:
③.打印棋盘print_board:
④.玩家着棋player_move:
⑤.电脑着棋computer_move:
⑥.胜利判定is_win:
⑦.交替着棋:
三、完整程序代码:
1.game.h:
2.game.c:
3.test.c:
四、总结:
在前面的九课中,我们学习了C语言程序的各个重要组成部分,结合往期文末的一些练习,想必现在大家对于知识已经有了不错的掌握,本文我将带领各位小伙伴们运用我们之前学过的知识,使用标准工程格式来写出我们的第一个小游戏——井字棋(又名三子棋)。
按照我们之前提到过的标准工程格式,我们首先建立三个文件:
该文件用于包含其它头文件,并存放功能实现函数的函数声明。好处是可以整合整个程序中所用到的全部头文件和函数声明,一目了然。并在引用时只需引用头文件game.h,其它被包含的头文件与函数声明均生效,省时省力:
该文件用于书写所有的程序功能实现的函数定义。使用这样格式的好处主要有三点:一来将所有定义书写在一起,便于我们进行阅读和修改;二是大大提升了程序的可移植性,当他人想要使用我们定义的函数时,只需将整个game.c文件发送给对方即可;第三点也是最重要的一点,我们可以通过将其设置为静态库,使其被转化为二进制码而进行加密,从而对我们的核心函数实现进行隐藏:
该文件用于书写我们的程序主体部分,供我们通过使用代码来对程序进行执行逻辑的编辑和检查。结合前两个文件的使用,可以使得我们的程序逻辑变得清晰,极有利于我们进行程序的运行逻辑检查:
各位小伙伴们一定都玩过各种各样的游戏,对于游戏也都有一定的了解。而游戏最中心的程序主体也不过是界面和游戏实现两个部分,依此我们可以直接写出井字棋小游戏的最核心主体:
void menu(void)
//菜单函数
{
...
}
void game(void)
//游戏函数
{
...
}
void test()
//整体流程
{
menu();
game();
...
}
int main()
{
test();
return 0;
}
他的执行流程很明确:自定义两个函数,menu为菜单函数,负责向玩家打印游戏菜单;game为游戏函数,负责实现整个游戏的逻辑实现。当程序开始编译运行后,将会按照顺序,先执行menu函数打印出游戏菜单,接着执行game函数让玩家们进行游戏。
并且我们都知道,大多数时候玩家往往会选择多次进行游戏,所以我们通过在数函数部分结合我们在之前介绍过的的循环和分支语句很容易实现该目的:
void test()
{
int input = 0;
do
{
menu();
printf("请您进行选择:");
scanf("%d", input);
switch (input)
{
case 1:
game();
break;
case 0:
break;
default:
printf("输入错误,请重新输入!\n");
break;
}
} while (input);
return 0;
}
通过使用do...while循环语句,既可以保证我们的玩家选择判断至少被执行一次,又可以供玩家选择继续游戏。
又通过使用switch分支语句,在打印出游戏菜单后让玩家进行选择,若玩家选择1则继续进行游戏,选择0则退出游戏。同时对输入数据进行合法性检测,使输入数据既不是1也不是0时要求玩家重新进行输入。
我们将程序运行起来检查一下我们的程序主体逻辑功能:
可以看到整个循环与分支选择以及功能模块都已成功实现,至此程序主体部分就已经确定好了,接下来只需完成功能模块的逻辑实现即可。
确定了程序的主体逻辑结构,我们就开始着手进行具体功能的实现。首当其冲的便是向玩家们展示游戏内容的menu菜单函数。
实质上这一部分并没有什么难度,这部分的功能仅仅是向玩家们打印出游戏功能供玩家们阅读,并没有实质的逻辑处理,因此仅使用printf函数即可轻松实现:
void menu()
{
printf("************************\n");
printf("************************\n");
printf("**** 欢迎游玩三子棋 ****\n");
printf("**** 请选择 ****\n");
printf("**** 1.开始游戏 ****\n");
printf("**** 0.退出游戏 ****\n");
printf("************************\n");
printf("************************\n");
}
没错,菜单函数就是这么朴实无华,运行起来看看效果:
轻轻松松,对于现在的小伙伴们来说正能说是是小菜一碟。菜单功能已经实现,可以进行到下一模块的编写。
(注:此处我们只关心实现逻辑,而具体功能实现后面会逐一进行研究)
程序的主体确定好了,游戏菜单也成功打印出来了,但是我们都很清楚,三子棋最重要的,还是游戏内容的实现,毕竟没有玩家会愿意去玩一个仅仅只有菜单的“游戏”。
首先我们要来了解一下三子棋游戏的游戏规则和胜利条件,只有了解了这些我们才能进行相关功能的实现。三子棋顾名思义,首先它是一种棋类游戏,它有标准的3×3九宫格棋盘,由游戏双方交替着棋,两方相互进行阻拦,当其中一方达成同一排、同一列或斜向三颗棋子连线时,该玩家获得胜利。
梳理了规则,我们开始尝试实现。
在游戏函数中,首先我们进行棋盘的初始化,即在game函数内打印出内留着棋位置的九宫格棋盘。具体的实现方式是,创建一个九宫格数组,用于处理玩家与电脑的着棋操作,同时在没有着棋的位置应当进行初始化,即打印空格:
void game()
{
char board[ROW][COL];
//创建九宫格数组,用于处理着棋
char ret = 0;
//定义字符,用于判断输赢后跳出着棋循环
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);
if (ret != 'C')
{
break;
}
//判断输赢
computer_move(board, ROW, COL);
//电脑进行着棋
print_board(board, ROW, COL);
//打印棋局
ret = is_win(board, ROW, COL);
if (ret != 'C')
{
break;
}
//判断输赢
}
if (ret == '*')
{
printf("恭喜玩家获得胜利!\n");
}
else if (ret == 'O')
{
printf("很遗憾,电脑取得了胜利。\n");
}
else if (ret == 'Q')
{
printf("棋逢对手!战至平局!\n");
}
这样,游戏的大致逻辑就编辑完成了,而接下来就是重点的各项功能的实现。
上面我们说过,在我们的头文件中,存放的是我们的其他头文件、宏定义与函数的声明。
首先是宏定义:
(此处为定义棋盘参数行ROW和列COL,并均赋值为3,即三行三列)
#define ROW 3
#define COL 3
//进行宏定义,好处是进行游戏修改时不需要对每个参数都进行修改
我们的程序为了更好的可拓展性,往往会对程序进行扩展。例如我们现在书写的是井字棋,倘若日后想要把他改写为五子棋等等,只需要对这两个宏定义的量进行改变即可,而免去了挨个修改参数的麻烦。
接着是头文件,为了使test.c文件看起来更加简洁清晰,我们可以将其他的头文件引用在game.h文件中,而同时,因为我们的test.c文件中又会引用我们的game.h头文件,所以被包含在game.h头文件中的其他头文件也会被附带引用至我们的test.c文件中:
(此处为本游戏所需要用到的三个头文件,关于其作用后面会逐一讲解)
#include
#include
#include
//因为此头文件会被引用,故在此头文件中引用的其它头文件也会被包含引用
最后就是我们的游戏逻辑函数中用到的函数声明了:
(此处为本游戏中全部的函数声明,函数定义将在后面逐一研究)
void init_board(char board[ROW][COL], int row, int col);
void print_board(char board[ROW][COL], int row, int col);
void playr_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);
这里的实现很简单,我们在之前就已经定义好了一个二维字符型数组board,在函数接收该数组后,首先对二维数组中的内容进行初始化。我们看到的空棋盘实际上并不为空,而是在未着棋处打印了一个空格字符。故其初始化也很简单,只需将其遍历,使数组board中的每一项均为空格字符即可:
//棋盘初始化(使无着棋位置均使用空格占位):
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 print_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 < col; j++)
{
printf("-------");
if (j < col - 1)
{
printf("|");
}
}
printf("\n");
}
}
}
我们可以看到在每一行的处理中,我们都以列为单位进行处理,交替执行打印字符与打印列分隔符“ | ”,并同时在打印列分隔符“ | ”时进行边界限制。
而在数据行打印后,同样每个数据行也与一组行分隔符“ ------- ”一同打印,且行分隔符间也使用与数据行同样的方式进行列的分隔。同时在使用行分隔符时也进行了边界限制。
如此循环每个数据行的处理,最终完成棋盘的打印,我们将其编译运行来看效果:
可以看到当前我们定义的棋盘格式已经完成了打印操作。
棋盘已经初始化完成了,这下我们就可以开始下棋了,要实现玩家的着棋操作,原理是由玩家输入着棋坐标,由函数进行处理后对数组内相应位置的数据元素进行符号替换:
void player_move(char board[ROW][COL], int row, int col)
{
while (1)
{
int x, y;
x = y = 0;
printf("请着棋:\n");
printf("您想下在第几行:");
scanf("%d", &x);
printf("您想下在第几列:");
scanf("%d", &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");
}
}
}
在这里有两个要注意的地方,一个是我们输入坐标时的行与列均是从1开始,而二维数组中的数据元素下标均为从0开始,所以在对下标进行操作时,需要将玩家输入的行和列均减一后进行操作。
第二点是在进行着棋时应当进行着棋可行性判断,若输入坐标不在棋盘范围内,应当予以提示“输入坐标有误,请重新输入”,若着棋处已经下过棋子了,即不为空格符,应当提示“这个位置已经有棋子了喔!请重新输入”。
当玩家着棋后,将会交替至电脑进行着棋操作,那么电脑如何着棋呢?
首先我们来看电脑的着棋步骤:
void computer_move(char board[ROW][COL], int row, int col)
{
printf("电脑正在着棋!\n");
while (1)
{
int x;
int y;
if (board[x][y] == ' ')
{
board[x][y] = 'O';
break;
}
}
}
很明显,电脑着棋的原理与玩家着棋原理完全一致,均是通过输入一个坐标,通过数组下标对数组内数据元素进行替换完成着棋。但问题是,电脑不同于玩家,并没有很强的随意性,可我们总不能让电脑顺着数组中的位置一个位置一个位置着棋吧?
所以这里显然我们需要想个办法来让电脑的着棋具有一定的随机性——使用时间戳。
大家对rand这个函数还有印象吗?
我们通过在头文件中引用头文件
#include
并在主程序中使用srand函数,并根据时间戳来规定随机值起点。同时这个随机数的起点在一次游戏中只需要生成一次即可,故应当放置在主函数的循环之外。并且这里一定注意,time(NULL)的返回值为time_t类型,而srand函数的参数应当为整形,故在此需要使用(unsigned int)来进行强制类型转换,将time(NULL)的返回值转换为无符号整型:
srand((unsigned int)time(NULL));
//用时间戳来生成随机数,用于电脑着棋位置判断
现在我们就可以使用根据时间戳规定起点而生成的随机数了:
int x = rand();
int y = rand();
可是这样就行了吗?显然不是,这个随机数的起点在哪里谁也不知道,而我们棋盘最多也只有三行三列,除非你的运气好到离谱,否则大概率会导致电脑着棋在这一步卡住,那么我们怎样进行处理呢?答案很简单,我们只需要让生成的随机数对我们的行和列进行求余即可,求余的结果刚好是0到行/列数-1,即刚好对应了我们二维数组的下标:
int x = rand() % row;
int y = rand() % col;
所以最终电脑着棋的函数定义就实现了:
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] = 'O';
break;
}
}
}
我们的游戏一定是需要进行胜利判定的,不可能无休止的进行下去。
在进行胜利判定时,首先分为两种情况,即棋盘已满与棋盘未满。
棋盘已满分为两种情况,一是若此时刚好分出胜负,就可以宣布胜利者了,另一种则是棋盘已满但并未分出胜负,此时应当宣布平局。
棋盘未满则分为三种情况,一是玩家获得了胜利,此时应当停止着棋并宣布玩家获得胜利;二是电脑获得了胜利,此时应当停止着棋并宣布电脑获胜;第三种情况便是棋盘既没有满,同时也没有获胜方出现,此时应当继续进行着棋:
首先我们逐行对行进行判定,若一行中三个数据元素均相同且不为空格符,则返回胜利者的棋子类型:
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];
}
再往后,我们使用遍历思想进行平局判定,我们依次检查每一个元素,只要存在元素为空格符就返回0,过程中遇到不为空格符的数据元素则不进行操作。但若所有元素都不为空格符,即棋盘已满,则返回1:
int is_full(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++)
{
if (board[i][j] == ' ')
{
return 0;
}
}
}
return 1;
}
接着在我们的判定函数中接收平局判定结果,若判定结果为真,且没有达成前述任何一个胜利条件,则返回平局符号Q:
if (is_full(board, row, col) == 1)
{
return 'Q';
}
但若上述情况都没有发生,即棋盘既没有满,也没有任何一方获胜,则返回继续着棋符号C:
return 'C';
把它们组装在一起,于是我们得到了以下完整的胜利判定函数:
//平局判定:
int is_full(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++)
{
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';
}
在胜利判定函数中我们看到,经由函数的判定,我们可以得到四种不同的返回值:" * "表示玩家胜利," O "表示电脑胜利," Q "表示平局," C "表示继续游戏。我们再将这组返回值返回至游戏的逻辑函数中进行循环判定:
while (1)
{
player_move(board, ROW, COL);
//玩家进行着棋
print_board(board, ROW, COL);
ret = is_win(board, ROW, COL);
if (ret != 'C')
{
break;
}
//判断输赢
computer_move(board, ROW, COL);
//电脑进行着棋
print_board(board, ROW, COL);
ret = is_win(board, ROW, COL);
if (ret != 'C')
{
break;
}
}
若返回值为表示继续游戏的C,则由循环条件可以得出将会继续进行着棋和判定的循环;若返回值不为C,则由break语句跳出循环。
当跳出循环后,就意味着已经产生了游戏结果,此时再根据三种不同的返回值来判定和宣布游戏的胜利者:
if (ret == '*')
{
printf("恭喜玩家获得胜利!\n");
}
else if (ret == 'O')
{
printf("很遗憾,电脑取得了胜利。\n");
}
else if (ret == 'Q')
{
printf("棋逢对手!战至平局!\n");
}
至此,井字棋游戏最基础的游戏功能就得到了实现。
#pragma once
#define ROW 3
#define COL 3
//进行宏定义,好处是进行游戏修改时不需要对每个参数都进行修改
#include
#include
#include
//因为此头文件会被引用,故在此头文件中引用的其它头文件也会被包含引用
//头文件中进行函数的声明:
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);
#define _CRT_SECURE_NO_WARNINGS 1
#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 print_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 < col; j++)
{
printf("-------");
if (j < col - 1)
{
printf("|");
}
}
printf("\n");
}
}
}
//玩家着棋:
void player_move(char board[ROW][COL], int row, int col)
{
while (1)
{
int x, y;
x = y = 0;
printf("请着棋:\n");
printf("您想下在第几行:");
scanf("%d", &x);
printf("您想下在第几列:");
scanf("%d", &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] = 'O';
break;
}
}
}
//平局判定:
int is_full(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++)
{
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';
}
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
//引用自定头文件
void menu()
{
printf("************************\n");
printf("************************\n");
printf("**** 欢迎游玩三子棋 ****\n");
printf("**** 请选择 ****\n");
printf("**** 1.开始游戏 ****\n");
printf("**** 0.退出游戏 ****\n");
printf("************************\n");
printf("************************\n");
}
void game()
{
char board[ROW][COL];
//创建九宫格数组,用于处理着棋
char ret = 0;
//定义字符,用于判断输赢后跳出着棋循环
init_board(board, ROW, COL);
//将数组与行列数传递给封装函数进行初始化
print_board(board, ROW, COL);
//将初始化完成的棋盘进行打印
while (1)
{
player_move(board, ROW, COL);
//玩家进行着棋
print_board(board, ROW, COL);
ret = is_win(board, ROW, COL);
if (ret != 'C')
{
break;
}
//判断输赢
computer_move(board, ROW, COL);
//电脑进行着棋
print_board(board, ROW, COL);
ret = is_win(board, ROW, COL);
if (ret != 'C')
{
break;
}
}
if (ret == '*')
{
printf("恭喜玩家获得胜利!\n");
}
else if (ret == 'O')
{
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:
break;
default:
printf("输入错误,请重新输入!\n");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
以上就是井字棋游戏的最基础功能的实现,但我们发现它仍存在着很多问题。例如它还没有优手判定,即电脑的着棋过分随机而不会对玩家的着棋威胁进行堵截;再比如我们的界面显示格式不够优美,同一屏幕打印显得游戏较为凌乱等等。
而这些问题我将会在下一篇文章中为各位小伙伴们进行介绍。希望各位小伙伴们下去以后仔细思考,认真研究,将这个综合性较强的阶段练手充分消化吸收。
以上就是今天我为大家介绍的基础版井字棋的知识啦!所有的努力,不是为了让别人觉得你了不起,而是让自己过得充实而有追求!
新人初来乍到,辛苦各位小伙伴们动动小手,三连走一走 ~ ~ ~ 最后,本文仍有许多不足之处,欢迎各位看官老爷随时私信批评指正!