Минимаксный алгоритм TicTacToe возвращает неожиданные результаты в играх 4x4

В моем методе newminimax499 у меня есть минимаксный алгоритм, который использует мемоизацию и альфа-бета-обрезку. Этот метод обычно работает для игр 3x3, однако, когда я играю в игры 4x4, я получаю странный, неожиданный выбор позиции для компьютера. Он по-прежнему никогда не проигрывает, но, похоже, он не играет на победу. Для иллюстрации проблемы вот сценарий из 2х игр в 3х3 и 4х4. Во-первых, это сценарий из игры 3x3, в которой игрок Х и делает первый ход:введите здесь описание изображения

Это неплохо, на самом деле это то, что можно было бы ожидать от компьютера. Теперь взглянем на сценарий из игры 4x4. Опять O — это компьютер, а X запускается: введите здесь описание изображения

Как вы можете видеть, компьютер просто размещает O в систематическом порядке один за другим и нарушает этот порядок, чтобы заблокировать X только тогда, когда у него есть потенциальный выигрыш. Это очень оборонительная игра, в отличие от игры 3х3. Так почему же метод ведет себя по-разному для 3x3 и 4x4?

Вот код:

//This method returns a 2 element int array containing the position of the best possible 
//next move and the score it yields. Utilizes memoization and  alpha beta 
//pruning to achieve better performance. 
public int[] newminimax499(int a, int b){
    //int bestScore = (turn == 'O') ? +9 : -9;  //X is minimizer, O is maximizer
    int bestPos=-1;
    int alpha= a;
    int beta= b;
    int currentScore;
    //boardShow();
    String stateString = "";                                                
    for (int i=0; i<state.length; i++) 
        stateString += state[i];                        
    int[] oldAnswer = oldAnswers.get(stateString);                          
    if (oldAnswer != null) 
        return oldAnswer;
    if(isGameOver()!='N'){
        int[] answer = {score(), bestPos};                                    
        oldAnswers.put (stateString, answer);                                   
        return answer;
    }
    else{
        for(int x:getAvailableMoves()){
            if(turn=='X'){  //X is minimizer
                setX(x);
                //System.out.println(stateID++);
                currentScore = newminimax499(alpha, beta)[0];
                revert(x);
                if(currentScore<beta){
                    beta=currentScore;
                    bestPos=x;
                }
                if(alpha>=beta){
                    break;
                }
            }
            else {  //O is maximizer
                setO(x);
                //System.out.println(stateID++);
                currentScore = newminimax499(alpha, beta)[0];
                revert(x);
                if(currentScore>alpha){
                    alpha=currentScore;
                    bestPos=x;
                }
                if(alpha>=beta){
                    break;
                }
            }
        }
    }
    if(turn=='X'){
        int[] answer = {beta, bestPos};                                    
        oldAnswers.put (stateString, answer);                                   
        return answer;
    }
    else { 
        int[] answer = {alpha, bestPos};                                    
        oldAnswers.put (stateString, answer);                                   
        return answer;
    }
}

Ниже приведены другие компоненты и дополнительные методы, необходимые для запуска кода. Поля и конструктор, используемые в моем классе State2:

private char [] state;  //Actual content of the board
private char turn;  //Whose turn it is
private Map<String,int[]> oldAnswers; //Used for memoization. It saves every state along with the score it yielded which allows us to stop exploring the children of a certain node if a similar node's score has been previously calculated. The key is the board state(i.e OX------X for example), the int array is a 2 element array containing the score and position of last placed seed of the state.  
private Map<Integer, int []> RowCol; //A mapping of positions from a board represented as a normal array to a board represented as a 2d array. For example: The position 0 maps to 0,0 on a 2d array board, 1 maps to 0,1 and so on.
private static int n;   //Size of the board
private static int stateID; //An simple incrementer used to show number of recursive calls in the newminiax49 method. 
private static int countX, countO; //Number of placed Xs and Os
private static int lastAdded; //Position of last placed seed
private char [][] DDState; //A 2d array representing the board. Contains the same values as state[]. Used for simplicity in functions that check the state of the board.

public State2(int n){
    int a=0;
    State2.n=n;
    state=new char[n*n];
    RowCol=new HashMap<Integer, int []>();
    countX=0;
    countO=0;
    //Initializing the board with empty slots
    for(int i = 0; i<state.length; i++){
        state[i]='-';
    }
    //Mapping
    for(int i=0; i<n; i++){
        for(int j=0; j<n; j++){
            RowCol.put(a, new int[]{i, j});
            a++;
        }
    }
    a=0;
    DDState=new char[n][n];
    //Initializing the 2d array with the values from state[](empty slots)
    for(int i=0; i<n; i++){
        for(int j=0; j<n; j++){
            DDState[i][j]=state[a];
            a++;
        }
    }
    oldAnswers = new HashMap<String,int[]>();
}

Дополнительные методы:

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

isGameOver2() просто проверяет текущее состояние доски на предмет того, окончена ли игра. возвращает символы 'X', 'O', 'D' и 'N', которые обозначают X выиграл, О выиграл, Ничья и Не завершение игры соответственно.

public char isGameOver2(){
    char turnOpp;
    int count;
    if(turn=='X'){
        count=countO;
        turnOpp='O';
    }
    else {
        count=countX;
        turnOpp='X';
    }
    if(count>=n){ 
        for(int i=0; i<n; i++){
            if(DDState[i][RowCol.get(lastAdded)[1]]!=turnOpp)
                break;
            if(i==(n-1)){
                return turnOpp;
            }
        }

        //Check row for win
        for(int i=0; i<n; i++){
            if(DDState[RowCol.get(lastAdded)[0]][i]!=turnOpp)
                break;
            if(i==(n-1)){
                return turnOpp;
            }
        }

        //Check diagonal for win
        if(RowCol.get(lastAdded)[0] == RowCol.get(lastAdded)[1]){

            //we're on a diagonal
            for(int i = 0; i < n; i++){
                if(DDState[i][i] != turnOpp)
                    break;
                if(i == n-1){
                    return turnOpp;
                }
            }
        }

        //check anti diagonal 

        for(int i = 0; i<n; i++){
            if(DDState[i][(n-1)-i] != turnOpp)
                break;
            if(i == n-1){
                return turnOpp;
            }
        }

        //check for draw
        if((countX+countO)==(n*n))
            return 'D';

            }
    return 'N';
}

boardShow, возвращает матричное отображение текущего состояния доски:

public void boardShow(){
    if(n==3){
        System.out.println(stateID);
        for(int i=0; i<=6;i+=3)
            System.out.println("["+state[i]+"]"+" ["+state[i+1]+"]"+" ["+state[i+2]+"]");
        System.out.println("***********");
    }
    else {
        System.out.println(stateID);
        for(int i=0; i<=12;i+=4)
            System.out.println("["+state[i]+"]"+" ["+state[i+1]+"]"+" ["+state[i+2]+"]"+" ["+state[i+3]+"]");
        System.out.println("***********");
    }   
}

score — это простая функция оценки, которая возвращает +10 при выигрыше O, -10 при выигрыше X и 0 при ничьей:

public int score(){
    if(isGameOver2()=='X')
        return -10;
    else if(isGameOver2()=='O')
        return +10;
    else 
        return 0;
}

Селекторы семян:

//Sets an X at a certain location and updates the turn, countX and lastAdded variables
public void setX(int i){
    state[i]='X';
    DDState[RowCol.get(i)[0]][RowCol.get(i)[1]]='X';
    turn='O';
    countX++;
    lastAdded=i;
}

//Sets an O at a certain location and updates the turn, countO and lastAdded variables
public void setO(int i){
    state[i]='O';
    DDState[RowCol.get(i)[0]][RowCol.get(i)[1]]='O';
    turn='X';
    countO++;
    lastAdded=i;
}

Revert, просто отменяет ход. Например, если X был помещен в позицию 0, revert(0) устанавливает '-' на его место и обновляет переменные, измененные setX:

public void revert(int i){
    state[i]='-';
    DDState[RowCol.get(i)[0]][RowCol.get(i)[1]]='-';
    if(turn=='X'){
        turn = 'O';
        countO--;
    }
    else {
        turn = 'X';
        countX--;
    }
}

Как может выглядеть основной метод:

