【Unity3d學習】魔鬼與牧師過河遊戲智能幫助

寫在前面

實驗內容

(跑過了自己的算法之後,發現在參考博客裏面,發現其實有個地方是有錯的。它的狀態圖是用起始岸的魔鬼與牧師數來表示的,另一邊岸就可以通過3減去起始岸的角色數量來得到。)
截自參考博客的狀態圖:
在這裏插入圖片描述
瞭解遊戲規則都知道,這個狀態是不能存在的,因爲另一邊就是1P2D,遊戲結束了。
整個狀態圖裏面,除非兩者都是2,否則不可能出現2P這樣的狀態。這個應該是博主的一個小錯誤。

狀態圖自動生成(使用DFS)

自動生成過程可以利用搜索算法來實現,實際上我們都可以知道整個狀態圖的狀態數其實不是很多(畢竟要適合用戶玩,遊戲難度本來就不太高),所以搜索過程實際上也是很快就能夠得出解的。而關鍵在於如何設計狀態的轉移,如何將其程序實現實現?首先搜索算法中需要表示每一個狀態,然後就是狀態到狀態的轉移的表示,最後就是算法的設計(包括Closed表、最佳路徑等的生成)。

1. 狀態表示

每一個狀態都可以看成由兩個部分組成:角色的數量、船的位置。
角色的數量又可以看情況分爲:河兩岸的人數、每一邊牧師數量和魔鬼的數量
由於是深度優先搜索,所以還需要記錄到下一個節點的狀態,類似樹的結構。爲了方便各個狀態之間的雙向轉移,可以構建一個雙向鏈表,指向父節點。
結構體如下:

public class State{
    public int priest;
    public int devil;
    public bool boat;
    public State parent; // 記錄深搜時從哪一個狀態擴展出來,沒什麼重要用途
    public State best_way; //最佳路徑,遍歷全部狀態後得到一條通向解的路徑
    
    public State() {}
    public State(int p, int d, bool b) {
        this.priest = p;
        this.devil = d;
        this.boat = b;
    }

    public State(int p, int d, bool b, State par) {
        this.priest = p;
        this.devil = d;
        this.boat = b;
        this.parent = par;
    }
    public State(State copy) {
        this.priest = copy.priest;
        this.devil = copy.devil;
        this.boat = copy.boat;
        this.parent = copy.parent;
        this.best_way = copy.best_way;
    }

    public bool isEqual(State compare) {
        return this.priest==compare.priest && this.devil==compare.devil && this.boat == compare.boat;
    }

    // override object.Equals
    public override bool Equals(object obj)
    {
        if (obj == null || GetType() != obj.GetType())
        {
            return false;
        }
        State tmp = (State)obj;
        return this.priest==tmp.priest && this.devil==tmp.devil && this.boat == tmp.boat;
    }
    
    // override object.GetHashCode
    public override int GetHashCode()
    {
        throw new System.NotImplementedException();
    }

    public override String ToString() {
        if (best_way == null) {
            return "priest: " + priest.ToString() + " devil: " + devil.ToString() + " boat: " + boat.ToString() + 
        "\nNext: " + "NULL";
        }
        return "priest: " + priest.ToString() + " devil: " + devil.ToString() + " boat: " + boat.ToString() + 
        "\nNext: " + best_way.priest.ToString() + " " + best_way.devil.ToString() + " " + best_way.boat.ToString();
    }
}

這個類定義了上述可以表示狀態的一些變量,表示牧師與魔鬼的數量時,只需要記錄起始岸邊(左邊河岸)的數量就好的,因爲總數已知,所以可以通過總數減左邊河岸得到右邊河岸的數量,沒有必要再另外存儲。
定義了幾個不同簽名的構造函數,方便創建狀態。
重載了Equals函數,便於使用List等集合結構來存儲。
重載了ToString函數, 便於打印當前狀態的信息。

2.DFS算法實現

DFS只需要從一個狀態轉移到另一個狀態,就需要定義轉移的操作。

  1. 可以明確的是一定需要有人在船上,才能發生轉移;
  2. 有船的岸邊才能載人;
  3. 而且船的轉移一定是從一個岸邊轉移到另一個岸邊;

除了以上固定的轉移規則,其餘的規則定義如下:

  • 一次轉移一個牧師/兩個牧師
  • 一次轉移一個魔鬼/兩個魔鬼
  • 一次轉移一個魔鬼和一個牧師

轉移的方法就是將有船的一邊人數減少,另一邊人數增加;但是我們狀態只記錄了左岸的人數,所以當船在左岸時,發生轉移則人數減少;當船在右岸時,發生轉移則人數增加。

每次深搜就是從這些轉移的狀態中找一個,繼續搜索下去,注意只能是有效狀態才能夠繼續搜索。有效狀態的定義就是沒有觸發遊戲結束條件的狀態。
算法類代碼:

