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

寫這個程序是因爲在看《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存儲時)、在目標點上的箱子
mapsymble

MapDirection:

地圖方向:上下左右
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
剛點擊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:

樣本1

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

多線程版本100次平均耗時:3700ms

單線程版本100次平均耗時:6251ms

樣本2:

樣本2

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

多線程版本100次平均耗時:431ms

單線程版本100次平均耗時:828ms

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章