C++华容道

  1. 1. 引言
  2. 2. 代码目录
  3. 3. color.h
  4. 4. piece.h
  5. 5. print.h
  6. 6. control.h
  7. 7. main.cpp
  8. 8. 历程
  9. 9. 下载
  10. 10. 写在最后

引言

  快乐的暑假就需要折腾来充实自己,折腾的第一项是自己写一些游戏,第一个project选择了华容道。

华容道
(中国民间智力游戏)

  华容道是古老的中国民间益智游戏,以其变化多端、百玩不厌的特点与魔方、独立钻石棋一起被国外智力专家并称为“智力游戏界的三个不可思议”。它与七巧板、九连环等中国传统益智玩具还有个代名词叫作“中国的难题”。据《资治通鉴》注释中说“从此道可至华容也”。华容道原是中国古代的一个地名,相传当年曹操曾经败走此地。由于当时的华容道是一片沼泽,所以曹操大军要割草填地,不少士兵更惨被活埋,惨烈非常。

  通过移动各个棋子,帮助曹操从初始位置移到棋盘最下方中部,从出口逃走。不允许跨越棋子,还要设法用最少的步数把曹操移到出口。曹操逃出华容道的最大障碍是关羽,关羽立马华容道,一夫当关,万夫莫开。关羽与曹操当然是解开这一游戏的关键。四个刘备军兵是最灵活的,也最容易对付,如何发挥他们的作用也要充分考虑周全。“华容道”有一个带二十个小方格的棋盘,代表华容道。

--来源:百度百科

  详见 华容道-百度百科

  初步考虑使用打印字符来代表棋子。使用getch()函数来获取键盘输入,然后通过算法得出相应的棋子坐标变化,并重新打印。
  游戏中采用 wasd 进行移动,空格键切换选择状态。当状态为未锁定时,wasd 为切换人物;当状态为锁定时,wasd 为移动人物。

代码目录

1
2
3
4
5
6
7
8
9
10
11
12
|-- include
|-- color.h //提供控制台打印颜色接口
#include <windows.h>
|-- piece.h //定义棋盘及棋子信息
|-- print.h //提供打印接口
#include "color.h"
#include "piece.h"
|-- control.h //读取键盘输入 控制中心
#include "print.h"
|-- src
|-- main.cpp
#include "control.h"

color.h

  此段代码对输出流进行了重载,可以通过调用函数 cout << red 直接将控制台输出颜色转变为红色,其他颜色同理。
  代码来源于CSDN,感谢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#ifndef COLOR_H
#define COLOR_H

#include <iostream>
#include <windows.h>


inline std::ostream& blue(std::ostream &s)
{
HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hStdout, FOREGROUND_BLUE
|FOREGROUND_GREEN|FOREGROUND_INTENSITY);
return s;
}

inline std::ostream& red(std::ostream &s)
{
HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hStdout,
FOREGROUND_RED|FOREGROUND_INTENSITY);
return s;
}

inline std::ostream& green(std::ostream &s)
{
HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hStdout,
FOREGROUND_GREEN|FOREGROUND_INTENSITY);
return s;
}

inline std::ostream& yellow(std::ostream &s)
{
HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hStdout,
FOREGROUND_GREEN|FOREGROUND_RED|FOREGROUND_INTENSITY);
return s;
}

inline std::ostream& white(std::ostream &s)
{
HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hStdout,
FOREGROUND_RED|FOREGROUND_GREEN|FOREGROUND_BLUE);
return s;
}

struct color {
color(WORD attribute):m_color(attribute){};
WORD m_color;
};

template <class _Elem, class _Traits>
std::basic_ostream<_Elem,_Traits>&
operator<<(std::basic_ostream<_Elem,_Traits>& i, color& c)
{
HANDLE hStdout=GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hStdout,c.m_color);
return i;
}

#endif

piece.h

  首先定义一个piece类,表示棋子。
  其含有变量lxlyrxry,分别表示棋子的左上角横纵坐标以及右下角横纵坐标;变量status表示棋子的状态,0表示未选中状态,1表示选中状态,2表示锁定状态。代码如下。

1
2
3
4
typedef struct piece{
int lx, ly, rx, ry;
int status;
}piece;

  2.0版本时加入了多个关卡,主要区别是棋子的初始位置不同。因此定义一个chapter类,表示一个关卡。变量name储存关卡名字,变量status表示选中/未选中状态。代码如下。

