Tic-Tac-Toe minimax算法不适用于4x4板

时间:2015-08-11 06:58:35

标签: java algorithm loops tic-tac-toe minimax

所以我过去3周一直在研究这个项目。我设法让minimax函数尽早开始使用3x3电路板,但是当我尝试将它用于4x4电路板时出现问题,即Java堆空间错误。从那时起,在Alpha beta修剪的帮助下,我设法从aprox中降低了minimax函数中所需的minimax调用次数。 59000到16000到11000,然后最终到8000个呼叫(假设已经填充了一个插槽的电路板的初始minimax调用)。然而,现在的问题是该方法只是继续运行4x4游戏。它只是不停地调用自己,没有错误,没有结果,没有任何结果。从理论上讲,我看到它的方式,我的功能应该适用于任意的电路板尺寸,唯一的问题是内存。现在,由于我已经大大减少了我的功能的记忆贪婪,我预计它会起作用。嗯,它适用于3x3。但是,它并不适用于4x4。 该函数的功能简要说明: 该函数返回一个大小为2的数组,其中包含所有可能的下一个移动中最有利的下一步移动以及预期从该移动中获得的分数。评分系统很简单。 O赢得+10,X赢得-10,平局0。该功能当然是递归的。在其中,您将找到某些快捷方式,可以减少对自身所需的调用次数。例如,如果它的X转并且返回的分数是-10(这是X的最佳可能分数),则退出循环,即停止观察来自该状态的其他可能的移动。这是类State的代码:

private String [] state;    //Actual content of the board
private String turn;        //Whose turn it is
private static int n;       //Size of the board

public State(int n) {
    state = new String[n*n];
    for(int i = 0; i < state.length; i++) {
        state[i] = "-";
    }
    State.n = n;
}


public int[] newminimax47(int z) {
    int bestScore = (turn == "O") ? +9 : -9;    //X is minimizer, O is maximizer
    int bestPos = -1;
    int currentScore;
    int lastAdded = z;
    if(isGameOver() != "Not Gameover") {
        bestScore= score();
    }
    else {
        int i = 0;
        for(int x:getAvailableMoves()) {
            if(turn == "X") {   //X is minimizer
                setX(x);
                currentScore = newminimax47(x)[0];
                if(i == 0) {
                    bestScore = currentScore;
                    bestPos = x;
                    if(bestScore == -10)
                        break;
                }
                else if(currentScore < bestScore) {
                    bestScore = currentScore;
                    bestPos = x;
                    if(bestScore == -10)
                        break;
                }
            }
            else if(turn == "O") {  //O is maximizer
                setO(x);
                currentScore = newminimax47(x)[0];
                if(i == 0) {
                    bestScore = currentScore;
                    bestPos = x;
                    if(bestScore == 10)
                        break;
                }

                else if(currentScore > bestScore) {
                    bestScore = currentScore;
                    bestPos = x;
                    if(bestScore == 10)
                        break;
                }
            }
            i++;
        }
    }
    revert(lastAdded);
    return new int [] {bestScore, bestPos};
}

newminimax47()使用的补充函数:

isGameOver():

