您的位置:首页 > 编程语言 > Java开发

推箱子暴力求解程序(SokobanSolver)

2017-01-17 15:39 706 查看
写这个程序是因为在看《Java并发编程实战》书的时候,提到过用多线程来解决推箱子游戏,感觉挺好玩的,于是就开始写啦!!

准备阶段

先介绍一个推箱子网站(主页):http://sokoban.cn/

在这个网站你在它的规则(格式)下,也可以轻松获得推箱子地图、验证答案。

规则、格式:http://sokoban.cn/xsb_lurd.php

推箱子地图获取、答案验证:http://sokoban.cn/sokoplayer/SokoPlayer_HTML5.php

详细使用可以看下面的运行介绍

程序总结

代码

github:https://github.com/ZhongWenhui1995/SokobanSolver

csdn:http://download.csdn.net/detail/name_z/9742995

结果:

对于不复杂的图(地形不大、箱子不多或者求解比较复杂)的图,大部分可以完成任务,但对于太复杂的地图,时间很长,而且最终有可能会导致OutOfMemory异常,而解不出来。(后面有几个解决的想法)

多线程比单线程速度快,但是多线程时间不稳定,起伏较大。

程序介绍

简介:

简单来说,就是使用深度遍历所有路径,直到找到解决的路径或者找不到退出。

其中,有多线程版本和单线程版本

程序主要分为3部分:

1.地图

2.人的移动

3.路径搜索

类总览:



地图部分

类:



功能:

保存地图信息,以及提供关于地图的基础功能:地图检查、修改地图信息

类介绍

Point:

坐标点:x,y

创建后便不可修改

MapSymble:

指定地图信息用什么字符表示:墙壁、普通地板、站在目标点上的人、目标点、箱子、人、地图行间分隔符(整个地图用一个String存储时)、在目标点上的箱子



MapDirection:

地图方向:上下左右



SokobanMap:

创建后便不可修改

保存地图及相关信息:

1.地图信息

2.人的坐标点

3.长度,宽度

4.达到当前地图信息人所走过的路径(如果解决后,最后输出的结果)

提供地图相关功能:

1.获取某坐标的字符

2.指定将某坐标的字符替换成指定字符,然后返回新的SokobanMap对象(不是返回当前对象)

MapChecker:

功能:

检查地图是否有效,标准为:

1.地图必须为长方形的规整图形

2.不能存在无效字符(不在MapSymble没有的字符)

3.WALL必须封闭

4.必须有且仅有一个人

5.不能存在无效行(整行都是GROUND)

检查WALL是否封闭方法:

采用深度搜索,选定起始墙壁(通常为第一行中出现的第一个WALL),然后沿着墙壁遍历没走过的墙壁(只走墙壁),如果最后返回起始点则表明这墙壁是封闭的。

缺点:

不能检查出是多个闭环还是只有一个闭环

。。。

人的移动部分

类:



功能:

用于执行人的上下左右的移动,提供移动后的地图。

其中移动会有两种操作:

1.普通的移动,由地板到另一个地板(目标点)

2.推动箱子的移动,移动方向上有一个箱子

其中移动有可能会有两种结果,能否移动,其中,导致不能移动的原因有:

1.遇到了墙壁

2.遇到箱子,但箱子贴着墙壁

3.遇到箱子,但是箱子又贴着另一个箱子

类介绍:

IMapMoveRule:

运行规则,决定一个字符移动后地图的变化(目的地点以及原地点的变化)

包含两个方法:

1.当字符A移动到字符B后,字符B的坐标上应该显示什么字符

2.当字符A移动后,原来字符A的坐标上应该显示什么字符

DefaultMapMoveRule:

实现IMapMoveRule

下面列举的是方法:2.当字符A移动后,原来字符A的坐标上应该显示什么字符

public Character getCharOfGoalAfterMove(char goalChar, char moveChar) {...}

/**
* 返回moveChar移动后原来所处的位置应该显示的字符
* @param moveChar 移动的字符(只能为人和箱子,只能为MAN,BOX,MAN_ON_GOAL,BOX_ON_GOAL)
* @return 移动后原来地点应该显示的字符
* @see MapSymble
*/
@Override
public Character getCharOfMoveAfterMove(char moveChar) {
Character res = null;
//如果原来是人或者箱子,则移动后就是普通的地板,如果是在目标点上的人或者箱子,则移动后就是目标点
if (moveChar == MapSymble.MAN_CHAR || moveChar == MapSymble.BOX_CHAR) {
res = MapSymble.GROUND_CHAR;
} else if (moveChar == MapSymble.MAN_ON_GOAL_CHAR || moveChar == MapSymble.BOX_ON_GOAL_CHAR) {
res = MapSymble.GOAL_CHAR;
}
return res;
}


ManMover:

返回人往指定方向移动一格后的地图(返回的地图是新对象),如果不可移动直接返回原来的地图。

/**
* 如果往该方向移动一格是合法的移动,则返回移动后的SokobanMap,否则返回原来的SokobanMap,使用默认的移动规则DefaultMapMoveRule
* @param map
* @param direction
* @return
*/
public static SokobanMap moveOneStep(SokobanMap map, int direction){
return ManMover.moveOneStep(map, direction, new DefaultMapMoveRule());
}

public static SokobanMap moveOneStep(SokobanMap map, int direction, IMapMoveRule mapRule) {
switch (direction) {
case MapDirection.UP:
return ManMover.up(map, mapRule);
case MapDirection.DOWN:
return ManMover.down(map, mapRule);
case MapDirection.LEFT:
return ManMover.left(map, mapRule);
case MapDirection.RIGHT:
return ManMover.right(map, mapRule);
default:
break;
}
return null;
}


/**
* 每次移动一格,修改传入的地图参数,返回新的地图
*
* @param map
* @param movePoint
* @param direction
* @param mapRule
* @return
*/
private static SokobanMap move(SokobanMap map, int direction, IMapMoveRule mapRule) {
SokobanMap resMap = null;
Point movePoint = map.manPoint;
Point nextPoint = ManMover.getTargetPoint(movePoint, direction);
Point nextNextPoint = ManMover.getTargetPoint(nextPoint, direction);
...
//判断人的目的地点是否为地板或目标地点
else if(goalChar == MapSymble.GROUND_CHAR || goalChar == MapSymble.GOAL_CHAR){
//修改地图,字符移动后原来的坐标应该显示什么字符
resMap = map.modifyPoint(movePoint, mapRule.getCharOfMoveAfterMove(moveChar));
//修改地图,字符移动到目标坐标后,目标坐标应该显示什么字符
resMap = resMap.modifyPoint(nextPoint, mapRule.getCharOfGoalAfterMove(goalChar, moveChar));
//修改路径
resMap = new SokobanMap(resMap.getMapList(), resMap.path + MapDirection.getPath(direction));
}
...
}


。。。

路径搜索部分

类:



功能:

深度搜索,直至发现解决问题的路径

类介绍

ISokobanSolver:

包含一个方法:

1.传入初始地图,然后开始搜索

2.返回解决了的SokobanMap,如果没有则返回null

IJudger:

包含3个方法

1.判断地图是否已经解决

2.判断地图是否已经走过(如果走过了,如果当前的路径比存储的路径短,则更新路径)

3.清空缓存

DefaultJudger:

实现了IJudger,其中用Set来保存遍历过的地图信息

//地图信息
private Set<String> paths = new ConcurrentSkipListSet<String>();

@Override
public boolean isSolved(SokobanMap map) {
if(! map.mapStr.contains(MapSymble.GOAL) && ! map.mapStr.contains(MapSymble.MAN_ON_GOAL)){
return true;
}
return false;
}

/**
* 判断当前地图情况是否之前已经出现过,如果没有则将添加当前地图情况,返回false,
*
* @param map
* @return
*/
@Override
public boolean isPathed(SokobanMap map) {
if (!paths.contains(map.mapStr)) {
paths.add(map.mapStr);
return false;
}
return true;
}


ViolentSingleSolver:

单线程的暴力(深度搜索)求解程序

ViolentConcurrentSolver

多线程的暴力(深度搜索)求解程序。

线程数量固定为电脑的cpu核数,每条线程负责搜索一条路径,直到该路径解决问题,当该路径已经无法继续走时,结束当前线程。在别的运行中的线程,开出一条新线程来执行新路径的搜索。

程序唤醒机制:

采用唤醒机制,来使主线程等待路径搜索线程执行完任务

if (this.solvedMap == null) {
//递归版本
// executeRecursiveFindPath(map);
//执行路径搜索(迭代版本)
executeIterateFindPath(map);
//开始对lock的监听,主程序进入睡眠状态
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}


...
if (this.judger.isSolved(nextMap)) {
//检测到nextMap已经解决
//保存结果地图
this.solvedMap = nextMap;
//唤醒主程序
synchronized (lock) {
lock.notify();
}
return;
}
...


多线程执行部分:

而且要让线程保持在指定的数量

这里采用了固定的线程池以及信号量来完成

private ExecutorService pool = Executors.newFixedThreadPool(POOL_SIZE);
private Semaphore aliveThread = new Semaphore(POOL_SIZE);


然后执行中,通过信号量来限制是创建新线程来执行该路径的遍历,还是继续在当前线程中执行遍历