1
2
3
4
5
6
#include <string> //需包含该头文件以支持string的使用

typedef struct chapter{
string name;
bool status;
}chapter;

  main.cpp 中的 main 函数定义了 chapter a[12]piece hrd[10],分别表示12个关卡和10枚棋子,并在各个函数中以指针形式进行通讯。

  1.0版本选择的关卡是横刀立马,各棋子初状态如下:
初始地图

  于是定义各棋子的编号如下:

棋子 张飞 曹操 马超 黄忠 关羽 赵云 兵1 兵2 兵3 兵4
编号 0 1 2 3 4 5 6 7 8 9

  我们定义5×4棋盘中左上角的坐标为(0,0),以此得出棋盘上各位置的坐标。
  我们初定提供12个关卡,按照各关卡各棋子的初始坐标,我们可以按照如下代码定义两个结构体的初始化函数。其中 coordinatexcoordinatey 函数初始化的值均为左上角坐标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
void initialChapter(chapter *a){
a[0].name = "横刀立马"; a[0].status = true;
a[1].name = "指挥若定"; a[1].status = false;
a[2].name = "将拥曹营"; a[2].status = false;
a[3].name = "齐头并进"; a[3].status = false;
a[4].name = "兵分三路"; a[4].status = false;
a[5].name = "屯兵东路"; a[5].status = false;
a[6].name = "左右布兵"; a[6].status = false;
a[7].name = "桃花园中"; a[7].status = false;
a[8].name = "一路进军"; a[8].status = false;
a[9].name = "一路顺风"; a[9].status = false;
a[10].name = "四面楚歌"; a[10].status = false;
a[11].name = "兵临曹营"; a[11].status = false;
}
void initialPiece(piece *hrd, int result){
int sizeofpiecex[10] = {1, 2, 1, 1, 2, 1, 1, 1, 1, 1}; //表示各棋子的宽
int sizeofpiecey[10] = {2, 2, 2, 2, 1, 2, 1, 1, 1, 1}; //表示各棋子的高

int coordinatex[12][10] = { {0, 1, 3, 0, 1, 3, 0, 1, 2, 3}, //横刀立马
{0, 1, 3, 0, 1, 3, 0, 1, 2, 3}, //指挥若定
{0, 1, 3, 1, 0, 2, 0, 2, 3, 3}, //将拥曹营
{0, 1, 3, 0, 1, 3, 0, 1, 2, 3}, //齐头并进
{0, 1, 3, 0, 1, 3, 0, 1, 2, 3}, //兵分三路
{2, 0, 3, 0, 0, 1, 2, 2, 3, 3}, //屯兵东路
{0, 1, 1, 2, 1, 3, 0, 0, 3, 3}, //左右布兵
{0, 1, 3, 1, 1, 2, 0, 0, 3, 3}, //桃花园中
{0, 1, 0, 1, 1, 2, 3, 3, 3, 3}, //一路进军
{0, 1, 0, 2, 1, 3, 1, 1, 3, 3}, //一路顺风
{0, 1, 3, 0, 1, 3, 0, 1, 2, 3}, //四面楚歌
{0, 1, 3, 1, 1, 2, 0, 0, 3, 3} }; //兵临曹营

int coordinatey[12][10] = { {0, 0, 0, 2, 2, 2, 4, 3, 3, 4}, //横刀立马
{0, 0, 0, 3, 2, 3, 2, 3, 3, 2}, //指挥若定
{1, 0, 1, 2, 4, 2, 3, 4, 3, 4}, //将拥曹营
{0, 0, 0, 3, 3, 3, 2, 2, 2, 2}, //齐头并进
{1, 0, 1, 3, 2, 3, 0, 3, 3, 0}, //兵分三路
{0, 0, 0, 3, 2, 3, 2, 3, 2, 3}, //屯兵东路
{2, 0, 2, 2, 4, 2, 0, 1, 0, 1}, //左右布兵
{1, 0, 1, 2, 4, 2, 0, 3, 0, 3}, //桃花园中
{0, 0, 2, 2, 4, 2, 0, 1, 2, 3}, //一路进军
{0, 0, 2, 3, 2, 2, 3, 4, 0, 1}, //一路顺风
{0, 1, 0, 3, 3, 2, 2, 0, 0, 4}, //四面楚歌
{2, 0, 2, 3, 2, 3, 0, 1, 0, 1} }; //兵临曹营

for(int i = 0; i < 10; i++){
hrd[i].lx = X + 2 + 6 * coordinatex[result][i];
hrd[i].ly = Y + 1 + 3 * coordinatey[result][i];
hrd[i].rx = X + 2 + 6 * (coordinatex[result][i] + sizeofpiecex[i] - 1);
hrd[i].ry = Y + 1 + 3 * (coordinatey[result][i] + sizeofpiecey[i] - 1);
hrd[i].status = (i == 1)? 1 : 0;
}
return;
}

  我们设定初始选择的关卡是横刀立马,于是其变量status的值为true,初始状态的选择在曹操身上,于是hrd[1].status的值为1
  值得一提的是,变量lxlyrxry存放的是在控制台打印的实际坐标,我们在控制打印棋盘的单位格子大小为3×6,于是可以看到四个变量初始化的代码块进行了相应的坐标换算。XY表示棋盘即墙体的初始打印位置(左上角),以方便棋盘的整体移动,为后续调整棋盘在控制台的位置提供了较大的便利。在这里我们可以直接在文件头部进行define,后续需移动整个棋盘时修改XY的值即可。

