寫這個程序是因爲在看《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:
_#####__
_#--@###
##-#$--#
#-*.-.-#
#--$$-##
###-#.#_
__#---#_
__#####_