致佳音: 推箱子游戏自己主动求解算法设计(四)

这一节是本文的核心内容,即推箱子游戏求解算法的设计思路过程

前面已经说过过,推断局面反复的最好标准不是局面全然一致,而是坐标排序同样且角色坐标通行

例如以下图。角色不管怎么移动,不推动箱子的时候。都能回到原来的位置。算作同一个局面:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcHJzbmlwZXI=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" >

再例如以下图。两个箱子互换位置,结果与没有移动箱子是一样的,所以排序箱子坐标以后一致。还是同样局面

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcHJzbmlwZXI=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" >

问:有必要推断局面反复吗?是不是仅仅是提升一下效率?

答:不是为了提升效率,而是为了能解出来,假设使用递归,重复的局面重复耗尽堆栈,而队列则耗尽内存

正如上图,重复推这两个箱子往返。

问:排序全部箱子再比較,也太鸡肋了,有没精髓?

答:有。那就是哈希表,只是哈希表有一丝隐隐的风险,那就是假设计算结果哈希不同,那么两团棉花数据肯定不同

可是假设结果同样,两团棉花数据也可能不同,当然同样数据长度不同数据哈希同样的概率极其低。像MD5那样把数

据长度加进去哈希的。反复就更加低。把地球的沙子都哈希一遍可能也就几颗反复。为了速度,我使用CRC32

问:那么,有了上面的基础。把搬运工向四个方向移动生成快照。然后递归下去即可了吗?

答:理论上是能够的,只是如上面所说。搬运工不推动箱子的时候。没有意义,属于闲走,我们的对象应该转移到箱子

上,而不是搬运工。

把每一个箱子向四个方向推动都生成快照,过滤反复,并“递归”直到全部的箱子归位

综上所述。我们就能够開始动工了,给个小问题思考。得到解法后,会不会还有更好的解法?或者换个问法:队列的处理怎样进行?

我的方案是:先入先出。即先增加队列的先处理。这样保证更低步数的快照,先被分析。更低的步数当然是更好的解法,终于第一个

解法自然是最优解法……

场景数据结构:

#pragma pack(4)		// STAR.Value(__int64)默认以64位对齐
typedef struct tagStage{
	UINT Volume;			// 结构体实际大小(加速计算)
	UINT Flags;				// 场景标识
	PSTAR Stars;			// 箱子位置列表(内部指针, 不释放, 数量为.Count)
	PSTAR Series;			// 排序箱子坐标(内部指针, 不释放, 数量为.Count)
	UINT Count;				// 箱子数量(目标数量)
	UINT Value;				// 归位数量
	UINT Hash;				// 场景指纹(箱子坐标排序哈希值)
	tagStage *Prev;			// 上个场景(游戏中连接队列操作, 伪指针, 不释放)
	tagStage *Next;			// 下个场景(游戏中连接队列操作, 伪指针, 不释放)
	tagStage *Host;			// 父级场景(求解时反向父级搜索得到解法路径, 伪指针, 不释放)
	union {
		STAR Size;			// 场景尺寸
		struct {
			long SizeX;		// 场景宽度(列数)
			long SizeY;		// 场景高度(行数)
		};
	};
	union {
		STAR Position;		// 角色当前位置
		struct {
			long PosX;		// 角色水平位置
			long PosY;		// 角色垂直位置
		};
	};
	union {
		STAR Automation;	// 自己主动寻路位置
		struct {
			long AutoX;		// 寻路水平坐标
			long AutoY;		// 寻路垂直坐标
		};
	};
	PMOVE Moves;			// 可行走法列表(内部指针, 不释放, 数量为.Count * 4: 四个方向)
	UINT Range;				// 可行走法数量
	UINT Index;				// 当前測试走法
	UINT Slaves;			// 剩余未分析的子场景数量
	UINT Layer;				// 当前步数
	union {
		BYTE Matrix[1];		// 矩阵数据
		long Data;
	};
} STAGE, *PSTAGE;
#pragma pack()

当中的内部指针指向结构体内部,比方Stars指向各个箱子的坐标,而不用转换Matrix再计算偏移。我们用32位内存。换取20多条汇编指令

一个刺客换一个王朝。,,好快的剑……

STAR是AlphaStar算法的数据结构。是一个坐标对