1
2
#define X 5
#define Y 5

  需包含的头文件如下,同时仍然对XY进行定义。

1
2
3
4
5
6
7
8
#include <iostream>
#include <cstring>
#include <windows.h>
#include "../include/color.h"
#include "../include/piece.h"

#define X 5
#define Y 5

  首先我们有函数gotoxy,可以将运行窗口的光标移动到(x,y)的位置。调用此函数之后再进行打印可以实现在需要的位置打印。其中x为横向(列数),y为纵向(行数)

1
2
3
4
5
void gotoxy(int x, int y) {
COORD pos = {x,y};
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); //获取标准输出设备句柄
SetConsoleCursorPosition(hOut, pos); //两个参数分别是指定哪个窗体,具体位置
}

  该头文件的函数一览:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void printTimes(string str, int n); //打印一个字符串n次,方便对某个字符串打印次数进行调节
void changeColor(int color); //专门用于改变控制台的输出颜色,方便对3种状态对应的颜色进行更改

void printSelectionScreen(chapter *a); //打印关卡选择界面
void printchapter(chapter *a, int index); //打印关卡选择中的各关卡方框

void printMap(piece *hrd, int result, string name); //打印选择关卡后的界面
void printWall(); //打印棋盘墙体及右边提示信息

//color: 0 is unselected, 1 is selecting, 2 is selected
void printpiece(int x, int y, int index, int color); //print piece hrd[index] 打印棋子

void printZF(int x, int y); //print Zhang Fei
void printCC(int x, int y); //print Cao Cao
void printMC(int x, int y); //print Ma Cao
void printHZ(int x, int y); //print Huang Zhong
void printGY(int x, int y); //print Guan Yu
void printZY(int x, int y); //print Zhao Yun
void printXB(int x, int y); //print soldier

void erasepiece(int x, int y, int index); //erase piece hrd[index] 擦除棋子

void erase12(int x, int y); //erase 1*2 peice
void erase22(int x, int y); //erase 2*2 peice
void erase21(int x, int y); //erase 2*1 peice
void erase11(int x, int y); //erase 1*1 peice

  函数 printTimeschangeColor 代码如下。设定未选择状态为白色,选择状态为蓝色,锁定状态为红色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//print a string for n times
void printTimes(string str, int n){
for(int i = 0; i < n; i++){
cout << str;
}
return;
}