public class AI {
    public static List<State> closed = new List<State>();
    public static State end = new State(0, 0, true);
    public bool isFind = false;

    public bool DFS(ref State root) {
        closed.Add(root);
        if (root.isEqual(end)) {
            isFind = true;
        }
        for (int i = 0; i < 5; i ++) {
            State next = nextState(root, i);
            
            if (next != null) {
                if (closed.Contains(next))
                    continue;

                next.parent = root;
                if (isFind) {
                    next.best_way = root;
                }
                else {
                    closed.Remove(root);
                    root.best_way = next;
                    closed.Add(root);
                }
                
                DFS(ref next);
            }
            
        }
        if (!root.isEqual(end) && root.best_way == null) {
            root.best_way = root.parent;
        }
        return isFind;
    }

    public void print() {
        for (int i = 0; i < closed.Count; i ++) {
            Debug.Log(closed[i].ToString());
        }
    }
    public static bool isValid(State s) {
        if (s.priest != 0 && s.priest < s.devil) { // 左邊有牧師且 牧師人數不應少於魔鬼
            return false;
        }
        if (s.priest != 3 && (3-s.priest) < (3-s.devil)) { //右邊有牧師且 牧師人數不應少於魔鬼
            return false;
        }
        return true;
    }

    public State nextState(State s, int operation) {
        int p, d;
        bool b;
        p = s.priest;
        d = s.devil;
        b = s.boat;
        State next = null;
        if (b) { // 船在右方
            if (operation == 0) {
                if (3-p >= 1) { // 右方牧師大於1人,可過
                    next = new State(p+1, d, !b);
                }
                else {
                    return null;
                }
            } 
            else if (operation == 1) {
                if (3-p >= 2) { // 右方牧師大於2人,可過
                    next = new State(p+2, d, !b);
                }
                else {
                    return null;
                }
            }
            else if (operation == 2) {
                if (3-d >= 1) { // 右方魔鬼大於1人,可過
                    next = new State(p, d+1, !b);
                }
                else {
                    return null;
                }
            }
            else if (operation == 3) {
                if (3-d >= 2) { // 右方魔鬼大於1人,可過
                    next = new State(p, d+2, !b);
                }
                else {
                    return null;
                }
            }
            else if (operation == 4) {
                if(3-p >= 1 && 3-d >= 1) {
                    next = new State(p+1, d+1, !b);
                }
                else {
                    return null;
                }
            }
        }
        else { // 船在左方
           if (operation == 0) {
                if (p >= 1) {
                    next = new State(p-1, d, !b);
                }
                else {
                    return null;
                }
            } 
            else if (operation == 1) {
                if (p >= 2) {
                    next = new State(p-2, d, !b);
                }
                else {
                    return null;
                }
            }
            else if (operation == 2) {
                if (d >= 1) {
                    next = new State(p, d-1, !b);
                }
                else {
                    return null;
                }
            }
            else if (operation == 3) {
                if (d >= 2) {
                    next = new State(p, d-2, !b);
                }
                else {
                    return null;
                }
            }
            else if (operation == 4) {
                if (p >= 1 && d >= 1) {
                    next = new State(p-1, d-1, !b);
                }
                else {
                    return null;
                }
            } 
        }

        if (isValid(next)) {
            return next;
        }

        return null;
    }
}

創建一個closed表,存放已經訪問過的節點。搜索過程中,利用list.contain來判斷當前狀態是否已經訪問過,如果訪問過就不再拓展。
深搜的過程相信都很熟悉,就不再展開。只不過這裏的深搜實際上需要遍歷到所有的狀態,即使找到了一條正確的狀態轉移路徑也不會馬上停止,而是需要找到所有狀態,並且找出它下一步的最佳走法。
比如說:一個狀態無法在往下擴展,所以他的最佳狀態就只能是他的父節點狀態。
對於一個父節點,最佳狀態就是當前在搜索的那一條路徑。如果這條路徑又回溯回來,就將最佳狀態設爲下一條搜索路徑。如果這個節點被訪問過,但是狀態又發生改變的話,就需要從closed表中取出,再重新加入。

至於對下一個狀態的尋找,主要是分別根據以上所列幾種狀態轉移來判斷,如果沒有足夠的人轉移,則返回;狀態生成後,還需要判斷牧師與魔鬼的數量是否符合規則,如果不符合返回null。

搜索結束後,得到的結果全部存在了closed表中,closed表中存放的是一個個狀態,每個狀態都包含了自身信息,以及下一個最佳轉移狀態。通過這個轉移,就可以得到一條通向結果的路徑。

3.DFS生成結果