typedef union tagStar{	// Point type(8B)
	struct {
		long X;
		long Y;
	};
	__int64 Value;
} STAR, *PSTAR;

Move是走法信息,记录了某种走法所影响到的数据,占48个字节。也存储与结构体内部,限于篇幅这里就不详述

然后是队列数据结构:

typedef struct tagQueue{	// 与堆栈不同, 先进先出
	UINT Volume;			// 队列容量(字节数)
	UINT Size;				// 元素内存大小
	UINT Count;				// 元素上限索引
	UINT Value;				// 当前元素个数(下个索引)
	UINT Used;				// 已用元素个数
	UINT Step;				// 结果步数
	PSTAGE Active;			// 首个活动场景(从此弹出)
	PSTAGE Backup;			// 末尾活动场景(向此压入)
	PSTAGE Stages;			// 过期场景(压入弹出)
	PSHOT Shots;			// 失败快照列表(外部指针, 外部释放)
	PSTACK Stacks;			// 扫描坐标列表(外部指针, 外部释放)
	union {
		BYTE Dummy[1];
		UINT Data;
	};
} QUEUE, *PQUEUE;

解法的逻辑步骤例如以下:

1.初始化队列。提取第一个场景到当前场景

2.当前场景全部箱子归位。函数返回

3.分析场景得到若干个新场景,过滤反复

4.过滤后新场景数量为零,场景无解,删除场景(可优化,见下一篇)

5.追加新场景到队列。分析队列下一个场景。反复2-4

6.队列场景数量为零,场景无解(或队列太小,内存不足)

依据上一级场景生成新场景的函数代码(其它代码见资源包,限于篇幅。这里不具体列出):

// 从队列中申请一个场景, 并以当前场景填充, 扫描后检測反复, 有效则追加到队列
PSTAGE fnStageNext(PQUEUE pQueue, PSTAGE pStage, int *pdwCode)
{
	PSTAGE pNext;	// 生成下一步场景
	PMOVE pMove;
	int dwRet;

	pNext = fnQueueApply(pQueue);
	if(pNext == NULL)
	{
		if(pdwCode) *pdwCode = 0;	// 队列耗尽
		fnStageCode(SEC_CACHE_NULL);
		return NULL;
	}
	// 复制上级数据, 修正指针
	V32Copy(pNext, pStage, pStage->Volume);
	pNext->Host = pStage;	// .Prev和.Next在丢弃前或增加队列时赋值
	fnStagePtr(pNext);	// 修正内部指针
	// 依据当前动作, 推动场景
	pMove = &pStage->Moves[pStage->Index];
#ifdef _DEBUG
	//fnPrint("当前场景=0x%08X, 父级场景=0x%08X, 玩家=(%d, %d), 箱子:\r\n", pStage, pStage->Host, pStage->PosX, pStage->PosY);
	//fnPrintBox(pStage);
	//fnPrint("当前动作: 箱子%d移至(%d, %d), 玩家移至(%d, %d), 寻路坐标为(%d, %d).\r\n\r\n",
	//	pMove->Index, pMove->ObjX, pMove->ObjY, pMove->PortX, pMove->PortY, pMove->MoveX, pMove->MoveY);
#endif
	fnStagePush(pNext, pMove, SMF_MOVE_NONE);	// 应用走法
	pNext->Range = 0;	// 没有走法
	pNext->Index = 0;	// ...
	pNext->Layer++;		// 步数
	// 扫描线填充可通行单元
	if(pNext->PosX == 2 && pNext->PosY == 4)
	{
		dwRet = 0;
	}
	dwRet = fnStageScan(pQueue, pNext);
	// 检验局面反复
	pNext->Hash = fnStageHash(pNext->Stars, pNext->Series, pNext->Count);	// 排序计算哈希
	dwRet = fnStageLoop(pQueue, pNext);
	if(dwRet != 0)
	{
#ifdef _DEBUG
		fnPrint("丢弃反复场景=0x%08X.\r\n", pStage);
#endif
		pNext->Prev = NULL;		// 孤立, 防止队列删除(场景尚未增加队列, 仅仅追加到回收链表)
		pNext->Next = NULL;
		fnQueueRemove(pQueue, pNext);	// 移除场景
		if(pdwCode) *pdwCode = -1;		// 反复局面
		fnStageCode(SEC_ERROR_NONE);	// 清零错误
		return NULL;
	}
	// 函数返回
	if(pdwCode) *pdwCode = 1;
	return pNext;
}

执行效果如图所看到的:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcHJzbmlwZXI=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" >

debug记录文件内容(下一节说说,程序的进一步优化。这个结果未经过优化):

開始求解, 队列尺寸=8192, 解法尺寸=200...
求解成功, 队列使用峰值=3869, 剩余有效个数=3867!
(4, 1)
<4, 2>
(3, 5)
<3, 4>
(2, 4)
<2, 3>
(5, 4)
<4, 4>
(4, 2)
<4, 3>
<3, 3>
<3, 4>
(1, 3)
<2, 3>
<3, 3>
<4, 3>
(6, 3)
<5, 3>
(5, 4)
<4, 4>
<3, 4>
(3, 3)
<4, 3>
(1, 4)
<2, 4>
(5, 4)
<5, 3>
(2, 4)
<3, 4>
(2, 1)
<2, 2>
<2, 3>
(3, 6)
<3, 5>
<3, 4>
<4, 4>
(1, 4)
<2, 4>
<3, 4>
(6, 2)
<5, 2>
(4, 3)
<3, 3>
(5, 5)
<5, 4>
(3, 4)
<4, 4>
(1, 3)
<2, 3>
(4, 1)
<4, 2>
<4, 3>
<3, 3>
(2, 4)
<2, 3>
(6, 3)
<5, 3>
<4, 3>
<3, 3>
(5, 5)
<5, 4>
(3, 4)
<4, 4>
(6, 3)
<5, 3>
<5, 4>
(3, 3)
<4, 3>
(1, 3)
<2, 3>
(2, 1)
<2, 2>
最优解法推动 43 次, 寻路 29 次, 合计坐标 72 个!
时间: 2024-10-10 14:29:29

致佳音: 推箱子游戏自己主动求解算法设计(四)的相关文章

致佳音: 推箱子游戏自动求解算法设计(四)

这一节是本文的核心内容,即推箱子游戏求解算法的设计思路过程 前面已经说过过,判断局面重复的最好标准不是局面完全一致,而是坐标排序相同且角色坐标通行 如下图,角色无论怎么移动,不推动箱子的时候,都能回到原来的位置,算作同一个局面: 再如下图,两个箱子互换位置,结果与没有移动箱子是一样的,所以排序箱子坐标以后一致,还是相同局面 问:有必要判断局面重复吗?是不是只是提升一下效率? 答:不是为了提升效率,而是为了能解出来,如果使用递归,重复的局面反复耗尽堆栈,而队列则耗尽内存 正如上图,反复推这两个箱子

致佳音: 推箱子游戏自动求解算法设计(一)

本来酷爱音乐, 老衲也想谱一曲<献给爱丽丝>之类, 通俗又有境界的曲子, 奈何没有那个水平, 也不是一个程序员做的勾当,于是就有了本文. 希望莲花妹妹跟着思路走,能遗忘那些太多的忧伤-- 本文分以下四个小节: 一.平面寻路算法(Alpha Star) 二.闭合图形填充算法(扫描线种子填充) 三.推箱子求解 四.执行效率的优化 日本人有个程序叫Sokuban Automatic Solver,文件名是sokoban722.exe我附带在资源里面 不过日本人的东西没有开源,我们也不知道它里面的花花

致佳音: 推箱子游戏自动求解算法设计(二)

这一个小节我们说一说传说中的A×算法,其实之前也上传过类似的小件件,这里我们就去剖析一下它 毕竟在游戏程序,我们要从一点移动到另一点,并得到最短路程的轨迹,类似这种算法还有好几种,执行效率都差不多,不过大多不能得到轨迹 首先,从一点移动到另一点,最快就是直接走过去了,就像小男生爱上小女生,最好的办法就是直接走到她面前说:我爱你 不过理想状态,几乎是没有的,弯路那是必然的经过,有曲线,其实更美-- 那么弯路该怎么走呢,是不是先去背景看下毛主席,再去三亚晒个太阳,再回来告诉她外面的世界好美,不,不,

