在学C++的时候,在做课设的时候做了一个五子棋课设,当时选这个就是觉得挺好玩的,想理解一下游戏是如何实现的,然后就入了这个坑。。。这个我没有用图形化界面来实现,完全是使用字符来实现。然后没有用图形化界面的最大的问题就是闪屏。。。因为普通的实现一般是用到清屏命令,每一次刷新输出的时候都要时间,所以就会闪屏,看着这一闪一闪的屏幕的确令有强迫症的我看到很难受,然后在全部完成五子棋的功能之后,整整花了两三天时间去解决这个问题,最终还是解决了。废话不说那么多了,直接开始吧哈哈哈。
功能介绍:
(1)开始界面和界面大小的控制 在游戏运行开始的时候,出现一个初始化界面 ,介绍游戏名字、游戏背景选择项(使用system()来改变控制台背景颜色使用sleep()来实现提示语的先后出现 ,使用字符串格式化函数sprintf()函数来实现控制台窗口的大小)。
(2)下棋操作 先通过键盘的上下左右箭头和WASD的字母移动下棋的位置,按空格键进行下棋(通过getch()函数和switch语句实现)。
(3)人人对战功能 玩家可通过先后移动棋子坐标位置进行下棋,进而进行对战游戏,直至分出胜负,在游戏期间玩家可以按n来实现重新游戏,按Esc退出游戏,可以在下方看到该是哪方下棋(通过坐标值的变化然后重新打印一张棋盘来实现光标的移动,这里用到双缓存技术来解决system(“cls”)的闪屏问题)。
(4)判断玩家棋子位置的合法性 玩家通过键盘的输入确定棋子位置坐标,系统会人性化判断棋子的合法性。若棋子不合法,玩家需要再次输入棋子位置坐标;若棋子合法,未决出胜负则继续由另一玩家确定棋子坐标。
(5)胜负判断功能 游戏系统程序可以通过棋盘上棋子的横竖方向上相同玩家棋子数量的关系,判断玩家棋子是否已构成五连子。若棋子构成五连子,则可选择结束游戏或者再来一局;若棋子没有构成五连子,则玩家可继续游戏。
(6)结束界面 玩家分出胜负之后,出现结束界面,玩家可选择是否继续玩游戏。若玩家选择继续游戏,则会重新出现空白棋盘;若玩家选择不继续游戏,则退出游戏界面。
(7)悔棋功能实现(新增) 实现原理:利用栈,在下子即按下空格键的时候,将当前子的坐标压栈,我是用了两个栈,一个记录X坐标,一个记录Y坐标,即X.push(g_cursorX); Y.push(g_cursorY);
然后在按下悔棋键的时候,将栈顶的X,Y坐标赋值为0,即g_checkboard[X.top()][Y.top()] = 0;
代码修改见文末。
五子棋基本的实现并不难,认真的看代码的话都会看的懂,最难的部分就是解决闪屏问题那部分,我当时也尝试了很多弯路,想要真正了解原理对于初学者并不容易,因为涉及到了比较底层的东西,但是不了解并影响实现,基本就是一个模板,要改的地方就是输出内容那个地方。我当初就是经过不断找资料不断地尝试,然后就意外的成功了,也算是比较幸运吧。所以叫我解释如何实现双缓冲我也说不清楚,上一个我参考过得链接吧。希望对你们有帮助。
https://blog.csdn.net/z1832729975/article/details/88382018
完整代码(很多地方都有注释了,所以我也不多说了直接看代码吧,有不懂或者改进的建议欢迎留言):
tips:这个程序在旧版控制台输出更加完美,因为旧版和新版的符号格式有些不一样,一般没有设置过的话都是新版的控制台,要设置的话也很简单,在输出框上方白色的地方右键,然后选择属性,选项的界面中勾上使用旧版控制台退出重新运行即可。
#include
#include
#include
#include
using namespace std;
#define CHECKBROADSIZE 25 //棋盘的大小
class Rungame //运行游戏:绘制棋盘
{
private:
int g_checkboard[CHECKBROADSIZE][CHECKBROADSIZE];
char data[CHECKBROADSIZE][CHECKBROADSIZE]; //存放并输出棋盘组成符号
int g_currentGamer; //当前玩家下子 1代表黑子,2代表白子
int g_cursorX, g_cursorY; // 坐标
public:
void DrawCheckborad(); //绘制棋盘
void Init(); //棋盘初始化
int RunGame();
int Put(); //下子成功 成功返回1 失败返回0
int Check(); //判断输赢
void buffer(); //双缓冲实现函数
void blackground(); //背景颜色设置函数
};
int zhixiang_hOutput = 0; //通过指针轮流指向两个缓冲区,实现双缓冲
HANDLE hOutput, hOutBuf; //控制台屏幕缓冲区句柄
HANDLE *houtpoint;
COORD coord = { 6, 6 }; //起始点坐标
DWORD bytes = 0; //DWORD 双字即为4个字节,每个字节是8位,共32位,变量类型的内存占位
int main()
{
Rungame chessboard;
Rungame rungame;
chessboard.blackground();
chessboard.buffer();
SetConsoleTitle(L"五子棋人人对战"); //设置控制台的标题
//进入游戏
while (1)
{
rungame.RunGame(); //运行函数
}
return 0;
}
void Rungame::blackground() //设置背景函数
{
//设置控制台窗口大小
char stCmd[32];
sprintf(stCmd, "mode con cols=%d lines=%d", CHECKBROADSIZE + 35, CHECKBROADSIZE + 8); //格式化控制台窗口函数
system(stCmd);
puts("*****************welcome to wuziqi world!******************");
Sleep(500);
puts("please press 'w' to turn to the white background...");
Sleep(400);
puts("please press 'g' to turn to the green background...");
Sleep(300);
puts("please press 'b' to turn to the blue background...");
Sleep(200);
puts("please press 'r' to turn to the red background...");
Sleep(100);
puts("please press any other key to turn to the black background.");
//控制控制台的颜色输出 如8代表输出灰色背景 0为黑色字体
switch (getch())
{
case'g':
system("color 0A");
break;
case'w':
system("color F0");
break;
case 'b':
system("color F9");
break;
case 'r':
system("color 0C");
break;
}
}
int Rungame::RunGame()
{
Init(); //初始化
int nInput = 0; //键盘输入值
int nWinner = 0; //赢家
while (1)
{
//system("cls"); //清屏 导致头上冒出一串东西
DrawCheckborad(); //绘制棋盘
switch (getch())
{
case 32: //space
{
// 1.下子 能不能下
if (Put())
{
//2.判断输赢,每下一颗子判断一次输赢
nWinner = Check();
//更换玩家
g_currentGamer = 3 - g_currentGamer;
if (nWinner == 1)
{
if (MessageBox(NULL, L"'■'方胜,是否再来一局?", L"五子棋", MB_YESNO) == IDYES)
{
RunGame(); //重新开始游戏
}
}
else if (nWinner == 2)
{
if (MessageBox(NULL, L"'○'方胜,是否再来一局?", L"五子棋", MB_YESNO) == IDYES)
RunGame(); //重新开始游戏
}
}
}
break;
case 75: //向左
case 'a':
g_cursorY--;
if (g_cursorY < 0) //到最左的时候从最右边出来
g_cursorY = 0;
break;
case 77: //向右
case'd':
g_cursorY++;
if (g_cursorY > CHECKBROADSIZE - 1)
g_cursorY = CHECKBROADSIZE - 1;
break;
case 72://向上
case 'w':
g_cursorX--;
if (g_cursorX < 0)
g_cursorX = 0;
break;
case 80://向下
case 's':
g_cursorX++;
if (g_cursorX > CHECKBROADSIZE - 1)
g_cursorX = CHECKBROADSIZE - 1;
break;
case 'n': //按下n重新游戏
//提示信息
if (MessageBox(NULL, L"是否要重新开始游戏?", L"五子棋", MB_YESNO) == IDYES)
{
RunGame();
}
break;
case 27:
//提示信息
if (MessageBox(NULL, L"是否要退出游戏?", L"五子棋", MB_YESNO) == IDYES)
{
exit(0);
}
break;
}
}
return 0;
}
void Rungame::Init()
{
Rungame rungame;
memset(g_checkboard, 0, sizeof(g_checkboard)); //清空二维数组为0在下子的时候判断
g_currentGamer = 1; //黑子先下
g_cursorX = CHECKBROADSIZE / 2; //开始游戏的时候光标在棋盘中间
g_cursorY = CHECKBROADSIZE / 2;
}
void Rungame::DrawCheckborad()
{
//输出提示信息
char Tips[] = "please press 'w' 'a' 's' 'd' to control the cursor... ";
char Tips1[] = "press 'n' to have a new game... ";
char Tips2[] = "press 'space' to play the chess... ";
char Tips3[] = "press 'Esc' to exit the game... ";
char score[50];
for (int i = 0; i < CHECKBROADSIZE; i++) //控制行
{
for (int j = 0; j < CHECKBROADSIZE; j++) //控制列
{
if (i == g_cursorX && j == g_cursorY)
data[i][j] = 206; //╬
else if (g_checkboard[i][j] == 1) //黑子
data[i][j] = 219; //■
else if (g_checkboard[i][j] == 2) //白子
data[i][j] = 9; //○
else if (i == 0 && j == 0)
data[i][j] = 218; //┏
else if (i == 0 && j == CHECKBROADSIZE - 1)
data[i][j] = 191; //┓
else if (i == CHECKBROADSIZE - 1 && j == 0)
data[i][j] = 192; //┗
else if (i == CHECKBROADSIZE - 1 && j == CHECKBROADSIZE - 1)
data[i][j] = 217; //┛
else if (i == 0)
data[i][j] = 194; //┳
else if (i == CHECKBROADSIZE - 1)
data[i][j] = 193; //┻
else if (j == 0)
data[i][j] = 195; //┣
else if (j == CHECKBROADSIZE - 1)
data[i][j] = 180; //┫
else
data[i][j] = 197; //┼
}
cout << endl;
}
//实现双缓冲
//用指针实现指向两个缓冲区
zhixiang_hOutput = !zhixiang_hOutput;
if (!zhixiang_hOutput)
{
houtpoint = &hOutput;
}
else
{
houtpoint = &hOutBuf;
}
coord.Y = 1; //起始坐标
//以下分别输出三个Tips
WriteConsoleOutputCharacterA(*houtpoint, Tips, strlen(Tips), coord, &bytes); // 在指定位置处插入指定数量的字符
coord.Y++; //换一行显示
WriteConsoleOutputCharacterA(*houtpoint, Tips2, strlen(Tips2), coord, &bytes);
coord.Y++;
WriteConsoleOutputCharacterA(*houtpoint, Tips1, strlen(Tips1), coord, &bytes);
coord.Y++;
WriteConsoleOutputCharacterA(*houtpoint, Tips3, strlen(Tips3), coord, &bytes);
coord.Y++;
for (int i = 0; i < CHECKBROADSIZE; i++) //绘制棋盘的符号
{
coord.Y++;
WriteConsoleOutputCharacterA(*houtpoint, (char *)data[i], CHECKBROADSIZE, coord, &bytes);
}
//显示到哪方下
sprintf(score, "current player: %d Tips: 1:square 2:circle ", g_currentGamer);
coord.Y++; //换一行
WriteConsoleOutputCharacterA(*houtpoint, score, strlen(score), coord, &bytes);
SetConsoleActiveScreenBuffer(*houtpoint);
}
void Rungame::buffer()
{
//实现双缓冲
SetConsoleOutputCP(437);//改成标准英文输出标志,这样可以使用扩展ASCII码表,以方便后面游戏界面的制作
//创建新的控制台缓冲区
hOutBuf = CreateConsoleScreenBuffer(
GENERIC_WRITE, //定义进程可以往缓冲区写数据
FILE_SHARE_WRITE, //定义缓冲区可共享写权限
NULL,
CONSOLE_TEXTMODE_BUFFER,
NULL
);
hOutput = CreateConsoleScreenBuffer(
GENERIC_WRITE, //定义进程可以往缓冲区写数据
FILE_SHARE_WRITE, //定义缓冲区可共享写权限
NULL,
CONSOLE_TEXTMODE_BUFFER,
NULL
);
//隐藏两个缓冲区的光标
CONSOLE_CURSOR_INFO cci;
cci.bVisible = 0;
cci.dwSize = 1;
SetConsoleCursorInfo(hOutput, &cci);
SetConsoleCursorInfo(hOutBuf, &cci);
}
int Rungame::Put() //下子成功 成功返回1 失败返回0
{
//怎么才是下子成功,而且改下那颗子
if (g_checkboard[g_cursorX][g_cursorY] == 0) //下子成功
{
g_checkboard[g_cursorX][g_cursorY] = g_currentGamer;
return 1;
}
else
return 0;
}
int Rungame::Check() //判断输赢
{
int nHorizontal = 1; //水平方向
int nVertical = 1; //垂直方向
int nPostiveDirection = 1; //正斜向
int nSkewDirection = 1; //反斜向
//水平 向右检查
//g_cursorY 列的变化
for (int i = 1; i < 5; i++)
{
if (g_cursorY + i < CHECKBROADSIZE && g_checkboard[g_cursorX][g_cursorY + i] == g_currentGamer)
nHorizontal++;
else
break;
}
//水平 向左检查
for (int i = 1; i < 5; i++)
{
if (g_cursorY - i > 0 && g_checkboard[g_cursorX][g_cursorY - i] == g_currentGamer)
nHorizontal++;
else
break;
}
if (nHorizontal >= 5)
{
g_cursorY += 1;
return g_currentGamer;
}
//垂直 向下检查
for (int i = 1; i < 5; i++)
{
if (g_cursorX + i < CHECKBROADSIZE && g_checkboard[g_cursorX + i][g_cursorY] == g_currentGamer)
nVertical++;
else
break;
}
// 向上检查
for (int i = 1; i < 5; i++)
{
if (g_cursorX - i > 0 && g_checkboard[g_cursorX - i][g_cursorY] == g_currentGamer)
nVertical++;
else
break;
}
if (nVertical >= 5)
{
g_cursorY += 1;
return g_currentGamer;
}
//正斜向上 检查
for (int i = 1; i < 5; i++)
{
if (g_cursorX - i > 0 && g_cursorY + i < CHECKBROADSIZE && g_checkboard[g_cursorX - i][g_cursorY + i] == g_currentGamer)
nPostiveDirection++;
else
break;
}
//正斜向下检查
for (int i = 1; i < 5; i++)
{
if (g_cursorY - i > 0 && g_cursorX + i < CHECKBROADSIZE && g_checkboard[g_cursorX + i][g_cursorY - i] == g_currentGamer)
nPostiveDirection++;
else
break;
}
if (nPostiveDirection >= 5)
{
g_cursorY += 1;
return g_currentGamer;
}
//反斜向下检查
for (int i = 1; i < 5; i++)
{
if (g_cursorX + i < CHECKBROADSIZE && g_cursorY + i < CHECKBROADSIZE && g_checkboard[g_cursorX + i][g_cursorY + i] == g_currentGamer)
nSkewDirection++;
else
break;
}
//正斜向上检查
for (int i = 1; i < 5; i++)
{
if (g_cursorX - i > 0 && g_cursorY - i > 0 && g_checkboard[g_cursorX - i][g_cursorY - i] == g_currentGamer)
nSkewDirection++;
else
break;
}
if (nSkewDirection >= 5)
{
g_cursorY += 1;
return g_currentGamer;
}
return 0;
}
效果图(放不了视频,就放张图片吧):
悔棋功能实现要修改的地方(在源代码中修改这些地方即可)
1.加入头文件 #include
2.在Rungame类中添加记录X,Y坐标的栈变量和记录悔棋次数的变量
3.修改RunGame()函数
a.每一次下棋都将坐标压栈,下一次棋就重置Retract_Time,这样在每一次下棋之后都可以悔一次棋
b.添加悔棋键(r)
也可以自己定义悔棋次数,修改if中的Retract_Time的限制条件即可
效果: