Ошибка минимаксного алгоритма

Я пытался изучить алгоритм минимакса и наткнулся на ошибку, которую не могу понять, как решить. Код:

    private List<Integer> generatemoves(int[] evalFields) {
    List<Integer> nextMoves = new ArrayList<Integer>();
    for (int i = 0; i < evalFields.length; i++) {
        if (evalFields[i] == 0) {
            nextMoves.add(i);
        }
    }
    return nextMoves;
}

private int evaluateLine(int p1, int p2, int p3, int[] evalFields) {
    int score = 0;
    if (evalFields[p1] == 1) {
        score = 1;
    } else if (evalFields[p1] == 10) {
        score = -1;
    }

    if (evalFields[p2] == 1) {
        if (score == 1) {
            score = 10;
        } else if (score == -1) {
            return 0;
        } else {
            score = 1;
        }
    } else if (evalFields[p2] == 10) {
        if (score == -1) {
            score = -10;
        } else if (score == 1) {
            return 0;
        } else {
            score = -1;
        }
    }

    if (evalFields[p3] == 1) {
        if (score > 0) {
            score *= 10;
        } else if (score < 0) {
            return 0;
        } else {
            score = 1;
        }
    } else if (evalFields[p3] == 10) {
        if (score < 0) {
            score *= 10;
        } else if (score > 1) {
            return 0;
        } else {
            score = -1;
        }
    }
    return score;
}

private int evaluateBoard(int [] evalFields) {
    int score = 0;
    score += evaluateLine(0, 1, 2, evalFields);
    score += evaluateLine(3, 4, 5, evalFields);
    score += evaluateLine(6, 7, 8, evalFields);
    score += evaluateLine(0, 3, 6, evalFields);
    score += evaluateLine(1, 4, 7, evalFields);
    score += evaluateLine(2, 5, 8, evalFields);
    score += evaluateLine(0, 4, 8, evalFields);
    score += evaluateLine(2, 4, 6, evalFields);

    return score;
}

private int bestMove(int currentTurn, int[] board) {
    int move;
    int bestScore;
    if (currentTurn == 1) {
        bestScore = Integer.MIN_VALUE;
    } else {
        bestScore = Integer.MAX_VALUE;
    }
    List<Integer> nextMoves = generatemoves(board);
    List<Integer> bestScores = new ArrayList<Integer>();
    for (int i = 0; i < nextMoves.size(); i++) {
        int[] newBoards = new int[9];
        for (int j = 0; j < board.length; j++) {
            newBoards[j] = board[j];
        }
        newBoards[nextMoves.get(i)] = turn;
        bestScores.add(evaluateBoard(newBoards));
    }


    for (int scores : bestScores) {
        if (currentTurn == 1) {
            if (scores > bestScore) bestScore = scores;
        } else {
            if (scores < bestScore) bestScore = scores;
        }
    }
    move = nextMoves.get(bestScores.indexOf(bestScore));

    return move;
}

Это самая важная часть кода. Что он делает, или я думаю, что он делает, так это то, что он генерирует все возможные ходы с доски, которые называются полями. Затем он вычисляет счет для каждого хода. Затем он продолжает делать ход, который приводит к наибольшему или наименьшему количеству очков, x (1) пытается получить наивысшее значение, а O (10) - наименьшее. Возникает баг в том, что когда игрок стартует и выходит на поле посередине, то ИИ ведет себя нормально, но после второго хода игроков ИИ начинает вести себя странно:

[ ][ ][ ]    [O][ ][ ]    [O][ ][O]
[ ][x][ ] => [ ][x][ ] => [x][x][ ]
[ ][ ][ ]    [ ][ ][ ]    [ ][ ][ ]

Если игрок выбирает это:

[O][ ][ ]    [O][ ][ ]
[ ][x][x] => [O][x][x]
[ ][ ][ ]    [ ][ ][ ]

Тогда ИИ действует нормально. Я не знаю, что не так, или даже правильно ли я понял алгоритм минимакса.