致佳音: 推箱子游戏自动求解算法设计(三)

这一节我们说说闭合曲线的填充,为什么会有这个东西呢 当我们递归一个场景时,我们以推动箱子为标志,如果不推动箱子,那么跑到哪里都白跑,而出现重复的判别最好就是所有坐标相同 包括这些坐标互换位置(排序结果相同),而后一个场景搬运工坐标能移动到另一个场景搬运工的位置(求解算法部分再详细说) 由于场景有多个箱子,每个箱子可以有几个方向移动,反复的寻路效率不高,起初我想删除路径部分,只检测能否移动到目标 来提升执行效率,就是偷懒一下,然后想想既然是礼物,偷懒也不是分时候,也有脸献给别人于是废弃了A×算法

致佳音: 推箱子游戏自动求解算法设计(五)

说了这么多,这一节是本文最后一节啦,就是程序的进一步优化. 这一节呢,还分那么几个小意思,- -! 1.程序逻辑和机制的优化 2.源码级代码的优化 3.针对CPU和操作系统的编译优化 问:大侠,我是过来人,排序哈希我渐渐习惯了,不痛了,还有哪些地方可以更刺激的 答:前面我们提到检测局面重复,不要让后面的局面有跟走过的局面一样,导致无限的堕落,无法自拔,还有一样是可以算作重复的 那就是失败的局面,即没有一个箱子可以有效推的局面,再出现这个局面就不要分析了,直接删掉吧,那么我们就要再创建一个失败 局

JavaScript写一个小乌龟推箱子游戏

推箱子游戏是老游戏了, 网上有各种各样的版本, 说下推箱子游戏的简单实现,以及我找到的一些参考视频和实例: 推箱子游戏的在线DEMO : 打开 如下是效果图: 这个拖箱子游戏做了移动端的适配, 我使用了zepto的touch模块, 通过手指滑动屏幕就可以控制乌龟走不同的方向: 因为推箱子这个游戏比较简单, 直接用了过程式的方式写代码, 模块也就是两个View 和 Model, 剩下就是用户的事件Controller, 用户每一次按下键盘的方向键都会改变数据模型的数据,然后重新生成游戏的静态htm

推箱子游戏

本游戏为推箱子游戏,即通过移动方向键来控制小人去推动箱子,直到把所有的箱子都推动到各个目标中.游戏需要在人物行走过程中无法穿越墙和箱子,并在有限的范围中放好各箱子,且每次只能搬运一个箱子.所基于的语言是8086汇编,使用的编译环境是唐都的集中开发环境TD-PIT. 本次设计的基本思想是通过将不同的元素(墙.路.箱子.人.目标位)抽象为不同的矩阵,然后将所设计的地图描抽象成一个控制矩阵来控制图像的显示,每一个控制矩阵块代表一个元素,我们通过不断刷新控制矩阵来达到显示人物移动及推箱子的效果. 1.1

jQuery版推箱子游戏详解和源码

前言 偶然间看到很多用js写游戏的感觉很炫酷的样子,所以就想试试,就看了一些资料和某前端站点的视屏.于是乎就自己动手实践了一下,上推箱子截图 感觉很丑陋,但是功能是实现了.再说貌似大多都是这样的吧,这一关其实还是有点难度的,我做完之后想检测一下下一关正确么,居然还玩了以后才通关. 如果你看到这张图让你想起了你童年的回忆,说明你老了,这里可以试玩一下(很遗憾没有链接地址,最后又源码可以下载). css布局 主要考虑的是地图是怎么动态生成的,地图中有灰色的,还有墙,箱子,蓝色,红色背景,人物.先看c

用C写一个简单的推箱子游戏(一)

我现在在读大二,我们有一门课程叫<操作系统>,课程考查要求我们可以写一段程序或者写Windows.iOS.Mac的发展历程.后面我结合网上的资料参考,就想用自己之前简单学过的C写一关的推箱子小程序. 这一程序主要用到了C语言中的二维数组,头文件#include<conio.h>(因为要调用getch()函数以记录输入内容),switch函数等. 一.     功能概述 a)   游戏规则概述 玩家通过键盘输入W.S.A.D四键或者“↑”.“↓”.“←”.“→”四个方向键推动箱子,而