写了个拼图游戏,探讨一下相关的AI算法。拼图游戏的复原问题也叫做N数码问题。
拼图游戏
N数码问题
广度优先搜索
双向广度优先搜索
A*搜索
游戏设定
实现一个拼图游戏,使它具备以下功能:
自由选取喜欢的图片来游戏
自由选定空格位置
空格邻近的方块可移动,其它方块不允许移动
能识别图片是否复原完成,游戏胜利时给出反馈
一键洗牌,打乱图片方块
支持重新开始游戏
难度分级:高、中、低
具备人工智能,自动完成拼图复原
实现几种人工智能算法:广度优先搜索、双向广度优先搜索、A*搜索
保存游戏进度
读取游戏进度
Puzzle Game.png
自动完成拼图复原
先看看完成后的效果。点自动按钮后,游戏将会把当前的拼图一步一步移动直到复原图片。
自动复原.gif
图片与方块
图片的选取可通过拍照、从相册选,或者使用内置默认图片。
由于游戏是在正方形区域内进行的,所以若想有最好的游戏效果,我们需要一张裁剪成正方形的图片。
截取正方形区域.png
选好图片后,需要把图片切割成n x n块。这里每一个方块PuzzlePiece都是一个UIButton。
由于图片是会被打散打乱的,所以每个方块应该记住它自己在原图上的初始位置,这里给方块添加一个属性ID,用于保存。
@interface PuzzlePiece : UIButton
/// 本方块在原图上的位置,从0开始编号
@property (nonatomic, assign) NSInteger ID;
/// 创建实例
+ (instancetype)pieceWithID:(NSInteger)ID image:(UIImage *)image;
@end
难度选择
切割后的图片块组成了一个n x n矩阵,亦即n阶方阵。而想要改变游戏难度,我们只需要改变方阵的阶数即可。
设计三档难度,从低到高分别对应3 x 3、4 x 4、5 x 5的方阵。
难度选择.gif
假如我们把游戏中某个时刻的方块排列顺序称为一个状态,那么当阶数为n时,游戏的总状态数就是n²的阶乘。
在不同难度下进行游戏将会有非常大的差异,无论是手动游戏还是AI进行游戏。
在低难度下,拼图共有(3*3)! = 362880个状态,并不多,即便是最慢的广搜算法也可以在短时间内搜出复原路径。
3阶方阵的搜索空间.png
在中难度下,拼图变成了4阶方阵,拼图状态数飙升到(4*4)! = 20922789888000,二十万亿。广搜算法已基本不能搜出结果,直到爆内存。
广搜算法占用的巨量内存.gif
在高难度下,拼图变成了5阶方阵,状态数是个天文数字(5*5)! = 1.551121004333098e25,10的25次方。此时无论是广搜亦或是双向广搜都已无能为力,而A*尚可一战。
高难度下的5阶方阵.png
方块移动
在选取完图片后,拼图是完整无缺的,此时让第一个被触击的方块成为空格。
从第二次触击开始,将会对所触击的方块进行移动,但只允许空格附近的方块发生移动。
每一次移动方块,实质上是让方块的位置与空格的位置进行交换。在这里思维需要转个小弯,空格并不空,它也是一个对象,只不过表示出来是一块空白而已。那么我们移动了方块,是否可以反过来想,其实是移动了空格?答案是肯定的,并且思维这样转过来后,更方便代码实现。
方块移动.gif
打乱方块顺序
这里为了让打乱顺序后的拼图有解,采用随机移动一定步数的方法来实现洗牌。
对于n阶方阵,可设计随机的步数为:n * n * 10。在实际测试当中,这个随机移动的步数已足够让拼图完全乱序,即使让随机的步数再加大10倍,其复原所需的移动步数也变化不大。复原步数与方阵的阶数有关,无论打乱多少次,复原步数都是趋于一个稳定的范围。
打乱方块顺序.gif
随机移动一定步数.png
拼图状态
我们需要定义一个类来表示拼图在某个时刻的状态。
一个状态应持有以下几个属性:
矩阵阶数
方块数组,以数组的顺序来表示本状态下方块的排列顺序
空格所在的位置,其值指向方块数组中显示成空