Минимаксный алгоритм в теории игр | Набор 3 (Крестики-нолики AI - Поиск оптимального хода)

Опубликовано: 20 Января, 2022

Предпосылки: минимаксный алгоритм в теории игр, оценочная функция в теории игр.
Давайте объединять то , что мы узнали до сих пор о минимаксе и функции оценки , чтобы написать правильный Tic-Tac-Toe AI (A rtificial I ntelligence) , который играет идеальную игру. Этот ИИ рассмотрит все возможные сценарии и сделает наиболее оптимальный ход.

В поисках лучшего хода:

Мы представим новую функцию под названием findBestMove () . Эта функция оценивает все доступные ходы с помощью minimax (), а затем возвращает лучший ход, который может сделать максимайзер. Псевдокод выглядит следующим образом:

 функция findBestMove (доска):
    bestMove = NULL
    за каждый ход на доске:
        если текущий ход лучше, чем bestMove
            bestMove = текущий ход
    вернуть bestMove

Минимакс:

Чтобы проверить, лучше ли текущий ход лучшего, чем лучший, мы воспользуемся функцией minimax (), которая рассмотрит все возможные варианты развития игры и вернет лучшее значение для этого хода, предполагая, что противник также играет оптимально.
Код для максимизатора и минимизатора в функции minimax () аналогичен findBestMove () , с той лишь разницей, что вместо возврата хода он возвращает значение. Вот псевдокод:

 функция minimax (доска, глубина, isMaximizingPlayer):

    если текущее состояние платы является конечным:
        возвращаемое значение платы
    
    если isMaximizingPlayer:
        bestVal = -INFINITY 
        за каждый ход на доске:
            значение = минимакс (доска, глубина + 1, ложь)
            bestVal = max (bestVal, значение) 
        вернуть bestVal

    еще :
        bestVal = + БЕСКОНЕЧНОСТЬ 
        за каждый ход на доске:
            значение = минимакс (доска, глубина + 1, истина)
            bestVal = min (bestVal, значение) 
        вернуть bestVal

Проверка состояния GameOver:

Чтобы проверить, окончена ли игра и не осталось ходов, мы используем функцию isMovesLeft () . Это простая прямолинейная функция, которая проверяет, доступен ли ход или нет, и возвращает истину или ложь соответственно. Псевдокод выглядит следующим образом:

 функция isMovesLeft (доска):
    для каждой ячейки на доске:
        если текущая ячейка пуста:
            вернуть истину
    вернуть ложь

Делаем наш ИИ умнее:

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

  • Ход A : X может выиграть за 2 хода
  • Ход B : X может выиграть за 4 хода

Наша оценочная функция вернет значение +10 для обоих ходов A и B. Несмотря на то, что ход A лучше, потому что он обеспечивает более быструю победу, наш ИИ может иногда выбирать B. Чтобы решить эту проблему, мы вычитаем значение глубины из оцененной оценки. Это означает, что в случае победы он выберет победу, которая требует наименьшего количества ходов, а в случае проигрыша будет пытаться продлить игру и сыграть как можно больше ходов. Таким образом, новое оцененное значение будет

  • Движение A будет иметь значение +10-2 = 8.
  • Движение B будет иметь значение +10-4 = 6.

Теперь, поскольку ход A имеет более высокий балл по сравнению с ходом B, наш ИИ выберет ход A, а не ход B. То же самое нужно применить и к минимайзеру. Вместо вычитания глубины мы добавляем значение глубины, которое минимизатор всегда пытается получить, как можно более отрицательное значение. Мы можем вычесть глубину либо внутри функции оценки, либо вне ее. Везде нормально. Я решил сделать это вне функции. Реализация псевдокода выглядит следующим образом.

 если максимайзер выиграл:
    return WIN_SCORE - глубина

иначе, если минимизатор выиграл:
    вернуть LOOSE_SCORE + depth

Рекомендуется: сначала решите эту проблему на «ПРАКТИКЕ», прежде чем переходить к решению.

Реализация :

Below is implementation of above idea. 
 

C++

// C++ program to find the next optimal move for
// a player
#include<bits/stdc++.h>
using namespace std;
 
struct Move
{
    int row, col;
};
 
char player = "x", opponent = "o";
 
// This function returns true if there are moves
// remaining on the board. It returns false if
// there are no moves left to play.
bool isMovesLeft(char board[3][3])
{
    for (int i = 0; i<3; i++)
        for (int j = 0; j<3; j++)
            if (board[i][j]=="_")
                return true;
    return false;
}
 
// This is the evaluation function as discussed
// in the previous article ( http://goo.gl/sJgv68 )
int evaluate(char b[3][3])
{
    // Checking for Rows for X or O victory.
    for (int row = 0; row<3; row++)
    {
        if (b[row][0]==b[row][1] &&
            b[row][1]==b[row][2])
        {
            if (b[row][0]==player)
                return +10;
            else if (b[row][0]==opponent)
                return -10;
        }
    }
 
    // Checking for Columns for X or O victory.
    for (int col = 0; col<3; col++)
    {
        if (b[0][col]==b[1][col] &&
            b[1][col]==b[2][col])
        {
            if (b[0][col]==player)
                return +10;
 
            else if (b[0][col]==opponent)
                return -10;
        }
    }
 
    // Checking for Diagonals for X or O victory.
    if (b[0][0]==b[1][1] && b[1][1]==b[2][2])
    {
        if (b[0][0]==player)
            return +10;
        else if (b[0][0]==opponent)
            return -10;
    }
 
    if (b[0][2]==b[1][1] && b[1][1]==b[2][0])
    {
        if (b[0][2]==player)
            return +10;
        else if (b[0][2]==opponent)
            return -10;
    }
 
    // Else if none of them have won then return 0
    return 0;
}
 
// This is the minimax function. It considers all
// the possible ways the game can go and returns
// the value of the board
int minimax(char board[3][3], int depth, bool isMax)
{
    int score = evaluate(board);
 
    // If Maximizer has won the game return his/her
    // evaluated score
    if (score == 10)
        return score;
 
    // If Minimizer has won the game return his/her
    // evaluated score
    if (score == -10)
        return score;
 
    // If there are no more moves and no winner then
    // it is a tie
    if (isMovesLeft(board)==false)
        return 0;
 
    // If this maximizer"s move
    if (isMax)
    {
        int best = -1000;
 
        // Traverse all cells
        for (int i = 0; i<3; i++)
        {
            for (int j = 0; j<3; j++)
            {
                // Check if cell is empty
                if (board[i][j]=="_")
                {
                    // Make the move
                    board[i][j] = player;
 
                    // Call minimax recursively and choose
                    // the maximum value
                    best = max( best,
                        minimax(board, depth+1, !isMax) );
 
                    // Undo the move
                    board[i][j] = "_";
                }
            }
        }
        return best;
    }
 
    // If this minimizer"s move
    else
    {
        int best = 1000;
 
        // Traverse all cells
        for (int i = 0; i<3; i++)
        {
            for (int j = 0; j<3; j++)
            {
                // Check if cell is empty
                if (board[i][j]=="_")
                {
                    // Make the move
                    board[i][j] = opponent;
 
                    // Call minimax recursively and choose
                    // the minimum value
                    best = min(best,
                           minimax(board, depth+1, !isMax));
 
                    // Undo the move
                    board[i][j] = "_";
                }
            }
        }
        return best;
    }
}
 
// This will return the best possible move for the player
Move findBestMove(char board[3][3])
{
    int bestVal = -1000;
    Move bestMove;
    bestMove.row = -1;
    bestMove.col = -1;
 
    // Traverse all cells, evaluate minimax function for
    // all empty cells. And return the cell with optimal
    // value.
    for (int i = 0; i<3; i++)
    {
        for (int j = 0; j<3; j++)
        {
            // Check if cell is empty
            if (board[i][j]=="_")
            {
                // Make the move
                board[i][j] = player;
 
                // compute evaluation function for this
                // move.
                int moveVal = minimax(board, 0, false);
 
                // Undo the move
                board[i][j] = "_";
 
                // If the value of the current move is
                // more than the best value, then update
                // best/
                if (moveVal > bestVal)
                {
                    bestMove.row = i;
                    bestMove.col = j;
                    bestVal = moveVal;
                }
            }
        }
    }
 
    printf("The value of the best Move is : %d ",
            bestVal);
 
    return bestMove;
}
 
// Driver code
int main()
{
    char board[3][3] =
    {
        { "x", "o", "x" },
        { "o", "o", "x" },
        { "_", "_", "_" }
    };
 
    Move bestMove = findBestMove(board);
 
    printf("The Optimal Move is : ");
    printf("ROW: %d COL: %d ", bestMove.row,
                                  bestMove.col );
    return 0;
}

Java

// Java program to find the
// next optimal move for a player
class GFG
{
static class Move
{
    int row, col;
};
 
static char player = "x", opponent = "o";
 
// This function returns true if there are moves
// remaining on the board. It returns false if
// there are no moves left to play.
static Boolean isMovesLeft(char board[][])
{
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
            if (board[i][j] == "_")
                return true;
    return false;
}
 
// This is the evaluation function as discussed
// in the previous article ( http://goo.gl/sJgv68 )
static int evaluate(char b[][])
{
    // Checking for Rows for X or O victory.
    for (int row = 0; row < 3; row++)
    {
        if (b[row][0] == b[row][1] &&
            b[row][1] == b[row][2])
        {
            if (b[row][0] == player)
                return +10;
            else if (b[row][0] == opponent)
                return -10;
        }
    }
 
    // Checking for Columns for X or O victory.
    for (int col = 0; col < 3; col++)
    {
        if (b[0][col] == b[1][col] &&
            b[1][col] == b[2][col])
        {
            if (b[0][col] == player)
                return +10;
 
            else if (b[0][col] == opponent)
                return -10;
        }
    }
 
    // Checking for Diagonals for X or O victory.
    if (b[0][0] == b[1][1] && b[1][1] == b[2][2])
    {
        if (b[0][0] == player)
            return +10;
        else if (b[0][0] == opponent)
            return -10;
    }
 
    if (b[0][2] == b[1][1] && b[1][1] == b[2][0])
    {
        if (b[0][2] == player)
            return +10;
        else if (b[0][2] == opponent)
            return -10;
    }
 
    // Else if none of them have won then return 0
    return 0;
}
 
// This is the minimax function. It considers all
// the possible ways the game can go and returns
// the value of the board
static int minimax(char board[][],
                    int depth, Boolean isMax)
{
    int score = evaluate(board);
 
    // If Maximizer has won the game
    // return his/her evaluated score
    if (score == 10)
        return score;
 
    // If Minimizer has won the game
    // return his/her evaluated score
    if (score == -10)
        return score;
 
    // If there are no more moves and