****edit**** Добавленный код по-прежнему имеет ту же проблему

    private int[] evaluateMove(int [] board, int currentTurn) {
    int bestScore;
    int currentScore;
    int bestMove = -1;
    if (currentTurn == 1) {
        bestScore = Integer.MIN_VALUE;
    } else {
        bestScore = Integer.MAX_VALUE;
    }

    List<Integer> nextMoves = generatemoves(board);
    if (nextMoves.isEmpty()) {
        bestScore = evaluateTheBoard(board);
    } else {
        for (int move : nextMoves) {
            int[] nextBoard = new int[9];
            for (int i = 0; i < nextBoard.length; i ++) {
                nextBoard[i] = board[i];
            }
            nextBoard[move] = currentTurn;
            currentScore = evaluateMove(nextBoard, nextTurn())[0];
            if (currentTurn == 1) {
                if (currentScore > bestScore) {
                    bestScore = currentScore;
                    bestMove = move;
                }
            } else {
                if (currentScore < bestScore) {
                    bestScore = currentScore;
                    bestMove = move;
                }
            }
        }
    }
    return new int[] {bestScore, bestMove};
}

person reggin    schedule 02.04.2015    source источник
comment
минимакс - это соглашение о подсчете очков, а не алгоритм. При минимаксе позиции, благоприятствующие одному игроку, отрицательны, а позиции, благоприятствующие другому игроку, положительны. То, что вы на самом деле делаете с этой оценкой, является работой алгоритма. Алгоритм здесь представляет собой просто перебор будущих позиций, но примеры именованных алгоритмов включают обрезку альфа-бета, MTD (f), Negascout и т. Д., Ни один из которых не требуется для крестиков-ноликов, потому что это просто совершенная оптимизация по сравнению с классическим перебором. сила. Кроме того, IMO, негамаксная оценка лучше, чем минимаксная, и обычно приводит к более чистому коду.   -  person VoidStar    schedule 02.04.2015
comment
Кроме того, поскольку игра в крестики-нолики обычно приводит к ничьей, особенно когда вы ходите вторым, компьютер часто видит, что у него есть только один вариант — сделать ставку, что может объяснить то, что вы видите. Когда лучшим вариантом является ничья, он просто сыграет что угодно и не даст противнику выиграть. Когда вы говорите, что играете странно, это на самом деле позволяет игроку выиграть? Потому что это указывало бы на ошибку.   -  person VoidStar    schedule 02.04.2015
comment
не прошел всю историю, но возможна ли ничья в два хода? то есть с одинаковым счетом? если да, что бы вы сделали?   -  person shole    schedule 02.04.2015
comment
@shole в примере игрок x, с этими ходами может быть ничья. Проблема в том, что ИИ позволяет игроку выиграть.   -  person reggin    schedule 05.04.2015


Ответы (1)


Я думаю, вы неправильно понимаете, как смотреть вперед в такой игре. Не суммируйте значения, возвращаемые evaluateLine.

Вот псевдокод для минимаксного счета доски для игры в крестики-нолики (что должно возвращать evaluateBoard). Обратите внимание, что evaluateBoard должно иметь представление о currentTurn.

function evaluateBoard(board, currentTurn)

// check if the game has already ended:
if WhiteHasWon then return -10
if BlackHasWon then return +10

// WhiteHasWon returns true if there exists one or more winning 3-in-a-row line for white. 
// (You will have to scan for all 8 possible 3-in-a-row lines of white pieces)
// BlackHasWon returns true if there exists one or more winning 3-in-a-row line for black

if no legal moves, return 0 // draw

// The game isn't over yet, so look ahead:
bestMove = notset
resultScore = notset
for each legal move i for currentTurn,
   nextBoard = board
   Apply move i to nextBoard
   score = evaluateBoard(nextBoard, NOT currentTurn).score
   if score is <better for currentTurn> than resultScore, then   
      resultScore = score
      bestMove = move i
return (resultScore, bestMove)

Одно очень важное различие между этой и вашей версией и моей версией заключается в том, что моя версия является рекурсивной. Твой идет только на один уровень вглубь. Мой вызывает evaluateBoard изнутри evaluateBoard, что было бы бесконечным циклом, если бы мы не были осторожны (как только доска заполняется, она не может идти глубже, поэтому на самом деле она не бесконечна)

Еще одно отличие состоит в том, что ваш суммирует вещи, когда этого не следует делать. В результате игра в крестики-нолики получает -10,0 или 10 только после того, как вы досмотрели игру до конца. Вы должны выбрать наилучший возможный ход, доступный этому игроку в данный момент, и полностью игнорировать все другие возможности, потому что вас интересует только «лучшая» линия игры. Счет игры равен результату оптимальной игры.