public static void main(String[] args) {
    State2 s=new State2(4);
    int [] results=new int[2];
    s.setX(0);
    long startTime = System.currentTimeMillis();
    results=s.newminimax499(Integer.MIN_VALUE,Integer.MAX_VALUE);
    long endTime = System.currentTimeMillis();
    System.out.println("Score: "+results[0]+" Position: "+ results[1]);
    System.out.println("Run time: " + (endTime-startTime));
    s.boardShow();

}

person Omar Sharaki    schedule 20.08.2015    source источник
comment
есть просмотр кода, не так ли?   -  person Andrew Tobilko    schedule 20.08.2015
comment
Если обе стороны играют оптимально, игра будет ничейной. Минимакс предполагает, что вы будете играть оптимально, поэтому я не думаю, что это обязательно проблема. Может быть, и я сомневаюсь, что кто-то будет внимательно читать такой большой код, но, судя по вашему описанию, алгоритм выглядит хорошо. Пробовали ли вы играть плохо и посмотреть, идет ли компьютер на победу? Если вы играете хорошо, имеет смысл закончить игру вничью. Можете ли вы на самом деле победить компьютер? Если нет, то играет хорошо.   -  person IVlad    schedule 20.08.2015
comment
@AndrewTobilko, у автора неожиданные результаты, а это означает, что код не работает должным образом или представляет собой сломанный код, что строго не по теме в обзоре кода.   -  person Quill    schedule 20.08.2015
comment
@IVlad Как бы я ни играл, плохо это или хорошо, компьютер в игре 4x4 размещает свои ОС в систематическом порядке (т. Е. Заполняет слоты в порядке возрастания от 0 до 15 везде, где он может). Единственный раз, когда он нарушает эту последовательность, это когда мой следующий ход потенциально выигрывает. В этом случае он блокирует меня, заполняя слот, который мог привести меня к победе.   -  person Omar Sharaki    schedule 20.08.2015
comment
Кстати, не так просто создать простой MINMAX, который успешно выигрывает в игре 3x3 и более высокой матричной игре. В простой игре 3х3 у вас есть один раном-токен, после чего вы можете атаковать или защищаться. Например, в 4x4 вам нужно сначала найти тактику. Возможно, это ваша проблема.   -  person MrT    schedule 20.08.2015
comment
Четверо в ряд на доске 4x4 — почти беспроигрышная игра для любого игрока, поэтому минимакс ничего не даст. Попробуйте поиграть с тем же алгоритмом на сетке 10x10, где вам нужно получить только 5 в ряд, и посмотрите, насколько умнее будет играть компьютер.   -  person durron597    schedule 20.08.2015
comment
@ durron597 Будет ли работать минимакс в игре с такими правилами? Также будет ли минимакс оптимальным для платы такого размера?   -  person Omar Sharaki    schedule 21.08.2015
comment
@ Омар, насколько я знаю, самым большим размером из четырех подряд, который был полностью минимизирован, была доска 6x7, созданная группой ученых. Невозможно полностью минимаксировать доску 10x10 с нашими сегодняшними знаниями и техникой. Тем не менее, минимакс - это решение для 10x10, но вам придется написать специализированную оценочную функцию, похожую на шахматы, и минимакс на глубину, может быть, 10 или около того.   -  person xXliolauXx    schedule 21.08.2015


Ответы (2)


Я не уверен, что здесь есть ошибка - если O играет в одной из более ранних позиций, она разветвляется, тогда как, если она играет в центре, она вызывает ничью. Предположительно, в игре 4x4 выиграть/проиграть еще сложнее, поэтому компьютер безразлично выбирает первую открытую клетку.

Ниже 1 обозначает принудительный ответ с помощью O, 2 обозначает разветвление с помощью X, а ? обозначает возможное место выигрыша.

X|O|
-+-+-
2|X|?
-+-+-
?| |1

X| |O
-+-+-
X|2|?
-+-+-
1| |?

X|2|?
-+-+-
O|X|
-+-+-
 |?|1
person David Eisenstat    schedule 20.08.2015
comment
Я согласен, что это не обязательно ошибка, компьютер просто не различает позицию, в которой вы можете помешать ему выиграть 3 разными способами или только 1 способом. Вот почему в большинстве случаев он просто делает первый непроигрышный ход. Так работает MinMax: вы всегда ожидаете от оппонента идеальной игры. Если преклир не обнаружит вынужденную победу, он просто помешает вам выиграть. - person xXliolauXx; 20.08.2015
comment
Так что, я думаю, можно с уверенностью сказать, что крестики-нолики 4x4 бессмысленны/скучны, если мы играем по этим правилам (т.е. 4 победы подряд)? - person Omar Sharaki; 20.08.2015
comment
@OmarSharaki Это один взгляд. Другой вариант заключается в том, что, возможно, есть способ оценить сложные розыгрыши как более ценные игровые состояния. - person David Eisenstat; 20.08.2015
comment
Можете ли вы уточнить значение слова «вызов»? - person Omar Sharaki; 20.08.2015
comment
@OmarSharaki Не совсем так, поэтому я взял это в кавычки. Может быть, запрограммировать более слабого противника и вознаграждать форсированные ничьи, которые, тем не менее, приводят к победам над этим противником. - person David Eisenstat; 20.08.2015

Я бросил попытки разобраться в коде — в комнате стало слишком шумно для моих коллег.

Однако я собрал свое собственное решение, которое, по моему мнению, стоит опубликовать. Он не предназначен для того, чтобы быть особенно эффективным - на самом деле я все еще жду, когда он сделает свой первый ход в тесте 4X4, но, похоже, он делает правильные вещи в тесте 3X3 (т.е. ничья против самого себя и победа над глупой стратегией). .

Он должен быть в состоянии обрабатывать доски неодинаковых размеров. Диагональ - это любая линия квадратов, идущая от одной стороны доски к другой с использованием диагональных шагов.

Как только он закончит пробег 4X4, я опубликую его ходы для сравнения.

public class TicTacToe {

    /**
     * Strategies for players.
     */
    interface Strategy {

        /**
         * Think up a place to move.
         *
         * @param board - The current board position.
         * @return Where to move.
         */
        Optional<Board.Square> think(Board board, Player player);
    }

    /**
     * Stupid strategy just finds the first empty square and plays it.
     */
    static final Strategy stupid = (Board board, Player player) -> {
        List<Board.Square> empty = board.getEmpties();
        if (empty.size() > 0) {
            return Optional.of(empty.get(0));
        }
        return Optional.empty();
    };

    /**
     * Minimax strategy.
     */
    static final Strategy minimax = new Strategy() {
        // Divide by this at each depth step.
        private static final double FACTOR = 3;

        // Memoize each discovered best move.
        Map<Player, Map<String, Board.Place>> best = new HashMap<>();

        {
            // One map for each player.
            for (Player p : Player.values()) {
                best.put(p, new HashMap<>());
            }
        }

        @Override
        public Optional<Board.Square> think(Board board, Player player) {
            Optional<Board.Square> win = Optional.empty();
            // Seen this one before?
            Board.Place seen = best.get(player).get(board.toString());
            if (seen == null) {
                double winValue = Double.NEGATIVE_INFINITY;
                for (Board.Square play : board.getEmpties()) {
                    double value = value(board, player, play, 1);
                    if (value > winValue) {
                        win = Optional.of(play);
                        winValue = value;
                    }
                }
                if (win.isPresent()) {
                    // Record my result.
                    best.get(player).put(board.toString(), win.get().place);
                } else {
                    System.out.println("No best found for " + board);
                }
            } else {
                // Reuse it.
                win = Optional.of(board.square(seen));
            }
            return win;
        }

        private double value(Board board, Player player, Board.Square move, double scale) {
            // My value.
            double value = 0;
            // Make the move.
            board.mark(player, move);
            try {
                // A win?
                if (!board.win()) {
                    // Remaining empties.
                    List<Board.Square> empties = board.getEmpties();
                    if (!empties.isEmpty()) {
                        // Record it's value.
                        double[] values = new double[empties.size()];
                        for (int i = 0; i < values.length; i++) {
                            // Try each further move negating and downscaling at each level.
                            values[i] = value(board, player.next(), empties.get(i), (scale / FACTOR) * -1);
                        }
                        // Add them up.
                        value += DoubleStream.of(values).sum();
                    }
                } else {
                    // Somebody already won.
                    value = scale;
                }
            } finally {
                //System.out.println("Value of " + board + " to player " + player + " = " + value);
                // Unroll the move.
                board.unmark(player, move);
            }
            return value;
        }
    };

    /**
     * There are exactly two players.
     */
    enum Player {

        O, X;

        /**
         * Each player has a strategy.
         *
         * Defaults to stupid.
         */
        private Strategy strategy = stupid;

        /**
         * What mark they make on the board.
         *
         * @return String representing the mark they make.
         */
        public char mark() {
            // The mark they make is the first character of their name.
            return name().charAt(0);
        }

        /**
         * Who is the next player.
         *
         * @return The other player.
         */
        public Player next() {
            return other(this);
        }

        /**
         * Who is the other player.
         *
         * @return The other player.
         */
        static Player other(Player it) {
            return it == O ? X : O;
        }

        public Strategy getStrategy() {
            return strategy;
        }

        public Player setStrategy(Strategy strategy) {
            this.strategy = strategy;
            return this;
        }
    }

    /**
     * The board.
     */
    static class Board {

        /**
         * Top left corner is 0,0. Access is [y][x].
         */
        final Square[][] board;
        // The board as columns.
        final List<List<Square>> columns;
        // The dagonals.
        final List<List<Square>> diagonals;
        // For sense checking.
        final int width;
        final int height;
        // Counts for each player - useful optimisation.
        final int[] counts = new int[Player.values().length];

        // A clear square.
        private static final char Clear = ' ';

        public Board(int width, int height) {
            this.width = width;
            this.height = height;
            board = new Square[height][];
            for (int y = 0; y < height; y++) {
                board[y] = new Square[width];
                // Fill it.
                for (int x = 0; x < width; x++) {
                    board[y][x] = new Square(x, y);
                }
            }
            // Build my columns lists.
            columns = new ArrayList<>();
            for (int x = 0; x < width; x++) {
                List<Square> column = new ArrayList<>();
                for (int y = 0; y < height; y++) {
                    column.add(board[y][x]);
                }
                columns.add(column);
            }
            // And the diagonals.
            diagonals = new ArrayList<>();
            for (int y = 0; y <= height - width; y++) {
                List<Square> diagonal = new ArrayList<>();
                // Left to right.
                for (int x = 0; x < width; x++) {
                    diagonal.add(board[y + x][x]);
                }
                diagonals.add(diagonal);
                // Right to left.
                diagonal = new ArrayList<>();
                for (int x = 0; x < width; x++) {
                    diagonal.add(board[y + x][width - 1 - x]);
                }
                diagonals.add(diagonal);
            }
        }

        public Board(int size) {
            this(size, size);
        }

        private Stream<List<Square>> asRows() {
            // Map each row to a list.
            return Arrays.stream(board).map(r -> Arrays.asList(r));
        }

        private Stream<List<Square>> asCols() {
            // Walk the columns.
            return columns.stream();
        }

        private Stream<List<Square>> asDiagonals() {
            // Walk the diagonals.
            return diagonals.stream();
        }

        private boolean winning(List<Square> row) {
            //System.out.println("winner(" + row + ")");
            if (row.isEmpty()) {
                return false;
            }
            Optional<Player> owner = row.get(0).owner;
            // First square owned.
            if (!owner.isPresent()) {
                return false;
            }
            return !row.stream()
                    //.peek(s -> System.out.println(s))
                    .anyMatch((s) -> (!s.owner.isPresent() || s.owner.get() != owner.get()));
        }

        public Square square(Place place) {
            return board[place.y][place.x];
        }

        private Optional<Player> getWinner() {
            Optional<Player> winner = Optional.empty();
            // Must be at least enough plays by one player to reach across/down the board.
            OptionalInt min = IntStream.of(counts).min();
            if (!min.isPresent() || min.getAsInt() < Math.min(width, height)) {
                // Nobody can posibly have won.
                return winner;
            }
            List<List<Square>> winners = asRows()
                    .filter(r -> winning(r))
                    .collect(Collectors.toList());
            if (winners.isEmpty()) {
                winners = asCols()
                        .filter(r -> winning(r))
                        .collect(Collectors.toList());
            }
            if (winners.isEmpty()) {
                winners = asDiagonals()
                        .filter(r -> winning(r))
                        .collect(Collectors.toList());
            }
            if (!winners.isEmpty()) {
                winner = winners.get(0).get(0).owner;
            }
            return winner;
        }