private boolean executeIterateFindPath(final SokobanMap map) {
//使用信号量限制新线程的创建
if (aliveThread.tryAcquire()) {
pool.execute(new Runnable() {
@Override
public void run() {
iterateFindPath(map);
//线程执行后,一定要释放信号量
aliveThread.release();
}
});
return true;
}
return false;
}

private void iterateFindPath(SokobanMap map) {
//采用深度搜索,因此要用栈的后进先出
Stack<SokobanMap> maps = new Stack<SokobanMap>();
maps.add(map);
while (!maps.isEmpty()) {
map = maps.pop();
SokobanMap nextMap = this.getNextMap(map);
while (nextMap != null && this.solvedMap == null) {
if (this.judger.isSolved(nextMap)) {
this.solvedMap = nextMap;
synchronized (lock) {
lock.notify();
}
return;
}
//如果如果线程成功,则不需要在当前线程中执行,因此不需要添加到maps中
if (!executeIterateFindPath(nextMap)) {
//如果不成功则需要继续在当前线程中执行,因此要添加到maps中
maps.add(nextMap);
}
nextMap = this.getNextMap(map);
}
}
}


运行介绍:

//创建求解器对象
ISokobanSolver solver = new ViolenceConcurrentSolver(new DefaultJudger());
//读取用户输入的地图信息
SokobanMap map = readMap();
//判断该地图是否有效地图
if(MapChecker.isValidMap(map)){
//开始进行求解
solver.solve(map);
//获取结果
SokobanMap resMap = solver.getSolvedMap();
//判断是否解出结果
if(resMap != null){
//输出结果路径
System.out.println(resMap.path);
}else{
System.out.println("can not find the way to solve the SokobanMap");
}
}else{
System.out.println("this is not valid sokoban map");
}


1.获取地图,输入地图:



1.打开上面给的网址http://sokoban.cn/sokoplayer/SokoPlayer_HTML5.php

2.在红框1中选择好关卡

3.点击红框2中的输出关卡

4.在红框3中会有该关卡的地图信息

5.复制地图信息到程序中,再在下一行输入end



2.进行求解,获取结果



刚点击enter后,会显示开始时间,过一会后,会出现解决结果,如果比较复杂的图(地形大、箱子多),可能会等很久或者解不出来



3.开始验证答案



1.将之前输出的结果复制到红框2中

2.再点击红框1中的载入答案,然后它就会自己运行答案



程序优化方案(未实现)

目前代码中有一个判断地图是否已经遍历过,其中用的是set来存储是否已经走过,其中走的路径越多,存储的地图也越多,因此解决部分复杂地图的时候,就会导致OutOfMemory异常。

这里有些可以改烧解决这方面问题的想法:

存储优化:

遍历过的地图信息不再保存在内存中,而是保存在本地中(数据库、文件),用空间换时间来进行优化。

可结合加上当前存储方法,对简单地图使用内存保存,复杂方法转向本地保存。可以增加存储的限额,当到达限额后,不再保存在内存中,而是保存到本地中。

路径选择优化:

目前是上下左右4个方向都回进行遍历,但实际上部分路径是没有意义的路径,考虑是否通过对路径的筛选,来进行优化

多线程、单线程版本时间比较

多线程版本和单线程版本采用的都是迭代的深度搜索

代码:

long time = 0;
final int count = 100;
int successCount = 0;
SokobanMap map = readMap();
for (int i = 0; i < count; i++) {
ISokobanSolver solver = new ViolenceConcurrentSolver(new DefaultJudger());
// ISokobanSolver solver = new ViolentSingleSolver(new DefaultJudger());
if (MapChecker.isValidMap(map)) {
try {
solver.solve(map);
} catch (Exception e) {
e.printStackTrace();
}
SokobanMap resMap = solver.getSolvedMap();
System.out.println("Time: " + solver.getSolvedTime());
if (resMap != null) {
time += solver.getSolvedTime();
successCount++;
System.out.println(resMap.path);
} else {
System.out.println("can not find the way to solve the SokobanMap");
}
} else {
System.out.println("this is not valid sokoban map");
}
}
System.out.println("Average: " + time / successCount);


样本1:



#####----
#---#####
#-#-#---#
#-$---$-#
#..#$#$##
#.@$---#-
#..--###-
######---


多线程版本100次平均耗时:3700ms

单线程版本100次平均耗时:6251ms

样本2:



_#####__
_#--@###
##-#$--#
#-*.-.-#
#--$$-##
###-#.#_
__#---#_
__#####_


多线程版本100次平均耗时:431ms

单线程版本100次平均耗时:828ms

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java 多线程 推箱子