//change the print color
void changeColor(int color){
switch(color){
case 0: cout << white; break;
case 1: cout << blue; break;
case 2: cout << red; break;
default: break;
}
return;
}

  函数 printSelectionScreenprintchapter 代码如下。初始化关卡选择并进行打印。打印位置通过不断运行观察调整得到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void printSelectionScreen(chapter *a){
system("cls");
changeColor(0);
gotoxy(X+22, Y-4);
cout << "华容道";

initialChapter(a);
for(int i = 0; i < 12; i++){
printchapter(a, i);
}

gotoxy(X+20, Y+17);
cout << "W ↑";
gotoxy(X+18, Y+18);
cout << "A S D ←↓→";
gotoxy(X+18, Y+20);
cout << "space 选择关卡";
gotoxy(X+45, Y+21);
cout << "by Ender";

return;
}
//按照6×2进行分布,因此 x = a(index % 2) + c,y = a(index / 2) + c 。
//关卡选择确定直接按space,因此只有未选中和选中两种状态,采用白色和红色区分。
void printchapter(chapter *a, int index){
if(a[index].status == true) changeColor(2); //当前选择关卡采用红色打印
else changeColor(0); //当前未选中关卡采用白色打印
gotoxy(X+11+index%2*16, Y+index/2*3-2);
cout << "┌─────────┐";
gotoxy(X+11+index%2*16, Y+index/2*3-1);
cout << "│ " << a[index].name << "│";
gotoxy(X+11+index%2*16, Y+index/2*3);
cout << "└─────────┘";
changeColor(0);
return;
}

  打印效果如图
选择界面

  函数 printMapprintWall 代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
void printMap(piece *hrd, int result, string name){
system("cls");
printWall(); //打印各关卡中不变的外墙体及提示部分

gotoxy(X+21, Y-2);
cout << name; //打印关卡名

//打印各棋子
initialPiece(hrd, result); //result为选中关卡的编号
for(int i = 0; i < 10; i++){
printpiece(hrd[i].lx, hrd[i].ly, i, hrd[i].status);
}

return;
}
void printWall(){
changeColor(0);
gotoxy(X+22, Y-4);
cout << "华容道";
gotoxy(X+36, Y+3);
cout << "W ↑";
gotoxy(X+34, Y+4);
cout << "A S D ←↓→";
gotoxy(X+34, Y+6);
cout << "space 锁定/解锁";
gotoxy(X+36, Y+8);
cout << "R 重新开始";
gotoxy(X+36, Y+10);
cout << "P 回到主界面";
gotoxy(X+36, Y+12);
changeColor(2);
cout << "■ 锁定状态";
gotoxy(X+36, Y+14);
changeColor(1);
cout << "■ 选择状态";
gotoxy(X+45, Y+21);
changeColor(0);
cout << "by Ender";
gotoxy(X, Y);
printTimes("■", 14);
for(int i = 1; i <= 15; i++){
gotoxy(X, Y+i);
cout << "■";
printTimes(" ", 12);
cout << "■";
}
gotoxy(X, Y+16);
printTimes("■", 4);
printTimes(" ", 6);
printTimes("■", 4);

return;
}

  printWall 函数打印墙体效果如图。
打印墙体

  函数 printpiece 代码如下。提供了打印各棋子的总接口。选择打印颜色也在此处完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void printpiece(int x, int y, int index, int color){
changeColor(color);
switch(index){
case 0: printZF(x, y); break;
case 1: printCC(x, y); break;
case 2: printMC(x, y); break;
case 3: printHZ(x, y); break;
case 4: printGY(x, y); break;
case 5: printZY(x, y); break;
case 6: case 7: case 8: case 9: printXB(x, y); break;
default: break;
}
changeColor(0);
return;
}

  函数 printZF 代码如下。在(x,y)处开始打印棋子,一行一行打印。(x,y)为左上角坐标。此处提供一个打印示例,其他各棋子的打印函数也大抵如此。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void printZF(int x, int y){
gotoxy(x, y);
cout << "┌───┐";
gotoxy(x, y+1);
cout << "│ │";
gotoxy(x, y+2);
cout << "│ 张│";
gotoxy(x, y+3);
cout << "│ 飞│";
gotoxy(x, y+4);
cout << "│ │";
gotoxy(x, y+5);
cout << "└───┘";
return;
}

  打印棋子初始状态及关卡名称后的效果如图。
初始状态

  函数 erasepiece 代码如下。提供了擦除各型号大小棋子的总接口。不同棋子对应各自大小的擦除函数。

1
2
3
4
5
6
7
8
9
10
void erasepiece(int x, int y, int index){
switch(index){
case 0: case 2: case 3: case 5: erase12(x, y); break; //张飞、马超、黄忠、赵云 1×2
case 1: erase22(x, y); break; //曹操 2×2
case 4: erase21(x, y); break; //关羽 2×1
case 6: case 7: case 8: case 9: erase11(x, y); break; //兵 1×1
default: break;
}
}

  示例擦除函数代码如下。在对应位置打印空格即可实现棋子的擦除。注意每格大小3×6(高×宽)

