1、前言
2015年,我刚刚上大一。大一上学期,我们就学习了C语言这门课程。学了大概两个多月吧,我就心血来潮,在学校图书馆的机房里自主编写了我的第一个C语言项目——五子棋小程序。
我依稀记得,那个夏日的夜晚,风带着些许暑气,一个少年背着沉重的书包,拖着疲惫却又轻快的身躯,满意地走出图书馆的身影。他的心中有说不尽的甜——毕竟,做成了此生第一个完全由自己开发的小游戏。
而当时的我编写的五子棋小游戏是什么样子的呢?当时的我还不会GUI编程(虽然到现在也还没学会……),所以就借鉴了一下更早以前玩过的一个基于CUI的围棋小程序。那个围棋小程序,是用点“·”来表示棋盘上的每一个交叉点,用加号“+”来表示九个星位,用字母x来表示黑棋,用字母o来表示白棋。所以我的第一个五子棋小程序,也是这么做的。
用一个二维数组来储存当前棋盘的状态,然后用switch-case结构与双重for循环把它显示成对应的字符。接着,提示用户输入纵坐标与横坐标,在落子前还要先判断一下用户输入的坐标值是否超出范围。然后,再用原始又冗长的一长段判断语句,来判断有没有连成五子。虽然现在回看起来,那个判断条件真的是蠢萌蠢萌的,但当时的我真的是较劲了脑汁、使出了浑身解数,才想到了相对当时来说最高效的算法。
总的来说吧,那段时间还是挺快乐的,因为过得很充实,做这个做那个的,学到了很多,也创造了很多。但是呢,这个程序也暴露了当时的我编程的很多问题。比如,代码冗长,不懂得封装(因为还没有学到函数);比如,(现在看来)不必要的状态指示变量太多了,结构过于复杂。
基于以上种种问题,我决心把我的这个五子棋小程序从头再写一遍。利用后来所学的知识,该优化规格的优化规格,该优化代码的优化代码,该优化算法的优化算法。于是就做成了这篇文章所介绍的,我的新版本(2021版)五子棋小程序。
2、项目规格
【外部规格】
界面表现形式:CUI
操作方法:通过WSAD键控制光标上下左右移动,按空格键落子。
对战形式:仅可进行双人单机对战。无法进行人机对战、联网对战模式。
异常处理:当要落子的坐标上已经有棋子时,会报错提醒玩家重新落子。
回合制:否。该版本暂时为一回合游戏。胜负分晓即结束游戏。
BUG情况:最新版本中暂未发现BUG
【内部规格】
开发所用语言:C语言
代码总行数:195行
函数总数:6个(包括main函数)
3、核心算法
本程序用以检验是否连成五子的算法为,当棋手按下空格键落下棋子后即判定。以棋手落子位置为原点,首先进行x轴方向的判定:判断其本身与左侧4坐标点、右侧4坐标点,合计9个坐标点的范围内是否存在连续的五个子。具体的方法是,定义一个stoneCounter变量(初始值设作0),用以计数目标棋子在指定范围内连续出现了多少次。用for循环遍历这9个位置,每遇到一个目标棋子,stoneCounter++。但凡遇到一个非目标棋子的点,stoneCounter就立刻清零,从下一次再遇到目标棋子时开始重新计数。如果stoneCounter的值一旦达到了5,则立刻终止循环(注意:检测到五子连珠后立即终止循环很重要,否则容易出现BUG,导致明明已经五子了却判定为没有赢。因为,如果不停下来,而五子后面还有待检测的坐标,与目标棋子的代码不一致的话,stoneCounter就会清零,显示没有找到连续的五个子),返回肯定的判定结果给main函数,主程序就知道这名棋手胜利了。而如果遍历完9个位置,stoneCounter却始终没有到达过5,则说明检测范围内没有出现五子连珠。以此类推,y轴方向、函数y=x图像所在直线方向、函数y=-x图像所在直线方向亦然。
4、源代码
#include <stdio.h>
#include <stdlib.h> //要调用系统命令,需要导入stdlib.h头文件
#include <conio.h> //要接收键盘事件,需要导入conio.h头文件
#define X 1
#define O 2
char toSymbol(int num){
//函数作用:给定一个棋盘位置的状态值,返回这个状态值所对应的图形符号
switch(num){
//其实本来是想用实心圆●表示黑棋,用空心圆⭕表示白棋的,但这次下的这个编译器似乎不太允许这样,显示出来全是乱码,于是只好改用X和O。
case 0:
return '.'; //空交叉点
case X:
return 'X'; //棋子X
case O:
return 'O'; //棋子O
case 9:
return '+'; //星位
}
}
int check_hor(int panel[][15], int y, int x, int object){
//检查x轴方向上是否连成五子
int stoneCount = 0;
for(int i = x-4; i<=x+4; i++){
if(panel[y][i]==object){
stoneCount++;
if(stoneCount == 5){
break;
}
}else{
stoneCount = 0;
}
}
if(stoneCount >= 5){
return 1;
}else{
return 0;
}
}
int check_ver(int panel[][15], int y, int x, int object){
//检查y轴方向上是否连成五子
int stoneCount = 0;
for(int i = y-4; i<=y+4; i++){
if(panel[i][x]==object){
stoneCount++;
if(stoneCount==5){
break;
}
}else{
stoneCount = 0;
}
}
if(stoneCount >= 5){
return 1;
}else{
return 0;
}
}
int check_slash(int panel[][15], int y, int x, int object){
//检查函数y=-x的图像所在直线方向上是否连成五子
int stoneCount = 0;
for(int i = y-4,j=x-4; i<= y+4; i++,j++){
if(panel[i][j]==object){
stoneCount++;
if(stoneCount==5){
break;
}
}else{
stoneCount = 0;
}
}
if(stoneCount >= 5){
return 1;
}else{
return 0;
}
}
int check_backslash(int panel[][15], int y, int x, int object){
//检查函数y=x的图像所在直线方向上是否连成五子
int stoneCount = 0;
for(int i = y+4, j=x-4; i>=y-4; i--,j++){
if(panel[i][j]==object){
stoneCount++;
if(stoneCount==5){
break;
}
}else{
stoneCount = 0;
}
}
if(stoneCount >= 5){
return 1;
}else{
return 0;
}
}
int main(void){
int key = 0;
//棋盘初始状态
int panel[15][15] =
{
{
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{
0,0,0,9,0,0,0,0,0,0,0,9,0,0,0},
{
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{
0,0,0,0,0,0,0,9,0,0,0,0,0,0,0},
{
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{
0,0,0,9,0,0,0,0,0,0,0,9,0,0,0},
{
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}};
int cus[]={
7,7}; //光标初始位置
int turn = X; //初始玩家
int winner=0; //赢家:默认暂时没有
while(1){
//清屏,并显示现在是谁走棋
system("cls");
printf("现在是【%c】方走棋……\n",toSymbol(turn));
//显示棋盘
for(int i = 0; i < 15; i++){
for(int j=0; j < 15; j++){
if(i==cus[0] && j==cus[1]){
printf("[ %c ]", toSymbol(panel[i][j]));
}else{
printf(" %c ", toSymbol(panel[i][j]));
}
}//next j
printf("\n\n");
}//next i
//如果有人赢了,显示赢家是谁,并结束程序
if(winner!=0){
printf("五子连珠!玩家【%c】胜!\n", toSymbol(winner));
return 0;
}
//接收键盘事件
key = getch();
//按WASD进行控制,空格键落子,L键结束游戏
switch(key){
case 'w': case 'W':
if(cus[0]==0) cus[0]=14;
else cus[0]--;
break;
case 'a': case 'A':
if(cus[1]==0) cus[1]=14;
else cus[1]--;
break;
case 's': case 'S':
if(cus[0]==14) cus[0]=0;
else cus[0]++;
break;
case 'd': case 'D':
if(cus[1]==14) cus[1]=0;
else cus[1]++;
break;
case 32:
if(panel[cus[0]][cus[1]] != X && panel[cus[0]][cus[1]] != O){
panel[cus[0]][cus[1]] = turn;
if(check_hor(panel,cus[0],cus[1],turn)==1
|| check_ver(panel,cus[0],cus[1],turn)==1
|| check_slash(panel,cus[0],cus[1],turn)==1
|| check_backslash(panel, cus[0],cus[1],turn)==1){
winner = turn;
break;
}
if(turn == X) turn = O;
else turn = X;
}else{
printf("这里已经有子了,重来!\n");
system("pause");
}
break;
case 'l': case 'L':
printf("结束程序……\n");
system("pause");
return 0;
default:
printf("无效按键!\n");
system("pause");
break;
}
}
return 0;
}
5、运行结果
一开始的时候出了几个小BUG,比如光标无法正确移动啦,无法正确判定是否连成五子什么的。但后来,这些问题也都在我的刻苦钻研下迎刃而解了。目前看来结果还算是正常的,没有再发现什么其他BUG了。不过如果有热心的同志运行我的代码发现了新的BUG,或者有什么意见或建议的(特别是在算法优化方面),欢迎评论留言私信。
6、未来计划
未来还计划制作基于CUI的象棋小程序、汉诺塔小程序。并且再往后还计划走出CUI,使用Java将他们GUI化,做出真正的图形界面游戏。如果你还有什么新颖的创意,也欢迎联系我。
路漫漫其修远兮,吾将上下而求索。
转载:https://blog.csdn.net/weixin_41018509/article/details/115557449