        private boolean isDrawn() {
            // All square occupied - counts add up to width*height.
            return IntStream.of(counts).sum() == width * height;
        }

        private boolean win() {
            return getWinner().isPresent();
        }

        /**
         * One square of the board.
         *
         * Remember that a Square is ON a board - i.e. it is a sub-object of a Board. Do not attempt to memoize Squares as they will hold a reference to their parent board.
         */
        class Square {

            // The owner of it.
            Optional<Player> owner = Optional.empty();
            // where it is on the board.
            final Place place;

            public Square(Place place) {
                this.place = place;
            }

            public Square(int x, int y) {
                this(new Place(x, y));
            }

            public char getMark() {
                return owner.isPresent() ? owner.get().mark() : Clear;
            }

            public boolean isTaken() {
                return owner.isPresent();
            }

            @Override
            public String toString() {
                return place + "(" + (owner.isPresent() ? owner.get().toString() : "") + ")";
            }

            private void take(Player player) {
                if (!isTaken()) {
                    owner = Optional.of(player);
                    // Keep track of the counts.
                    counts[player.ordinal()] += 1;
                } else {
                    throw new IllegalStateException("Attempt to take taken square!"
                            + " Square=" + this
                            + " Player=" + player
                            + " board=" + Board.this.toString()
                    );
                }
            }

            private void release(Player player) {
                if (isTaken()) {
                    if (owner.get() == player) {
                        owner = Optional.empty();
                        // Keep track of the counts.
                        counts[player.ordinal()] -= 1;
                    } else {
                        throw new IllegalStateException("Attempt to release other player's square!"
                                + " Square=" + this
                                + " Player=" + player
                                + " board=" + Board.this.toString()
                        );
                    }
                } else {
                    throw new IllegalStateException("Attempt to release untaken square!"
                            + " Square=" + this
                            + " Player=" + player
                            + " board=" + Board.this.toString()
                    );
                }
            }

        }

        private List<Square> getEmpties() {
            // Walk all squares.
            return Arrays.stream(board)
                    // Unroll each row.
                    .flatMap(l -> Arrays.stream(l))
                    // All not taken.
                    .filter(s -> !s.isTaken())
                    // As a list.
                    .collect(Collectors.toList());
        }

        /**
         * A place on the board.
         */
        class Place {

            final int x;
            final int y;

            public Place(int x, int y) {
                // Sense check.
                if (x < 0 || x >= width) {
                    throw new IllegalArgumentException("Off board: x = " + x);
                }
                if (y < 0 || y >= height) {
                    throw new IllegalArgumentException("Off board: x = " + x);
                }
                this.x = x;
                this.y = y;
            }

            @Override
            public String toString() {
                return "{" + x + "," + y + '}';
            }

        }

        /**
         * Mark a place on the board.
         *
         * @param player
         */
        public void mark(Player player, Square square) {
            // Take the square.
            square.take(player);
        }

        /**
         * UnMark a place on the board.
         *
         * Confirms the mark first.
         */
        public void unmark(Player player, Square square) {
            square.release(player);
        }

        @Override
        public String toString() {
            return Arrays.stream(board)
                    .map(r -> Arrays.stream(r)
                            .map(s -> String.valueOf(s.getMark()))
                            .collect(Collectors.joining("", "[", "]")))
                    .collect(Collectors.joining(""));
        }

    }

    class Game {

        // The current board state.
        final Board board;

        public Game(Board board) {
            this.board = board;
        }

        public Game(int size) {
            this(new Board(size));
        }

        public Game(int width, int height) {
            this(new Board(width, height));
        }

        private void play(Player firstPlayer) {
            boolean gameFinished = false;
            Player player = firstPlayer;
            do {
                Optional<Board.Square> move = player.getStrategy().think(board, player);
                if (move.isPresent()) {
                    board.mark(player, move.get());
                    System.out.println("Moved: " + move.get() + " -> " + board);
                } else {
                    System.out.println("No move!");
                }
                // Has anyone won?
                Optional<Player> winner = board.getWinner();
                if (winner.isPresent()) {
                    System.out.println("Player " + winner.get() + " won! " + board);
                    gameFinished = true;
                } else {
                    if (board.isDrawn()) {
                        System.out.println("Draw! " + board);
                        gameFinished = true;
                    }
                }
                // Next player.
                player = player.next();
            } while (!gameFinished);

        }

    }