1
2
3
4
5
6
7
void erase12(int x, int y){
for(int i = 0; i < 6; i++){
gotoxy(x, y+i);
printTimes(" ", 6);
}
return;
}

  至此,print.h 头文件已经完成。

control.h

  需包含的头文件有 conio.hprint.h 。其中 conio.h 头文件用于获取键盘输入。仍然对 XY 进行宏定义。
  该头文件所包含的函数一览:

1
2
3
4
5
6
7
8
9
10
11
12
int choosecontrol(chapter *a); //choose the chapter 选择关卡的总控

bool click(piece *hrd); //contain with main.cpp 与主函数对接(包含有循环调用clickcontrol)
int clickcontrol(piece *hrd); //deal with input 处理每一次输入

bool movejudge(piece judge, piece i); //Judging whether or not to move 判断能否移动

void space(piece *hrd); //Switch lock and unlock States 输入空格相应操作
void up(piece *hrd); //Press 'up' 输入w相应操作
bool down(piece *hrd); //Press 'down' 输入s相应操作
void left(piece *hrd); //Press 'left' 输入a相应操作
void right(piece *hrd); //Press 'right' 输入d相应操作

  函数 choosecontrol 的代码如下。返回值为选择的关卡编号。可以利用此值进行棋盘初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int choosecontrol(chapter *a){
int index = 0; //初始化的选中关卡为编号为0的横刀立马
char ch;
do{
ch = getch();
switch(ch){
case 'w':
a[index].status = false;
printchapter(a, index);
if(index >= 2) index -= 2;
else index += 10;
a[index].status = true;
printchapter(a, index);
break;
case 's':
a[index].status = false;
printchapter(a, index);
if(index <= 9) index += 2;
else index -= 10;
a[index].status = true;
printchapter(a, index);
break;
case 'a':
case 'd':
a[index].status = false;
printchapter(a, index);
if(index % 2 == 1) index -= 1;
else index += 1;
a[index].status = true;
printchapter(a, index);
break;
default: break;
}
}while(ch != ' '); //不断循环后,检测到空格输入即选择关卡确定时,退出循环,返回当前选中关卡编号。
return index;
}

  getch()函数为获取一次键盘输入。当输入wasd时,我们需要改变相应的关卡。以 case 'w' 为例。

1
2
3
4
5
6
7
8
case 'w':
a[index].status = false; //当前被选中关卡变换为未选中状态
printchapter(a, index); //重新打印该关卡选择框,使其变化为白色
if(index >= 2) index -= 2;
else index += 10; //更新index(当前选中关卡)
a[index].status = true; //将更新后的选中关卡变换为选中状态
printchapter(a, index); //重新打印该关卡选择框,使其变化为红色
break;

  获取输入后我们对关卡状态进行更新,并且重新打印,体现在了游戏界面上。

  函数 clickcontrol 代码如下。获取键盘输入并调用相应函数,作为总控制中心。
  返回值为 10000 时表示回到选择关卡界面,返回值为 10001 时表示当前关卡重新开始,返回值为 1 表示继续进行游戏。值得一提的是,down 函数是有 bool 返回值的,因为游戏胜利的最后一步一定是曹操从下方缺口逃出。因此 down 函数的返回值表示当前关卡通过与否。若通关返回 true,相应地 clickcontrol 函数返回 10086

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int clickcontrol(piece *hrd){
char ch;
ch = getch();
switch(ch){
case 'w': up(hrd); break; //up
case 's': if(down(hrd)) return 10086; break; //down
case 'a': left(hrd); break; //left
case 'd': right(hrd); break; //right
case ' ': space(hrd); break; //switch
case 'p': return 10000; //go back to home
case 'r': return 10001; //restart
default: break;
}
return 1;
}

  函数 click 代码如下。通过循环不断调用 clickcontrol 函数,若需回到主界面,则返回 true (通关也是返回主界面),若需当前关卡重新开始,则返回 false 。当 clickcontrol 返回 10086 即关卡胜利时,我们可以知道游戏的最后一步一定是曹操出现在缺口上,于是可以重新打印曹操使曹操“越过棋盘”,并打印游戏通关恭喜字句,同时使用 getchar() 函数使游戏暂停。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bool click(piece *hrd){
int result;
do{
result = clickcontrol(hrd);
if(result == 10000) return true;
if(result == 10001) return false;
}while(result != 10086);
erasepiece(X+8, Y+10, 1);
printpiece(X+8, Y+13, 1, 2);
gotoxy(X+7, Y+20);
cout << "恭喜您通过本关!";
gotoxy(X+4, Y+22);
cout << "请按回车键回到主界面";
getchar();
return true;
}

  游戏通关效果如图。