Расширение <better for currentTurn> в минимаксе грязно, поэтому негамакс чище. Белый предпочитает низкие баллы, а черный предпочитает высокие баллы, поэтому вам нужны некоторые операторы if, чтобы заставить его выбрать соответствующий предпочтительный балл. У вас уже есть эта часть (в конце вашего лучшего кода перемещения), но ее нужно оценить внутри рекурсии, а не только в конце.

person VoidStar    schedule 02.04.2015
comment
Спасибо за ваш ответ. Но у меня есть несколько вопросов. Верхняя часть вашего псевдокода просто проверяет, выиграли ли черные или белые или это ничья? или мне все еще нужна функция AssessmentLine? Я также не могу понять, как извлечь правильный ход из функции AssessmentBoard, если бы вы могли уточнить, что это было бы здорово. - person reggin; 02.04.2015
comment
Возможно, я неправильно понял это, но я думал, что AssessmentLine ищет 3 строки подряд, которые указывают на победу. Что вам нужно проверить на конец игры, так это найти любые 3 строки подряд (которые выигрывают игру). Если один (или более) существует для белых, то оценка равна -10, и то же самое для черных. - person VoidStar; 02.04.2015
comment
Я обновил ответ, чтобы показать, как вы можете извлечь лучший ход из AssessmentBoard, если вы хотите, чтобы ответ вышел из этой функции. Вы также можете пойти дальше и построить всю ожидаемую последовательность ходов, чтобы вы могли видеть линию игры, которая понравилась компьютеру. - person VoidStar; 02.04.2015
comment
Я немного поиграл с вашим решением. ИИ не стал умнее. Я сохранил большую часть кода, я только добавил вашу функцию. - person reggin; 04.04.2015
comment
Просто чтобы уточнить, что ИИ позволяет игроку выигрывать еще чаще с помощью рекурсивной функции, я почти уверен, что это не то, что должно происходить. - person reggin; 04.04.2015
comment
Ваш текущий код, по-видимому, не определяет условие выигрыша и продолжает углубляться, пока nextMoves.isEmpty() не вернет true. Разве это не означает, что он игнорирует 3 подряд и продолжает поиск, пока вся доска не будет заполнена, вместо того, чтобы останавливаться, когда игра выиграна? И даже тогда это не вознаграждает победу, которая является корнем проблемы. Вы, кажется, забыли, что если существует одна или несколько выигрышных линий 3-в-ряд для белых, верните шаг -10 в псевдокоде. - person VoidStar; 05.04.2015
comment
Я считаю, что предыдущий комментарий является основной проблемой, но также вы должны написать тесты для своего кода и добавить assert, чтобы убедиться, что части работают. Например, я бы assert сказал, что evaluateMove никогда не возвращает ни Integer.MIN_VALUE, ни Integer.MAX_VALUE, потому что это всегда указывает на ошибку. Кроме того, я не доверяю тому, как вы сделали nextTurn()... это должно быть функцией currentTurn, например. nextTurn(currentTurn) возвращает противоположный ход. - person VoidStar; 05.04.2015
comment
На данный момент это работает, я еще не сталкивался со странной игрой от ИИ. Я также читал в Интернете, что мне нужно было применить глубину к подсчету очков, чтобы исправить эти странные игры. Я думаю, что сделал что-то неправильно, что испортило ИИ, когда я тестировал его с функцией hasWon() и лучшей функцией nextTurn(). Он работает так, как я хочу, поэтому мне не нужна глубина. Спасибо за уделенное время. - person reggin; 05.04.2015
comment
Да, код, который я дал, нетерпим к обрезанию глубины. Лучше смотреть вперед до конца, если игра достаточно проста для этого. В сложных играх, таких как шахматы, мы должны генерировать оценку того, кто выигрывает после 10 ходов вперед и т. д. Чтобы сделать игру в крестики-нолики терпимой только к тому, чтобы смотреть, скажем, на 2 хода вперед, нам нужен код, который оценивает, кто выигрывает. как минимаксный счет (или он будет выбран случайным образом). Но нет лучшего способа получить такую ​​оценку в крестиках-ноликах, чем просто смотреть вперед, поэтому лучше смотреть вперед, пока кто-то не выиграет. - person VoidStar; 05.04.2015
comment
Ааа ладно, смысл есть. - person reggin; 07.04.2015