public String isGameOver() {
    if(n == 3) {
        //Rows 1 to 3
        if((state[0] != "-") && (state[0] == state[1]) && (state[1] == state[2]))
            return (state[0] == "X") ? "X Won" : "O Won";
        else if((state[3] != "-") && (state[3] == state[4]) && (state[4] == state[5]))
            return (state[3] == "X") ? "X Won" : "O Won";
        else if((state[6] != "-") && (state[6] == state[7]) && (state[7] == state[8]))
            return (state[6] == "X") ? "X Won" : "O Won";

        //Columns 1 to 3
        else if((state[0] != "-")&&(state[0] == state[3]) && (state[3] == state[6]))
            return (state[0] == "X") ? "X Won" : "O Won";
        else if((state[1] != "-") && (state[1] == state[4]) && (state[4] == state[7]))
            return (state[1] == "X") ? "X Won" : "O Won";
        else if((state[2] != "-") && (state[2] == state[5]) && (state[5] == state[8]))
            return (state[2] == "X") ? "X Won" : "O Won";

        //Diagonals
        else if((state[0] != "-") && (state[0]==state[4]) && (state[4] == state[8]))
            return (state[0] == "X") ? "X Won" : "O Won";
        else if((state[6] != "-") && (state[6] == state[4]) && (state[4] == state[2]))
            return (state[6] == "X") ? "X Won" : "O Won";

        //Checking if draw
        else if((state[0] != "-") && (state[1]!="-") && (state[2] != "-") && (state[3]!="-") &&
                (state[4] != "-") && (state[5] != "-") && (state[6] != "-") && (state[7] != "-") &&
                (state[8] != "-"))
            return "Draw";
        else
            return "Not Gameover";
    }
    else {
        //Rows 1 to 4
        if((state[0] != "-") && (state[0] == state[1]) && (state[1] == state[2]) && (state[2] == state[3]))
            return (state[0] == "X") ? "X Won" : "O Won";
        else if((state[4] != "-") && (state[4] == state[5]) && (state[5]==state[6]) && (state[6] == state[7]))
            return (state[4] == "X") ? "X Won" : "O Won";
        else if((state[8] != "-") && (state[8] == state[9]) && (state[9]==state[10]) && (state[10] == state[11]))
            return (state[8] == "X") ? "X Won" : "O Won";
        else if((state[12] != "-") && (state[12] == state[13]) &&(state[13] == state[14]) && (state[14] == state[15]))
            return (state[12] == "X") ? "X Won" : "O Won";

        //Columns 1 to 4
        else if((state[0] != "-") && (state[0] == state[4]) && (state[4] == state[8]) && (state[8] == state[12]))
            return (state[0] == "X") ? "X Won" : "O Won";
        else if((state[1] != "-") && (state[1] == state[5]) && (state[5] == state[9]) && (state[9] == state[13]))
            return (state[1] == "X") ? "X Won" : "O Won";
        else if((state[2] != "-") && (state[2] == state[6]) && (state[6] == state[10]) && (state[10] == state[14]))
            return (state[2] == "X") ? "X Won" : "O Won";
        else if((state[3] != "-") && (state[3] == state[7]) && (state[7] == state[11]) && (state[11] == state[15]))
            return (state[3] == "X") ? "X Won" : "O Won";

        //Diagonale
        else if((state[0] != "-") && (state[0] == state[5]) && (state[5] == state[10]) && (state[10] == state[15]))
            return (state[0] == "X") ? "X Won" : "O Won";
        else if((state[12] != "-") && (state[12] == state[9]) &&  (state[9] == state[6]) && (state[6] == state[3]))
            return (state[0] == "X") ? "X Won" : "O Won";

        //Pruefe ob Gleichstand
        else if((state[0] != "-") && (state[1] != "-") && (state[2] != "-") && (state[3]!="-") &&
                (state[4] != "-") && (state[5] != "-") && (state[6] != "-") && (state[7] != "-") &&
                (state[8] != "-") && (state[9] != "-") && (state[10] != "-") && (state[11] != "-") &&
                (state[12] != "-") && (state[13] != "-") && (state[14] != "-") && (state[15] != "-")) 
            return "Draw";
        else
            return "Not Gameover";
    }   
}

请原谅isGameOver()方法的直率,它只是检查棋盘的状态(即Win,Draw,Game not Over)

getAvailableMoves()方法:

public int[] getAvailableMoves() {
    int count = 0;
    int i = 0;
    for(int j = 0; j < state.length; j++) {
        if(state[j] == "-")
            count++;
    }
    int [] availableSlots = new int[count];
    for(int j = 0; j < state.length; j++){
        if(state[j] == "-")
            availableSlots[i++] = j;        
    }
    return availableSlots;
}

此方法仅返回一个包含所有可用下一步动作的int数组(关于当前状态),如果没有可用的动作或游戏结束,则返回空数组。

得分()方法:

public int score() {
    if(isGameOver() == "X Won")
        return -10;
    else if(isGameOver() == "O Won")
        return +10;
    else 
        return 0;
}

setO(),setX()和revert():