游戏通关

  函数 space 代码如下。使用循环检测10枚棋子中处于选择/锁定状态的,切换其状态,并对其重新进行打印(颜色改变)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void space(piece *hrd){
int index;
for(int i = 0; i < 10; i++){
if(hrd[i].status == 1){
index = i;
hrd[i].status = 2;
break;
}
else if(hrd[i].status == 2){
index = i;
hrd[i].status = 1;
break;
}
}
printpiece(hrd[index].lx, hrd[index].ly, index, hrd[index].status);
return;
}

  函数 movejudge 代码如下。我们定义棋子的时候记录了其左上角及右下角。当棋子空间 judge 不与棋子 i 重合时,我们返回 true 。由于棋子占领的空间是矩形,因此未重合时,应该至少两个棋子占领的 x 范围或者 y 范围没有交集。

1
2
3
4
bool movejudge(piece judge, piece i){
if(judge.rx < i.lx || judge.lx > i.rx || judge.ry < i.ly || judge.ly > i.ry) return true;
else return false;
}

  接下来看看 up 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
void up(piece *hrd){
int index; //记录当前操作棋子编号
bool locked; //记录当前棋子是否锁定
for(int i = 0; i < 10; i++){
if(hrd[i].status == 1 || hrd[i].status == 2){
index = i;
locked = (hrd[i].status == 2)? true : false;
break;
}
} //找出当前被选中的棋子,以及其是否锁定
if(hrd[index].ly == Y + 1) return; //若当前被选中棋子在顶端格子,则up操作没有意义,直接返回

if(locked){ //若棋子是锁定状态
bool can = true; //记录棋子能否进行up移动,初始赋值为可移动
piece judge = hrd[index];
judge.ly -= 3; //此两行代码表示当前棋子进行up移动所需的空间(棋子本身和其上面一行的空间)
for(int i = 0; i < 10; i++){
if(index != i){
if(!movejudge(judge, hrd[i])) can = false;
}
} //对除了此棋子之外的其他所有棋子进行可移动判断,若有任一不符合则不可移动

if(can){
erasepiece(hrd[index].lx, hrd[index].ly, index);
hrd[index].ly -= 3;
hrd[index].ry -= 3;
printpiece(hrd[index].lx, hrd[index].ly, index, hrd[index].status);
} //如果可移动,则擦除棋子,更新棋子移动后的状态,重新打印棋子。
}
else{ //若棋子是选择状态
bool content[10]; //记录满足up条件的其他棋子
int num = 0; //记录满足up条件的棋子数
for(int i = 0; i < 10; i++){
if(hrd[i].ry < hrd[index].ly) //满足当前棋子在选中棋子的严格上方
if((hrd[i].lx >= hrd[index].lx && hrd[i].lx <= hrd[index].rx) || (hrd[i].rx >= hrd[index].lx && hrd[i].rx <= hrd[index].rx)){ //满足当前棋子在选中棋子占领的列空间里有重合
content[i] = true;
num++;
}
else content[i] = false;
else content[i] = false;
}
if(num == 0) return; //若没有满足以上条件的棋子,则直接返回,up操作不生效
else if(num == 1){ //若满足条件的棋子唯一
for(int i = 0; i < 10; i++){
if(content[i] == true){
hrd[index].status = 0;
printpiece(hrd[index].lx, hrd[index].ly, index, hrd[index].status);
hrd[i].status = 1;
printpiece(hrd[i].lx, hrd[i].ly, i, hrd[i].status);
return;
}
} //找到满足条件的棋子并改变当前选中的棋子编号,对两枚棋子重新进行打印
}
else{ //若满足条件的棋子大于一个
int max = Y;
for(int i = 0; i < 10; i++){
if(content[i] == true)
if(hrd[i].ry > max) max = hrd[i].ry;
} //找出最靠近的那枚棋子(严格在其上方,又是满足条件里最下方的棋子)
for(int i = 0; i < 10; i++){
if(content[i] == true) //直接选择满足以上所有条件的第一枚棋子
if(hrd[i].ry == max){
hrd[index].status = 0;
printpiece(hrd[index].lx, hrd[index].ly, index, hrd[index].status);
hrd[i].status = 1;
printpiece(hrd[i].lx, hrd[i].ly, i, hrd[i].status);
return;
}
}
}
}
return;
}

  可以看到,核心难点即在于对于不同大小的棋子,我们该如何在切换棋子的时候,选择到尽可能符合认知的那枚棋子,而且还要保证所有的棋子都一定能被选择到。该程序还有不足之处便是只能选择严格在其正上方最靠近的棋子,若仍然不止一枚的话只会打印编号靠前的第一枚。如以下这种情况,关羽进行up操作,只会选择两个中编号靠前的那一个。

