引言
快乐的暑假就需要折腾来充实自己,折腾的第一项是自己写一些游戏,第一个project选择了华容道。
华容道 (中国民间智力游戏) 华容道是古老的中国民间益智游戏,以其变化多端、百玩不厌的特点与魔方、独立钻石棋一起被国外智力专家并称为“智力游戏界的三个不可思议”。它与七巧板、九连环等中国传统益智玩具还有个代名词叫作“中国的难题”。据《资治通鉴》注释中说“从此道可至华容也”。华容道原是中国古代的一个地名,相传当年曹操曾经败走此地。由于当时的华容道是一片沼泽,所以曹操大军要割草填地,不少士兵更惨被活埋,惨烈非常。
通过移动各个棋子,帮助曹操从初始位置移到棋盘最下方中部,从出口逃走。不允许跨越棋子,还要设法用最少的步数把曹操移到出口。曹操逃出华容道的最大障碍是关羽,关羽立马华容道,一夫当关,万夫莫开。关羽与曹操当然是解开这一游戏的关键。四个刘备军兵是最灵活的,也最容易对付,如何发挥他们的作用也要充分考虑周全。“华容道”有一个带二十个小方格的棋盘,代表华容道。
--来源:百度百科
详见 华容道-百度百科
初步考虑使用打印字符来代表棋子。使用getch()函数来获取键盘输入,然后通过算法得出相应的棋子坐标变化,并重新打印。
游戏中采用 wasd 进行移动,空格键切换选择状态。当状态为未锁定时,wasd 为切换人物;当状态为锁定时,wasd 为移动人物。
代码目录
1 | |-- include |
color.h
此段代码对输出流进行了重载,可以通过调用函数 cout << red
直接将控制台输出颜色转变为红色,其他颜色同理。
代码来源于CSDN,感谢。
1 |
|
piece.h
首先定义一个piece
类,表示棋子。
其含有变量lx
、ly
、rx
、ry
,分别表示棋子的左上角横纵坐标以及右下角横纵坐标;变量status
表示棋子的状态,0表示未选中状态,1表示选中状态,2表示锁定状态。代码如下。
1 | typedef struct piece{ |
2.0版本时加入了多个关卡,主要区别是棋子的初始位置不同。因此定义一个chapter
类,表示一个关卡。变量name
储存关卡名字,变量status
表示选中/未选中状态。代码如下。
1 |
|
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个关卡,按照各关卡各棋子的初始坐标,我们可以按照如下代码定义两个结构体的初始化函数。其中 coordinatex
、coordinatey
函数初始化的值均为左上角坐标。
1 | void initialChapter(chapter *a){ |
我们设定初始选择的关卡是横刀立马,于是其变量status
的值为true
,初始状态的选择在曹操身上,于是hrd[1].status
的值为1
。
值得一提的是,变量lx
、ly
、rx
、ry
存放的是在控制台打印的实际坐标,我们在控制打印棋盘的单位格子大小为3×6,于是可以看到四个变量初始化的代码块进行了相应的坐标换算。X
、Y
表示棋盘即墙体的初始打印位置(左上角),以方便棋盘的整体移动,为后续调整棋盘在控制台的位置提供了较大的便利。在这里我们可以直接在文件头部进行define
,后续需移动整个棋盘时修改X
、Y
的值即可。
1 |
print.h
需包含的头文件如下,同时仍然对X
、Y
进行定义。
1 |
首先我们有函数gotoxy
,可以将运行窗口的光标移动到(x,y)的位置。调用此函数之后再进行打印可以实现在需要的位置打印。其中x为横向(列数),y为纵向(行数)
1 | void gotoxy(int x, int y) { |
该头文件的函数一览:
1 | void printTimes(string str, int n); //打印一个字符串n次,方便对某个字符串打印次数进行调节 |
函数 printTimes
、changeColor
代码如下。设定未选择状态为白色,选择状态为蓝色,锁定状态为红色。
1 | //print a string for n times |
函数 printSelectionScreen
、printchapter
代码如下。初始化关卡选择并进行打印。打印位置通过不断运行观察调整得到。
1 | void printSelectionScreen(chapter *a){ |
打印效果如图
函数 printMap
、printWall
代码如下。
1 | void printMap(piece *hrd, int result, string name){ |
printWall
函数打印墙体效果如图。
函数 printpiece
代码如下。提供了打印各棋子的总接口。选择打印颜色也在此处完成。
1 | void printpiece(int x, int y, int index, int color){ |
函数 printZF
代码如下。在(x,y)处开始打印棋子,一行一行打印。(x,y)为左上角坐标。此处提供一个打印示例,其他各棋子的打印函数也大抵如此。
1 | void printZF(int x, int y){ |
打印棋子初始状态及关卡名称后的效果如图。
函数 erasepiece
代码如下。提供了擦除各型号大小棋子的总接口。不同棋子对应各自大小的擦除函数。
1 | void erasepiece(int x, int y, int index){ |
示例擦除函数代码如下。在对应位置打印空格即可实现棋子的擦除。注意每格大小3×6(高×宽)
1 | void erase12(int x, int y){ |
至此,print.h
头文件已经完成。
control.h
需包含的头文件有 conio.h
和 print.h
。其中 conio.h
头文件用于获取键盘输入。仍然对 X
、Y
进行宏定义。
该头文件所包含的函数一览:
1 | int choosecontrol(chapter *a); //choose the chapter 选择关卡的总控 |
函数 choosecontrol
的代码如下。返回值为选择的关卡编号。可以利用此值进行棋盘初始化。
1 | int choosecontrol(chapter *a){ |
getch()
函数为获取一次键盘输入。当输入wasd时,我们需要改变相应的关卡。以 case 'w'
为例。
1 | case 'w': |
获取输入后我们对关卡状态进行更新,并且重新打印,体现在了游戏界面上。
函数 clickcontrol
代码如下。获取键盘输入并调用相应函数,作为总控制中心。
返回值为 10000
时表示回到选择关卡界面,返回值为 10001
时表示当前关卡重新开始,返回值为 1
表示继续进行游戏。值得一提的是,down
函数是有 bool
返回值的,因为游戏胜利的最后一步一定是曹操从下方缺口逃出。因此 down
函数的返回值表示当前关卡通过与否。若通关返回 true
,相应地 clickcontrol
函数返回 10086
。
1 | int clickcontrol(piece *hrd){ |
函数 click
代码如下。通过循环不断调用 clickcontrol
函数,若需回到主界面,则返回 true
(通关也是返回主界面),若需当前关卡重新开始,则返回 false
。当 clickcontrol
返回 10086
即关卡胜利时,我们可以知道游戏的最后一步一定是曹操出现在缺口上,于是可以重新打印曹操使曹操“越过棋盘”,并打印游戏通关恭喜字句,同时使用 getchar()
函数使游戏暂停。
1 | bool click(piece *hrd){ |
游戏通关效果如图。
函数 space
代码如下。使用循环检测10枚棋子中处于选择/锁定状态的,切换其状态,并对其重新进行打印(颜色改变)。
1 | void space(piece *hrd){ |
函数 movejudge
代码如下。我们定义棋子的时候记录了其左上角及右下角。当棋子空间 judge
不与棋子 i
重合时,我们返回 true
。由于棋子占领的空间是矩形,因此未重合时,应该至少两个棋子占领的 x
范围或者 y
范围没有交集。
1 | bool movejudge(piece judge, piece i){ |
接下来看看 up
函数。
1 | void up(piece *hrd){ |
可以看到,核心难点即在于对于不同大小的棋子,我们该如何在切换棋子的时候,选择到尽可能符合认知的那枚棋子,而且还要保证所有的棋子都一定能被选择到。该程序还有不足之处便是只能选择严格在其正上方最靠近的棋子,若仍然不止一枚的话只会打印编号靠前的第一枚。如以下这种情况,关羽进行up
操作,只会选择两个兵中编号靠前的那一个。
兵 | 兵 |
关羽 |
其他的 down
、left
、right
函数基本如上,只需改变相应的变量名和一些常量值,这里便不再赘述。
此外,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 |
|
至此便完成了所有的代码。
历程
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的格式,但是只写过一点点。而且当时也不知道那是什么),书写的确花了不少时间。不过经过这些天来的摸索以后应该就是轻车熟路了。
回头想想,这几天接触学习了好多新东西啊。在路上总是好事。