public void setX(int i) {
    state[i] = "X";
    turn = "O";
    lastAdded = i;
}
public void setO(int i) {
    state[i] = "O";
    turn = "X";
    lastAdded = i;
}
public void revert(int i) {
    state[i] = "-";
    if(turn == "X")
        turn = "O";
    else
        turn = "X";
}

我的主要方法在3x3游戏中看起来像这样:

public static void main(String args[]) {
    State s = new State(3);
    int [] ScoreAndRecommendedMove = new int[2];
    s.setX(8);
    ScoreAndRecommendedMove = s.newminimax47(8);
    System.out.println("Score: "+ScoreAndRecommendedMove[0]+" Position: "+ ScoreAndRecommendedMove[1]);
}

在这个游戏中,X已经开始在第8位移动游戏。本例中的方法将返回

Score: 0 Position: 4  

意味着O最有希望的举动是在第4位,而在最坏的情况下,将得分为0(即平局)。

以下图片旨在让您了解newminimax47()的工作原理。在这种情况下,我们的起始状态(板)被赋予数字1.注意:数字表示创建被认为状态的优先级。 1在2之前创建,2在3之前创建,3在4之前创建,依此类推。enter image description here

在这种情况下,最终返回状态1的分数和位置将是

Score: 0 Position: 6

来自州8。

注意:您看到的代码只是实际State类的代码段。这些片段本身应该允许你重新创建和使用newminimax47函数没有问题(至少3x3)。您可能发现的任何错误都不是真正的错误,它们根本没有包含在我复制的代码段中,代码应该在没有它们的情况下工作。例如,setO和setX函数中的lastAdded变量不包含在这里的片段中,但我刚刚意识到你不需要它来使用minimax函数,所以你可以只是注释掉它。

2 个答案:

答案 0 :(得分:6)

我玩了你的代码,还有很多话要说

<强>错误

首先是一个bug。我不认为你的代码实际上适用于3x3板。问题是您revert移动到电路板的位置。您只需在newminimax47方法的末尾执行此操作一次,即使在您添加的方法移动到for循环内的板上也是如此。这意味着调用该方法不仅会计算某些内容,还会更改电路板状态,而其余的代码则不会这样做。

请尽快删除revert所在的位置,并尽快恢复:

setX(x);                                                                                                                                                                                                                                             
currentScore = newminimax47(x)[0];                           
revert(x);       

这也意味着您不需要lastAdded变量。

播放

如果您实际上违反了自己的算法,那么看到发生的事情要容易得多。将方法添加到State类

public void dump() {                                                        
    for (int y = 0; y < n; y++) {                                           
        for (int x = 0; x < n; x++) {                                       
            System.out.print(state[y * n + x]);                             
        }                                                                   
        System.out.println();                                               
    }                                                                       
}

在你的主要内容

public void play() {                                                        
    State s=new State(3);                                                   
    Scanner in = new Scanner (System.in);                                   
    while (s.isGameOver().equals("Not Gameover")) {                         
        int[] options = s.getAvailableMoves();                              
        s.dump();                                                           
        System.out.println ("Your options are " + Arrays.toString(options));
        int move = in.nextInt();                                            
        s.setX(move);                                                       
        int [] ScoreAndRecommendedMove=new int[2];                          
        ScoreAndRecommendedMove=s.newminimax47(0);                           
        System.out.println("Score: "+ScoreAndRecommendedMove[0]+" Position: "+ ScoreAndRecommendedMove[1]);
        s.setO(ScoreAndRecommendedMove[1]);                                 
    }                                                                       
    s.dump();                                                               
}

你实际上可以对抗它。在3x3板上,这对我来说很好。不幸的是,我估计计算4x4的第一步是我的电脑大约需要48小时。

数据类型

您选择的数据类型通常有点......奇怪。如果您想记住单个字符,请使用char代替String。如果您想要返回是/否决定,请尝试使用boolean。程序的某些部分可以用较少的代码替换。但这不是你的问题,所以... ...

<强>算法

好的,那么minimax解决这个问题有什么不对? 假设前四个动作是X5,O8,X6 O7。另一种可能性是用X5,O7,X6,O8开始游戏。另一个是X6,O7,X5,O8。最后是X6,O8,X5,O7。