將closed表中的元素全部打印出來得到以下結果:(牧師魔鬼的數量只有在左邊岸上的數量,船的狀態:False表示在左岸,True表示在右岸)

priest: 3 devil: 2 boat: True
Next: 3 3 False

priest: 3 devil: 3 boat: False
Next: 3 1 True

priest: 3 devil: 1 boat: True
Next: 3 2 False

priest: 2 devil: 2 boat: True
Next: 3 2 False

priest: 3 devil: 2 boat: False
Next: 3 0 True

priest: 3 devil: 0 boat: True
Next: 3 1 False

priest: 3 devil: 1 boat: False
Next: 1 1 True

priest: 1 devil: 1 boat: True
Next: 2 2 False

priest: 2 devil: 2 boat: False
Next: 0 2 True

priest: 0 devil: 2 boat: True
Next: 0 3 False

priest: 0 devil: 3 boat: False
Next: 0 1 True

priest: 0 devil: 1 boat: True
Next: 1 1 False

priest: 1 devil: 1 boat: False
Next: 0 0 True

priest: 0 devil: 0 boat: True
Next: NULL

priest: 0 devil: 1 boat: False
Next: 0 0 True

priest: 0 devil: 2 boat: False
Next: 0 0 True

爲了更直觀地看結果,我按照以上信息,做了一個圖:
在這裏插入圖片描述
箭頭方向代表尋找最優解的路徑方向,每一個狀態都有一個最優的轉移狀態,這也是智能提示所做的工作:幫助玩家從當前狀態更快走到結束狀態。也就是判斷當前玩家的狀態,然後根據next來進行轉移。

更改Controller

在Controller開始,就通過AI的類,使用DFS計算出所有狀態的轉移路徑,這樣就會存在AI類中的closed表裏面,隨時可以取用。

實現交互功能,首先需要添加一個新的接口,也就是我們新加的功能,並且實現它:

public void getTips() {
    if (forbid) return;
    
    if (boat.getCount()[0] != 0 || boat.getCount()[1] != 0) {
        for (int i = 0; i < 2; i ++) {
            if (boat.getChar(i) != null)
                setCharacterPosition(boat.getChar(i));
        }
    }

    int[] count = leftBank.getCount();
    int d = count[0];
    int p = count[1];
    bool b = boat.getLR()==1;
    State current = new State(p,d,b);
    State next = AI.closed.Find((State s) => {return s.isEqual(current);}).best_way;
    Debug.Log("current: " + current);
    Debug.Log("next: " + next);
    if (next == null) return;
    if (b) {
        int d2 = next.devil - d;
        int p2 = next.priest - p;
        while (d2 > 0 || p2 > 0) {
            for (int i = 0; i < 6; i ++) {
                if (characters[i].getBank() != null && characters[i].getBank().getLR() == 1) {
                    if (d2 > 0 && characters[i].getMan() == "Devil") {
                        setCharacterPosition(characters[i]);
                        d2 --;
                        break;
                    }
                    if (p2 > 0 && characters[i].getMan() == "Priest") {
                        setCharacterPosition(characters[i]);
                        p2 --;
                        break;
                    }
                }
                if (i==5){
                    Debug.Log("Err");
                    return;
                }
            }
        }
    }
    else {
        int d2 = -next.devil + d;
        int p2 = -next.priest + p;
        while (d2 > 0 || p2 > 0) {
            for (int i = 0; i < 6; i ++) {
                if (characters[i].getBank() != null && characters[i].getBank().getLR() == 0) {
                    if (d2 > 0 && characters[i].getMan() == "Devil") {
                        setCharacterPosition(characters[i]);
                        d2 --;
                        break;
                    }
                    if (p2 > 0 && characters[i].getMan() == "Priest") {
                        setCharacterPosition(characters[i]);
                        p2 --;
                        break;
                    }
                }
                if (i==5){
                    Debug.Log("Err");
                    return;
                }
            }
        }
    }
    MoveBoat();
}

首先統計當前人數以確定當前狀態,爲了方便統計,所以需要先把船上的角色先重新移回岸上(之前的接口設計不完善),由於之前移動角色是用到了動作,有一個時間的問題,這裏代碼是連續執行的,就會起矛盾,因爲這裏直接調用了move的動作函數,但是又不能直接設置回調,所以難以修改。所以新建了一個函數,直接改變角色的位置,取消了動作執行的過程。
統計人數並且得出狀態後,根據狀態的next,構建一個目標狀態,根據這個目標的狀態選擇上下船的人數,最後執行moveBoat()完成一次提示。而這個接口可以綁定在UI的一個按鈕上(使用IMGUI實現),然後按鈕被調用就執行提示。

效果展示

在這裏插入圖片描述

發佈了31 篇原創文章 · 獲贊 11 · 訪問量 9353
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章