    private void testStupid() {
        // Both start off stupid.
        new Game(3).play(Player.X);
    }

    private void testMinimax() {
        // Test minimax strategy.
        Board testBoard = new Board(3);
        // Set it up like http://neverstopbuilding.com/minimax
        testBoard.mark(Player.O, testBoard.board[0][0]);
        testBoard.mark(Player.X, testBoard.board[0][2]);
        testBoard.mark(Player.X, testBoard.board[1][0]);
        testBoard.mark(Player.X, testBoard.board[2][0]);
        testBoard.mark(Player.O, testBoard.board[2][1]);
        testBoard.mark(Player.O, testBoard.board[2][2]);
        // What does the strategy do?
        Optional<Board.Square> play = minimax.think(testBoard, Player.X);
        System.out.println("minimax(" + testBoard + ") = " + play);
    }

    private void test3X3Game() {
        // Play a game.
        // Change strategies.
        new Game(3).play(Player.X);
    }

    private void test4X4Game() {
        // Play a game.
        new Game(4).play(Player.X);
    }

    public void test() {
        testStupid();
        testMinimax();
        System.out.println("Test minmax vs stupid");
        Player.X.setStrategy(minimax);
        test3X3Game();
        System.out.println("Test minmax vs minmax");
        Player.O.setStrategy(minimax);
        test3X3Game();
        System.out.println("Test 4 X 4");
        test4X4Game();
    }

    public static void main(String args[]) {
        try {
            new TicTacToe().test();
        } catch (Throwable t) {
            t.printStackTrace(System.err);
        }
    }

}

При игре в минимакс против самого себя он перемещается:

Moved: {1,1}(X) -> [   ][ X ][   ]
Moved: {0,0}(O) -> [O  ][ X ][   ]
Moved: {2,2}(X) -> [O  ][ X ][  X]
Moved: {0,2}(O) -> [O  ][ X ][O X]
Moved: {0,1}(X) -> [O  ][XX ][O X]
Moved: {2,1}(O) -> [O  ][XXO][O X]
Moved: {1,0}(X) -> [OX ][XXO][O X]
Moved: {1,2}(O) -> [OX ][XXO][OOX]
Moved: {2,0}(X) -> [OXX][XXO][OOX]
Draw! [OXX][XXO][OOX]

Обратите внимание, что эта ОС не публикует ходы OP, поэтому явно что-то не так с алгоритмом OP.

person OldCurmudgeon    schedule 21.08.2015
comment
Незадолго до того, как вы написали, я пришел к тому же шокирующему осознанию того, что мой алгоритм альфа-бета неверен. Пытаюсь исправить прямо сейчас. Однако мои предыдущие минимаксные алгоритмы, которые не реализуют альфа-бета, также имеют то, что результаты 4x4 являются правильными, но систематическими. - person Omar Sharaki; 21.08.2015
comment
Это был мой самый первый отзыв, когда я начал работать над этим проектом. Мой алгоритм работает почти так же, как его (то есть алгоритм, который не реализует альфа-бета) - person Omar Sharaki; 21.08.2015
comment
@OldCurmudgeon хороший ответ, пока я не прочитал последнее предложение ... Что неправильно. Несмотря на то, что оказалось, что на самом деле в коде БЫЛА ошибка, ваше предположение неверно: оптимальные 4x4 НЕ обязательно должны быть одинаковыми, так как большинство случаев закончится вничью, а поэтому оптимальных стратегий много. , игра теоретически разговорная. Поскольку MinMax знает, что они равны, он просто выберет одну из этих равных стратегий отрисовки. Так что тот факт, что вы получили другой результат, вовсе не является доказательством того, что какое-либо из ваших решений неверно. - person xXliolauXx; 21.08.2015
comment
@xXliolauXx Можете ли вы заметить что-то не так с моим минимаксным методом? Похоже, он не работает так, как должен. Я только что опубликовал новый вопрос, объясняющий проблему более подробно. - person Omar Sharaki; 22.08.2015