关羽

  其他的 downleftright 函数基本如上,只需改变相应的变量名和一些常量值,这里便不再赘述。
  此外,down 函数中增加了一行代码如下。当曹操到达这个位置(缺口上方)之后按下down时,曹操逃脱,游戏胜利,返回 true 。注意此行代码必须在处于低端格子直接返回之前,因为此时的曹操也同样是处于低端格子。

1
if(hrd[1].lx == X + 8 && hrd[1].ly == Y + 10) return true;

  至此,control.h 完成。

main.cpp

  main.cpp 的代码如下。上面已经说到,click 的返回值为 true 时返回选择界面,返回值为 false 时重新打印关卡。在主函数中得到了体现。另外,主函数的另一个功能是声明了关卡和棋子,并以指针形式传入各函数进行相应操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <windows.h>
#include "../include/control.h"

using namespace std;
int main(){

system("cls"); //清除整个屏幕
system("title 华容道"); //定义运行窗口的标题为“华容道”
system("mode con cols=60 lines=28"); //定义运行窗口的大小(宽×高)

chapter a[12];
piece hrd[10];

reselect: //重新选择
printSelectionScreen(a);
int result = choosecontrol(a);

restart: //重新开始当前关卡
printMap(hrd, result, a[result].name);

if(click(hrd)) goto reselect;
else goto restart;
return 0;
}

  至此便完成了所有的代码。

历程

  • v 0.5  2019-7-20
    还没有想好用 wasd 进行切换人物的算法,于是先写出了一个用 space 换人,wasd 移动的版本,由于人物有10个之多,用 space 按照编号顺序切换人物显得非常僵硬,于是不算为第一个版本,算是一个未完成品吧。

  • v 1.0  2019-7-21
    实现了 wasd 切换人物,空格切换选择/锁定状态,大大提高游戏操作的流畅性。

  • v 2.0  2019-7-22
    加入了不同的关卡,因此对整套代码进行重构,使用较易理解的5×4坐标,便于后续关卡的加入。

  • win7版  2019-7-23
    发现win7控制台打印的字符大小与win10不同,字符打印的方框没有重合。修改了打印函数即解决问题。

  • Future

    • 考虑可以锁定时直接移动。因为移动的选择只有一个或两个。
    • 五虎将只需满足大小为1×2,横竖并不是严格定义的。重构定义代码才能加入更多的关卡。
    • 学习鼠标捕捉并采用鼠标操作。
    • 重构后加入界面

下载

  采用photoshop为游戏制作了图标,文件已存放在 这里 。可自行下载并添加到工程中。

  所有代码已经存放到 https://github.com/Ender-coder/Klotski ,可自行下载查看。

写在最后

  很开心,考试月萌生的暑假要好好学习的想法在刚考完试浪完之后还是能开始实施(寒假的时候也有想法但是最终浪过去了)。写完这个项目的代码之后决定要写成博客于是又挖了新坑。花了好几天部署了博客,当然要好好感谢坎爷,基本是克隆了坎爷的博客然后再进行修改的。然后书写这个博客又花了两天,写出来和讲出来的区别还是很大的啊,markdown也是以前没怎么接触(其实以前在matrix上写题目说明用的就是markdown的格式,但是只写过一点点。而且当时也不知道那是什么),书写的确花了不少时间。不过经过这些天来的摸索以后应该就是轻车熟路了。
  回头想想,这几天接触学习了好多新东西啊。在路上总是好事。