Devil and Priest Smart Assist

Article directory

Experimental content

(After running through my own algorithm, I found that in the reference blog, there is actually something wrong. Its state diagram is represented by the number of devils and priests on the initial bank, and the other bank can pass 3 Get it by subtracting the number of characters on the starting side.)
The state diagram cut from the reference blog:
insert image description here
Anyone who knows the rules of the game knows that this state cannot exist, because the other side is 1P2D, and the game is over.
In the entire state diagram, unless both are 2, it is impossible to have a state like 2P. This should be a small mistake of the blogger.

Automatic generation of state diagrams (using DFS)

The automatic generation process can be realized by using a search algorithm. In fact, we can all know that the number of states in the entire state diagram is actually not many (after all, it is suitable for users to play, and the difficulty of the game is not too high), so the search process is actually very fast. can be solved. The key lies in how to design the state transfer and how to realize its program? First of all, each state needs to be represented in the search algorithm, then the state-to-state transition is represented, and finally the algorithm design (including the generation of Closed tables, optimal paths, etc.) is required.

1. Status representation

Each state can be seen as composed of two parts: the number of characters and the position of the ship.
The number of characters can be divided into: the number of people on both sides of the river, the number of priests on each side, and the number of devils.
Since it is a depth-first search, it is necessary to record the state of the next node, which is similar to a tree structure. In order to facilitate the two-way transfer between each state, a doubly linked list can be constructed to point to the parent node.
The structure is as follows:

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();
    }
}
  •  

This class defines some of the above-mentioned variables that can represent the state. When representing the number of priests and devils, it is only necessary to record the number of the starting bank (the left bank). Since the total number is known, it can be obtained by subtracting the left bank from the total number. There is no need to store the number of the right bank.
Several constructors with different signatures are defined to facilitate state creation.
The function is overloaded Equals, which is convenient to use collection structures such as List for storage.
The function is overloaded ToString, which is convenient for printing the information of the current state.

2. Implementation of DFS algorithm

DFS only needs to transfer from one state to another, and it needs to define the operation of the transfer.

  1. It is clear that someone must be on board for the transfer to occur;
  2. People can only be carried on the shore where there is a boat;
  3. And the transfer of the ship must be from one shore to another;

In addition to the above fixed transfer rules, the rest of the rules are defined as follows:

  • Transfer one priest/two priests at a time
  • Transfer one devil/two devils at a time
  • transfer one devil and one priest at a time

The method of transfer is to reduce the number of people on the side with the boat, and increase the number of people on the other side; but our state only records the number of people on the left bank, so when the boat is on the left bank, the number of people will decrease when the boat is transferred; when the boat is on the right bank, the number of people will be transferred Increase.

Every deep search is to find one of these transferred states and continue to search. Note that the search can only be continued in a valid state. A valid state is defined as one that does not trigger a game over condition.
Algorithm class code:

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;
    }
}
  •  

Create a closed table to store the nodes that have been visited. During the search process, use list.contain to determine whether the current state has been visited, and if it has been visited, it will not be expanded.
I believe that the process of deep search is very familiar, so I won't start it anymore. It's just that the deep search here actually needs to traverse all the states. Even if a correct state transition path is found, it will not stop immediately. Instead, it needs to find all the states and find out the best way to go next.
For example: a state cannot be expanded downwards, so its best state can only be the state of its parent node.
For a parent node, the best state is the path that is currently being searched. If this path is backtracked, the best state is set as the next search path. If this node has been visited, but the state changes again, it needs to be taken out from the closed table and re-joined.

As for the search for the next state, it is mainly judged according to the state transitions listed above. If there are not enough people to transfer, return; Match returns null.

After the search is over, all the obtained results are stored in the closed table, which stores states one by one, and each state contains its own information and the next best transition state. Through this transfer, a path to the result can be obtained.

3. DFS generated results

Print all the elements in the closed table to get the following results: (The number of priests and devils is only on the left bank, and the status of the ship: False means it is on the left bank, and True means it is on the right bank)

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
  •  

In order to see the results more intuitively, I made a picture based on the above information: the
insert image description here
direction of the arrow represents the direction of the path to find the optimal solution, and each state has an optimal transition state, which is also the work of the smart prompt: help The player moves from the current state to the end state faster. That is to judge the state of the current player, and then transfer according to next.

Change Controller

From the beginning of the Controller, use DFS to calculate the transfer path of all states through the AI ​​class, so that it will be stored in the closed table in the AI ​​class and can be accessed at any time.

To realize the interactive function, we first need to add a new interface, which is our newly added function, and implement it:

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();
}
  •  

First count the current number of people to determine the current state. For the convenience of statistics, it is necessary to move the characters on the ship back to the shore first (the previous interface design was not perfect). Since the previous movement of the characters used actions, there is a time problem. Here If the code is executed continuously, there will be contradictions, because the action function of move is directly called here, but the callback cannot be directly set, so it is difficult to modify. So a new function was created to directly change the position of the character and cancel the process of executing the action.
After counting the number of people and obtaining the state, build a target state according to the state of next, select the number of people who get on and off the boat according to the state of the target, and finally execute moveBoat() to complete a prompt. And this interface can be bound to a button in the UI (implemented using IMGUI), and then the prompt will be executed when the button is called.

Show results

insert image description here

Guess you like

Origin blog.csdn.net/weixin_40552127/article/details/112786804