游戏前四个动作的所有四种可能性导致完全相同的游戏状态。但是minimax不会认识到它们是相同的(基本上没有并行分支的记忆)所以它将计算它们全部四个。如果您进行更深入的搜索,计算每个电路板状态的次数会迅速增加。

可能的游戏数量远远超过可能的董事会状态数量。估计游戏数量:首先有16个可能的移动,然后是15个,然后是14,13,......等等。粗略的估计是16!,虽然minimax不必计算所有这些,因为其中许多将在第16次移动之前完成。

对游戏状态数量的估计是:棋盘上的每个方格都可以是空的,或者是X,或者是O.那就是3 ^ 16个棋盘。并非所有这些都是有效的电路板,因为电路板上的X数量最多可以是Os的数量,但仍然接近3 ^ 16。

16!可能的游戏大约是3 ^ 16个可能的棋盘状态的五十倍。这意味着我们大约只计算每个电路板五十万次而不是一次。

解决方案是开始记住你计算的每个游戏状态。每次调用递归函数时,首先检查您是否已经知道答案,如果是,则返回旧答案。这是一种名为memoization的技术。

<强>记忆化

我将介绍如何在使用您已选择的数据结构时添加memoization(即使我不同意它们)。要进行记忆,您需要一个集合,您可以在其上快速添加和快速查找。列表(例如ArrayList)对我们没有好处。添加值很快,但在长列表中进行查找非常慢。有一些选项,但最容易使用的是HashMap。为了使用HashMap,您需要创建代表您的状态的内容,并将其用作关键字。最简单的方法是只用一个String来表示你的电路板中包含所有X / O / - 符号。

所以添加

Map<String,int[]> oldAnswers = new HashMap<String,int[]>();                                                                  

到您的State对象。

然后在newminimax47方法的开头创建表示State的String,并检查我们是否已经知道答案:

    String stateString = "";                                                
    for (String field : state) stateString += field;                        
    int[] oldAnswer = oldAnswers.get(stateString);                          
    if (oldAnswer != null) return oldAnswer;

最后,当你计算一个新答案时newminimax47结束时,你不仅应该返回它,还要将它存储在地图中:

    int[] answer = {bestScore, bestPos};                                    
    oldAnswers.put (stateString, answer);                                   
    return answer;

随着记忆的到位,我能够对你的代码进行4x4游戏。第一步仍然很慢(20秒),但之后计算的一切都非常快。如果您想进一步加快速度,可以查看alpha beta pruning。但改进不会像记忆那样接近。另一种选择是使用更有效的数据类型。它不会降低算法的理论顺序,但仍然可以轻松地使其快5倍。

答案 1 :(得分:0)

正如user3386109所解释的,这里的问题是你计算一切的次数。有一些事情可以帮助你,考虑一个N尺寸的网格:

  • 如果用户的符号少于N,则用户无法获胜,因此您可以在isGameOver函数中将其用作第一次检查
  • 你要做的第一件事就是如果有可能他的下一步行动是胜利的话,就要防止你的对手获胜
  • 通过每次移动递增计数器,跟踪每行和每列中以及两个对角线中有多少“X”和“O”。如果有相同符号的N-1,则下一个将是你或你的对手的获胜动作。
  • 通过这样做,您可以轻松判断哪个是最佳动作,因为那样:
    • 如果你有一个获胜的举动,你将符号放在那里
    • 检查你的对手,如果他在同一行/列/对角线上有N-1个符号你就把它放在那里
    • 如果你的对手在某个地方有比你更多的符号,你甚至可以离开这个地方(这意味着+1或+2,取决于谁开始游戏)
    • 如果不是这样,你将下一个符号放在你有更多符号的行/列/对角线上
    • 如果你在某些地方有相同数量的符号,你只需将它放在你的对手有更多符号的地方
    • 如果你和你的对手完全平稳,那就去做你自己的策略(随机也不会坏,我猜:-))

除非你真的需要它(例如作为家庭作业),否则我不会使用递归。

正如旁注:我认为让实际上布尔函数返回一个字符串然后将其与固定值进行比较是不错的做法。 isGameOver函数的true / false返回值对我来说看起来好多了。