TicTacToe: Minimax-Agent e implementación de python para juegos hombre-máquina

Tabla de contenido

1. Introducción

2. Introducción al algoritmo Minimax

2.1 Árbol de juego

2.2 Función de valoración

2.3 Idea básica del algoritmo

2.4 Ejemplo 1

​​​​​​​​2.5 Ejemplo 2—Juego de ajedrez

2.6 Resumen

3. Implementación del Agente TicTacToe minimax

3.1 Descripción de la función

3.2 Flujo de procesamiento

código 3.3 

 4. Resumen


1. Introducción

        En el artículo anterior, se implementó un sencillo programa de juego hombre-máquina Tic- Tac -Toe. ver:

Programa de juego hombre-máquina Tic-Tac-Toe (implementación de python ) Para el cálculo del número y el número de estado del tablero, consulte: Este artículo primero implementa un programa simple de juego hombre-máquina Tic-Tac-Toe, haciendo una preparación para el siguiente paso para implementar el programa de juego hombre-máquina Tic-Tac-Toe basado en el algoritmo minimax. https://blog.csdn.net/chenxy_bwave/article/details/128555550         Entre ellos, el jugador de ajedrez de la computadora o el jugador de ajedrez de IA solo realiza movimientos aleatorios, excepto que pueden hacerse ganar o evitar que el oponente gane en el siguiente paso. Este documento presenta además el clásico algoritmo minimax del algoritmo de juego de confrontación clásico para apoyar a los jugadores de ajedrez de IA.

2.  Introducción al algoritmo Minimax

2.1 Árbol de juego

        El método general para resolver problemas de juegos es organizar el estado del juego en un árbol. Cada nodo del árbol representa un estado (como el estado del tablero en un juego de mesa), y la relación padre-hijo significa que el nodo padre puede llegar al estado secundario después de un paso. Los bordes se utilizan para representar acciones.

        Minimax no es una excepción Tomando los juegos de ajedrez como ejemplo, las capas impares de la estructura de árbol (el estado inicial, es decir, la capa donde se encuentra el nodo raíz es la primera capa) significa que es el turno del primer jugador . para jugar al ajedrez; la capa de número par es el turno del segundo jugador para jugar el estado de ajedrez. Minimax es un algoritmo para buscar la solución óptima para esta estructura de árbol. La capa con números impares se denomina capa de valor máximo (lado propio) (denominada capa máxima , y ​​los nodos que contiene se denominan nodos máximos ), y la capa con números pares se denomina capa de valor mínimo (oponente). (correspondientemente, capa mínima , nodo mínimo ).

2.2 Función de valoración

        La función de evaluación se usa para evaluar cada situación y se usa para juzgar la situación actual en el árbol del juego. En los sistemas tradicionales de inteligencia de juegos de mesa, la función de evaluación generalmente se designa artificialmente, lo que juega un papel decisivo en el nivel de inteligencia de los juegos de mesa.

        La forma de la función de evaluación no es fija, su entrada es generalmente la información de una situación y su salida es un valor que indica qué tan buena o mala es la situación correspondiente, como la tasa ganadora.

        Por ejemplo, un ejemplo de una función de valoración en tres en raya : el número de filas, columnas y diagonales posibles para el jugador X menos el número de filas, columnas y diagonales posibles para el jugador O. Como se muestra en la siguiente figura, el jugador X todavía tiene 6 filas, columnas y barras posibles , y el jugador O tiene 3 filas, columnas y barras posibles , por lo tanto, la salida de la función de evaluación en la situación de la Figura 3 3 . Por supuesto, también puede ser la razón de dos números, o cualquier otra relación funcional. . .

​​​​​​​​El ejemplo  anterior        La función de evaluación también puede provenir de la simulación Monte Carlo, es decir, a través de una gran cantidad de simulaciones aleatorias, se cuentan los resultados de todas las partidas de ajedrez posibles a partir de una determinada situación para determinar la puntuación (estimación del valor) de la situación.

        La estimación del valor del juego final o juego de ajedrez es relativamente simple y, por lo general, se puede definir directamente de manera heurística.

2.3 Ideas de algoritmos básicos

        El algoritmo Minimax (la traducción al chino se llama algoritmo de maximización de minimización ) se usa a menudo en algoritmos de búsqueda inteligente para juegos como el ajedrez, donde compiten dos partes. Este algoritmo es un algoritmo de suma cero ( zero-sum ), es decir, desde la perspectiva de un lado del juego, debe elegir la opción que maximiza su propia ventaja entre las opciones disponibles; mientras que el oponente tiene que elegir el opción que minimiza su propia ventaja.método. En los juegos de mesa para dos jugadores, como Backgammon, Ajedrez, Ajedrez, tres en raya y Go, dos jugadores se mueven alternativamente, un paso a la vez. El desarrollo de programas inteligentes para estos juegos puede utilizar el algoritmo Minimax .

        Por ejemplo, dos personas juegan al ajedrez. El jugador actual que está jugando al ajedrez (se llama a sí mismo o nuestro lado, y el jugador opuesto se llama el oponente) tiene N opciones para el siguiente movimiento: a1,a2,\cdots,a_N, que conducen a sus propios porcentajes de victoria respectivamente p1,p2,\cdots,p_N. Entonces la propia elección debería ser:

                \arg\max \limits_{i} p_i 

        Por el contrario, cuando es el turno del oponente para jugar al ajedrez, es natural elegir una posición o movimiento que pueda minimizar la tasa de ganancias de uno (porque es un juego de suma cero, minimizar la tasa de ganancias de uno naturalmente corresponde a maximizar la tasa de ganancias del oponente). ) .

        Minimax es un algoritmo pesimista que asume que el oponente tiene una capacidad de toma de decisiones perfecta, y cada paso del oponente nos llevará de la situación actual al estado del tablero con la tasa de victorias más baja en teoría. La estrategia propia debe ser elegir lo mejor en el peor de los casos que el oponente pueda lograr, es decir, dejar que el oponente cause la menor pérdida a uno mismo bajo una toma de decisiones perfecta. Para ser más específicos, supongamos que en el estado actual del tablero, hay N opciones para el lado propio: a1,a2,\cdots,a_N, que conducen al estado del tablero respectivamente s_1,s_2,\cdots,s_N. En cada siestado, el oponente adopta la estrategia óptima (para cada juego sique hace que la tasa de ganancias del jugador sea la más pequeña, la pérdida es la mayor ) y la tasa de ganancias del lado del jugador es respectivamente , entonces el movimiento que el lado del jugador debe elegir es determinado por .p1,p2,\cdots,p_N\arg\max \limits_{i} p_i

2.4 Ejemplo 1

        Este ejemplo está tomado del blog de referencia [2].

        Ahora considere tal juego: hay tres platos A , B y C , cada uno con tres billetes. A pone 1 , 20 , 50 ; B pone 5 , 10 , 100 ; C pone 1 , 5 , 10 . Los participantes del juego son A y B, y la información de las tres placas es completamente visible para ambos. El juego se divide en tres pasos:

  1. A elige uno de los tres platos.
  2. B saca dos billetes de la placa seleccionada por A y se los entrega a A.
  3. A elige uno de los dos billetes dados por B.

        En el juego, el objetivo de A es obtener la mayor cantidad posible de billetes, y el objetivo de B es hacer que las denominaciones de los billetes que A finalmente obtiene sean lo más pequeñas posible (tenga en cuenta que, aquí, el objetivo de B no es maximizar las denominaciones que finalmente obtiene). obtiene! Si se cambia el objetivo del juego para maximizar o minimizar el valor nominal obtenido por B, o para minimizar el valor nominal obtenido por A, que se convierte en otros tres juegos, y las soluciones obtenidas son diferentes).

        Según el algoritmo minimax, los pasos de procesamiento para buscar el árbol de nodos de estado del juego enraizados en el nodo actual para determinar el siguiente paso son los siguientes:

( 1 ) Construya un árbol de nodos de estado de acuerdo con las reglas del juego (acciones alternas A y B);

( 2 ) Determine el valor de cada hoja inferior

( 3 ) Comenzando desde la hoja inferior, actualice el valor del nodo de abajo hacia arriba de acuerdo con el principio minimax

(4) Basado en la decisión de actualización del valor del nodo del nodo raíz (es decir, seleccionando el valor máximo entre los nodos secundarios) para determinar el siguiente paso en el estado actual

​​​​​​​​Este         árbol de búsqueda basado en el principio minimax puede denominarse árbol minimax (búsqueda, decisión del juego).

        En este problema, la definición del valor de cada nodo es obvia, y se puede definir como el valor nominal que puede obtener A en el juego a partir del estado que representa el nodo en condiciones óptimas. En la capa máxima, el valor del nodo debe maximizarse, mientras que en la capa mínima , el valor del nodo debe minimizarse.

        La siguiente figura es el árbol de estado para el problema de ejemplo anterior:

        Considéralo desde el punto de vista de A. Entre ellos, el nodo cuadrado indica que es nuestro turno (A) de operar, y el triángulo indica que es el turno del oponente (B) de operar. Después de tres rondas de acciones (nuestro lado - oponente - nuestro lado), se alcanza el estado final (el nodo que representa el estado final es un nodo hoja, por lo que no es necesario hacer una distinción MIN/MAX ). Las hojas amarillas representan todos los finales posibles. Desde la perspectiva de la Parte A, dado que los ingresos finales pueden evaluarse por el valor nominal de los billetes, naturalmente podemos usar el valor nominal de los billetes obtenidos por la Parte A al final del juego para representar el valor del estado final. .

        A continuación, considere la penúltima capa de nodos. Esta capa es la llamada capa de valor máximo, es decir, es el turno de la Parte A de elegir la operación, y el resultado de la selección debe ser maximizar el valor de el nodo Es decir, el valor de cada nodo es el valor máximo de sus nodos hijos, por lo que el valor de esta capa se puede obtener de la siguiente manera:

        La penúltima capa es la capa de valor mínimo, y es el turno de la Parte B para elegir. Como se mencionó anteriormente, el propósito de la selección de la parte B es minimizar el valor de los nodos, por lo que el valor de estos nodos depende del valor mínimo de los nodos secundarios.

        Finalmente, el nodo raíz es el nodo máximo, por lo que el valor depende del valor máximo de los nodos hoja. El árbol minimax final totalmente asignado es el siguiente:

        Se puede ver que el resultado del juego es que A puede obtener un billete con un valor nominal de 20. La premisa es que ambos jugadores en el juego tienen una capacidad de toma de decisiones perfecta. En juegos reales como el ajedrez, los participantes del juego pueden no tener una capacidad de toma de decisiones perfecta y existe una cierta probabilidad de que tomen decisiones no óptimas. En este caso, habrá más y más cambios en el juego.

        En este ejemplo, el número de estados posibles es muy pequeño, por lo que se puede proporcionar un árbol de estado completo mediante enumeración de fuerza bruta. En este caso, la solución óptima global se puede obtener en base al algoritmo Minimax. En los problemas del mundo real, el árbol de estado suele ser muy grande, y es difícil o imposible incluso para una computadora dar un árbol completo, en este caso, a menudo es necesario limitar la profundidad de búsqueda, y la solución obtenida es una solución óptima local, que puede ser diferente de La solución óptima global está sesgada.

​​​​​​​​2.5  Ejemplo 2—Juego de ajedrez

        Este ejemplo está tomado del blog de referencia [3].

        A continuación se utiliza un juego de ajedrez jugado por dos jugadores como ejemplo de ilustración.

        Como se mencionó anteriormente, el primer punto clave del problema de búsqueda basada en árboles es la definición del valor del nodo (función).

        En el ejemplo anterior, el valor nominal de los billetes que puede obtener A se utiliza directamente como el valor de cada nodo. Además, dado que solo hay tres rondas (tenga en cuenta que una ronda en este documento corresponde a un movimiento, no a cada movimiento), es razonable suponer que ambos jugadores en el juego pueden calcular tres movimientos hacia adelante hasta el resultado del juego (mirar hacia adelante para el final del juego). Esto significa que ambos lados del juego conocen el árbol minimax completo desde el principio.

        En los juegos de ajedrez, el resultado final del juego de ajedrez es solo (desde el punto de vista de un lado) ganar y perder, por lo que, naturalmente, el nodo de estado final se puede considerar como {1,0, -1} para representar {win, flat , negativo} se utiliza como la función de valor del nodo. Sin embargo, la estimación del valor del estado intermedio del juego de ajedrez es más complicada En términos generales, la función de valor puede definirse por la tasa de ganancia (combinada con el método de Monte Carlo cuando es difícil o imposible confirmar el resultado de cada nodo ), u otro El método heurístico de - Esta es una historia, y la discutiremos más adelante.

        Además de los resultados de ganar-perder, se pueden considerar otras características para la definición del valor del nodo. A continuación se supone que se ha definido la función de estimación del valor del nodo.

        Además, incluso para un juego tan simple como Tic-Tac-Toe (3x3 tic-tac-toe), puede tomar hasta 9 rondas. De esta forma, la profundidad del árbol de búsqueda es como máximo 10, y considerando el factor de ramificación, un árbol de este tamaño (obtuvimos un total de 26830 resultados de ajedrez Tic-Tac-Toe en el artículo anterior) ha superado a la gran mayoría. El cerebro humano tiene poder de cómputo (por supuesto, esto todavía es fácil para las computadoras). Por lo tanto, para jugadores imperfectos (como los ajedrecistas humanos normales), es una suposición razonable suponer que la profundidad de cálculo (los pasos de anticipación) es limitada (menos que el número máximo de movimientos de ajedrez).

        Con base en las suposiciones anteriores, el proceso del juego de ajedrez se puede describir aproximadamente de la siguiente manera.

        Primero, considere el caso más simple, suponiendo que el jugador solo puede mirar hacia adelante un paso (la profundidad de cálculo es 1, mirando hacia adelante 1 paso). En este caso: cuando es el turno de la Parte A (el primer jugador, es decir, la capa máxima actual) para jugar al ajedrez, el jugador determina el próximo movimiento que puede realizar en función de la situación actual y predice todas las situaciones posibles que él puede completar este paso.Luego evalúa el valor de cada situación (aquí se supone que la evaluación del valor es posible, si es razonable u óptimo es otra cuestión), y luego elige el movimiento correspondiente al que tiene el mayor valor ( en otras palabras, la tasa de ganancia más baja del primer jugador). Cuando es el turno de la Parte B (oponente, revés) de jugar al ajedrez, la Parte B también predice todas las situaciones posibles en las que ha completado este paso, y luego elige la situación que se ve mejor entre todos los movimientos (el que tiene la función de menor valor). , En otras palabras, el lado de primera mano tiene la tasa de ganancias más baja). Este ciclo se repite hasta el final del juego. Tenga en cuenta que dado que se supone que la profundidad de cálculo de los dos lados es solo un paso, cuando cada uno decide a dónde ir a continuación, solo considera la situación causada por su propia ubicación opcional y no considera la respuesta del oponente a su propia ubicación. .

        Teniendo en cuenta que la profundidad de cálculo de ambos jugadores es 2, ¿qué sucederá?

Cuando es el turno de la Parte A (el primer jugador, es decir, la capa máxima actual) de jugar al ajedrez, el primer jugador predice todas las situaciones posibles { } en las que ha completado este paso, y además predice todas las posibles contramedidas del oponente . para cada situación obtener todo lo posible La posición es { { }, { }, ..., { }}. Teniendo en cuenta que el primer jugador quiere maximizar el valor del estado del tablero y el segundo jugador quiere minimizar el valor del estado del tablero, el proceso de toma de decisiones del primer jugador se muestra en la siguiente figura y el k_opt óptimo se obtiene como se muestra en la figura. s1,s2,\cdots,s_K s_{11},s_{12},\cdots,s_{1,M_1} s_{21},s_{22},\cdots,s_{2,M_2} s_{k,1},s_{k,2},\cdots,s_{k,M_k}

        Lo mismo es cierto cuando es el turno del segundo jugador, pero se invierte.La ecuación de toma de decisiones del segundo jugador es:

        En términos sencillos, es: supuse cómo irías si caminaba por este camino, así que elegí ir por este camino.

  Si la profundidad de cálculo de los dos lados es 3, la situación es aún más complicada: la primera mano predice todas las situaciones posibles cuando completa este paso, y al mismo tiempo predice todas las contramedidas del oponente, y al mismo tiempo piensa de cada situación posible cuando te enfrentas al oponente. Todos los movimientos posibles en un plan de respuesta, y luego elige uno óptimo. Lo mismo ocurre con el revés. Es decir, supuse que si voy por este camino, tú irás por ese camino y luego yo iré por ese otro, así que elijo ir por este camino. . .

        Usamos cuadrados para representar los nodos en la capa máxima (es decir, es el turno del primer jugador para jugar al ajedrez, y el objetivo de jugar al ajedrez es maximizar el valor del nodo actual), y círculos para representar los nodos en la capa mínima. (es decir, es el turno del segundo jugador de jugar al ajedrez, y el objetivo de jugar al ajedrez es maximizar el valor del nodo actual). minimizar el valor del nodo actual). Los nodos de hoja representan el estado final y tienen valores preestablecidos correspondientes (por ejemplo, use 1 para representar una victoria, -1 o 0 para representar una pérdida).

        Suponiendo que la profundidad de cálculo de los jugadores de ajedrez es 4 , es decir, pueden mirar hacia adelante 4 pasos, entonces un ejemplo de un árbol de juego parcial ("parcial" porque no ha llegado al final) visto por el primer jugador es el siguiente Entonces asumimos que el árbol de juego del juego es el siguiente ( Retrocede 4 pasos ) . De acuerdo con este árbol de juego (suponemos que se conoce el valor estimado del nodo "hoja" de este árbol de juego), cómo debería elegir el primer jugador en el estado actual:

        En primer lugar, el primer jugador debe calcular las estimaciones de valor de las diversas posiciones obtenidas por el segundo jugador en el cuarto paso (métodos heurísticos u otros para determinar. Debido a que este es un nodo hoja de una parte del árbol de búsqueda, no es necesario). basado en el valor de los nodos secundarios (estimación mínima/máxima), como se muestra a continuación (la capa inferior):

https://img-blog.csdnimg.cn/2019112619015222.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjE2NTk4MQ==,size_ 16,color_FFFFFF,t_70#pic_center

        Luego, calcule cómo debe elegir en el tercer paso (es decir, elija el más grande de todos los nodos secundarios), como se muestra en la siguiente figura (fuente roja):

         Luego calcule cómo se debe seleccionar la manecilla de segundos en el segundo paso (es decir, elija la más pequeña de todos los nodos secundarios), como se muestra en la siguiente figura (fuente roja):

         Al final, puede ir primero en la situación actual y cuál debería ser el siguiente paso (es decir, seleccionar el más grande de todos los nodos secundarios), como se muestra en la siguiente figura (nodo raíz):

        Por lo tanto, si la decisión óptima se toma con ambas manos primero y último, la dirección del juego de ajedrez será como se muestra en la figura a continuación (línea roja):

Por supuesto, si el segundo jugador no toma la decisión óptima, la dirección del juego de ajedrez puede no ser necesariamente así, y el primer jugador también puede intentar tomar una situación arriesgada pero potencialmente de mayor rendimiento (similar a la "trampa". truco". El "trampa" en el truco significa suponer que el oponente no puede tomar la decisión óptima, para que pueda obtener una mayor ventaja que si ambas partes hacen el movimiento óptimo. Pero una vez que el oponente ve a través del engaño y hace el movimiento óptimo, entonces la parte infiel incurrirá en mayores pérdidas), lo cual está más allá del alcance de nuestra discusión. Como se puede ver en la discusión anterior, si la decisión se toma de acuerdo con el algoritmo MiniMax , la cantidad de cálculo requerida aumenta exponencialmente con el aumento de la profundidad de cálculo (la cantidad de pasos a seguir). Cuanto mayor sea la profundidad de cálculo, mayor será el poder natural del ajedrez y mayor será la tasa de ganancias. Sin embargo, estos estados en realidad contienen muchos estados innecesarios, por lo que podemos optimizarlos mediante operaciones de poda.

2.6  Resumen

Resuma los puntos principales del algoritmo Minimax :

  1. Determine la profundidad máxima de búsqueda (cálculo) D , y después de construir una búsqueda minimax con una profundidad de D, puede llegar al final o solo puede alcanzar un estado intermedio . Para problemas simples, se puede construir un árbol de búsqueda minimax completo directamente desde el estado inicial del juego, pero no se puede construir un árbol de estado completo para problemas reales, por lo que se debe determinar una profundidad máxima D y las capas D se pueden calcular hacia abajo. desde el estado actual como máximo cada vez .
  2. Basado en el estado actual (con el nodo de estado actual como nodo raíz), se construye un árbol de búsqueda parcial (también llamado árbol de decisión del juego) con una profundidad de D+1
  3. Estime el valor de los nodos hoja de esta parte del árbol de búsqueda (utilice una estimación de valor predefinida o... Por ejemplo, en el proceso real del juego, los ajedrecistas humanos evalúan la tasa de victorias de la situación en función de la experiencia y el sentido del ajedrez. )
  4. Asigne valores a los nodos que no son hojas de abajo hacia arriba. Entre ellos , el nodo max toma el valor máximo del nodo hijo y el nodo min toma el valor mínimo del nodo hijo. Después de asignar el nodo raíz, se completa la construcción del árbol de búsqueda minimax

        La decisión de asignar un valor al nodo raíz determina naturalmente el próximo movimiento en el estado actual, y la rama con el valor más alto entre los nodos secundarios se selecciona del nodo raíz como una estrategia de acción. La siguiente figura muestra cómo se vería un posible árbol de búsqueda minimax en el juego Tic-Tac-Toe [3] :

Un ejemplo de árbol de búsqueda minimax para el juego Tic-Tac-Toe

       Si se puede construir un árbol de búsqueda minimax completo a partir del estado inicial del juego, el algoritmo minimax puede dar la solución óptima global. Esta situación se puede llamar complete-minimax .

        Cuando la profundidad de búsqueda es limitada (menor que el número de pasos necesarios para un juego completo), solo se puede construir un árbol de búsqueda minimax parcial (esto se denomina minimax parcial), y la solución obtenida puede considerarse como una solución óptima local . . Cuanto mayor sea la profundidad de búsqueda, más probable será encontrar una mejor solución, pero el tiempo de cálculo se expandirá exponencialmente. Bajo la condición de profundidad de búsqueda limitada, la aplicación del algoritmo Minimax es generalmente para calcular el árbol de búsqueda minimax local mientras se juega , similar al método de trabajo de la ventana deslizante .

3. Implementación del Agente TicTacToe minimax

El siguiente código se ha mejorado         en función del código del artículo anterior ( Programa de juego humano-ordenador Tic-Tac-Toe (implementación de Python) ). Lo principal es agregar el agente minimax AI implementado por la función nextMove_complete_minimax(). Y algunas otras modificaciones relacionadas, y algunas optimizaciones de código, consulte la descripción en el siguiente código para obtener más detalles.

3.1 Descripción de la función

        Prototipo de función: nextMove_complete_minimax (tablero, isMax, jugador)

        【Aporte】

        tablero: estado actual del tablero.

        isMax: si la capa actual es la capa MAX o la capa MIN. Tenga en cuenta que es desde la perspectiva del jugador actual. Tenga cuidado con el cambio al llamar recursivamente.

        jugador: El jugador actual al que le toca moverse.

        【Retirado】

        bestMove: el mejor movimiento para el jugador actual

        bestScore: la puntuación de la situación después de bestMove (estimación de valor)

3.2 Flujo de procesamiento

código 3.3 

# Tic Tac Toe
# Created by chenxy in 2017-06-23, with only drawBoard() copied from <<inventwithpython>>
# 2023-01-04 refined,rev0
# 2023-01-07 refined,rev1
#  (1) Add minimax AI agent, nextMove_complete_minimax()
#  (2) askGameStart() updated to support AI agent selection
#  (3) gameRole --> GAME_ROLE, used as global constant
#  (4) askNextMove() renamed to naiveAiNextMove(), in contrast with minimax-AI
#  (5) Main program updated in accordance with the added minimax AI
#  (6) Other miscellaneous editorial refinement
# 2023-01-07 rev2
#  (1) Correct a bug in gameJudge()
#  (2) Add layer parameter for the convenience of debug
#  (3) Refine the debug message print, with DEBUG for switch on/off debug msg print
import random
import sys

GAME_ROLE = ['A','H']; # 'A': AI;  'H': Human;
DEBUG     = 0

def drawBoard(board, initFlag = 0):
    # This function prints out the board that it was passed.

    brd_copy = board.copy()
    if initFlag:
        brd_copy = ['0','1','2','3','4','5','6','7','8','9']

    # "board" is a list of 10 strings representing the board (ignore index 0)
    print('=============')
    # print('   |   |')
    print(' ' + brd_copy[7] + ' | ' + brd_copy[8] + ' | ' + brd_copy[9])
    # print('   |   |')
    print('-----------')
    # print('   |   |')
    print(' ' + brd_copy[4] + ' | ' + brd_copy[5] + ' | ' + brd_copy[6])
    # print('   |   |')
    print('-----------')
    # print('   |   |')
    print(' ' + brd_copy[1] + ' | ' + brd_copy[2] + ' | ' + brd_copy[3])
    # print('   |   |')
    print('=============')
    print()

def askGameStart():
    # Ask human start a game or not;
    # print('Do you want to start a game? Y or y to start; Others to exit');
    # inputWord = input().lower();
    # if inputWord.startswith('y'):
    #     startNewGame = True;
    # else:
    #     startNewGame = False;

    print('Start a new game? Press 1 to start; Others to exit');
    cmd = input()
    if cmd.isdigit():
        inputWord = int(cmd);
        if inputWord == 1:
            startNewGame = True;
        else:
            startNewGame = False;
    else:
        startNewGame = False;
        
    aiAlgo = 0
    if startNewGame:    
        print('Please select the AI agent to fight with: [1] Unbeatable minimax AI; [0] naive AI(default);');
        cmd = input()
        if not cmd.isdigit():
            aiAlgo = 0
        else:        
            if int(cmd)==1:
                aiAlgo = 1

    return startNewGame, aiAlgo

# Decide whether the number human input for the next move has been already used or not.
# It can be decided by whether the corrsponding element is empty or not.
def isValidInput(board, humanNextMove):
    isValid = 1;
    if humanNextMove == 0:
        print('Please input 1~9, 0 is not an valid input for the next move!');
        isValid = 0;
    elif board[humanNextMove] != ' ':
        print('The space has already been used! Please select an empty space for the next move');
        isValid = 0;    
    return(isValid);    

# Ask the human player for the next move.
def askHumanNextMove(board):
    while True:
        print('Please input the next move!');
        c = input()
        if not c.isdigit():
            print('Invalid input! Please input [1-9]!');
            continue
        nextMove = int(c);
        if board[nextMove] == ' ':
            break;
        else:
            print('Stone already in this grid! Please input again!');
            continue;
    isValid = isValidInput(board, nextMove)
    return isValid,nextMove
        
def gameRsltDisplay(winner):    
    if   'A' == winner:
        print('AI win!');
    elif 'H' == winner:
        print('Human win!');
    else:    
        print('A tie game!');        

# Decide AI's next move.
# Decide whether the three input are all the same
def isTripleGoalReachedNext(board, idx1, idx2, idx3, role):
    in1 = board[idx1];
    in2 = board[idx2];
    in3 = board[idx3];
    
    if   in1 == ' ' and in2 == in3 and in2 == role:
        return idx1;
    elif in2 == ' ' and in1 == in3 and in3 == role:
        return idx2;
    elif in3 == ' ' and in1 == in2 and in1 == role:
        return idx3;
    else:
        return 0;   # Invalid space index.

def isGoalReachedNext(board, player):

    nextMove        = 0;
        
    nextMove  = isTripleGoalReachedNext(board, 1, 4, 7, GAME_ROLE[player]);
    if nextMove > 0:
        return True, nextMove
    nextMove  = isTripleGoalReachedNext(board, 1, 2, 3, GAME_ROLE[player]);
    if nextMove > 0:
        return True, nextMove
    nextMove  = isTripleGoalReachedNext(board, 1, 5, 9, GAME_ROLE[player]);
    if nextMove > 0:
        return True, nextMove
    nextMove  = isTripleGoalReachedNext(board, 2, 5, 8, GAME_ROLE[player]);
    if nextMove > 0:
        return True, nextMove
    nextMove  = isTripleGoalReachedNext(board, 3, 5, 7, GAME_ROLE[player]);
    if nextMove > 0:
        return True, nextMove
    nextMove  = isTripleGoalReachedNext(board, 3, 6, 9, GAME_ROLE[player]);
    if nextMove > 0:
        return True, nextMove
    nextMove  = isTripleGoalReachedNext(board, 4, 5, 6, GAME_ROLE[player]);
    if nextMove > 0:
        return True, nextMove
    nextMove  = isTripleGoalReachedNext(board, 7, 8, 9, GAME_ROLE[player]);
    if nextMove > 0:
        return True, nextMove

    return False, nextMove;
    
def naiveAiNextMove(board):

    # Temporarily, select the first empty space.
    # 1. First, check whether AI will reach the goal in the next step.
    #    GAME_ROLE[0] represents AI's role.
    goalReachedNext, nextMove = isGoalReachedNext(board, 0);
    
    if goalReachedNext == True:
        return nextMove;

    # 2. Secondly, check whether Human will reach the goal in the next step.
    #    GAME_ROLE[1] represents Human's role.
    #    Of course, AI should take the next move to blocking Human player to reach the goal.
    goalReachedNext, nextMove = isGoalReachedNext(board, 1);
    
    if goalReachedNext == True:
        return nextMove;
        
    # Randomly selected from the left spaces for the next move.
    spaces = []
    for k in range(1,10):    
        if board[k] == ' ':
            spaces.append(k)
        else:
            continue;
    nextMove = random.choice(spaces)
    
    return(nextMove);

def nextMove_complete_minimax(board, isMax, player, layer):
    '''
    Minimax algorithm for tic-tac-toe
    Decide the next move according to complete minimax algorithm for the current player
    board: Current board status, char array of 10 elements, ignoring [0]
    isMax: Is this eithe MAX of MIN layer for the current player
        True:  MAX layer
        False: MIN lafyer
    player: int
        0: Player represented by GAME_ROLE[0]
        1: Player represented by GAME_ROLE[1]
    '''
    if DEBUG:
        print('{0}Enter minimax(): board={1}, isMax={2}, player={3}'.format(layer*'+',board,isMax,player))
    
    bestScore = -1000 if isMax else 1000
    nextPlayer = 1 if player==0 else 0
    bestMove = 0
    
    # game over judge
    gameOver, winner = gameJudge(board)    
    if gameOver:
        if winner == ' ': # DRAW or TIE game           
            bestScore = 0
            if DEBUG:
                print('{0}GameOver: winner={1}, bestMove={2}, bestScore={3}'.format(layer*'+',winner,bestMove,bestScore))     
            return bestMove,bestScore
        else:
            # If it is the end of game, then it must be the win of the opponent.
            bestScore = (-1 if isMax else 1)
            if DEBUG:
                print('{0}GameOver: winner={1}, bestMove={2}, bestScore={3}'.format(layer*'+',winner,bestMove,bestScore))      
            return bestMove,bestScore

    for k in range(1,10):    
        if board[k] == ' ':
            board[k] = GAME_ROLE[player]
            move, score = nextMove_complete_minimax(board, (not isMax), nextPlayer, layer+1)
            board[k] = ' ' # Recover board status
            if isMax:
                if score > bestScore:
                    bestScore = score
                    bestMove  = k            
            else:
                if score < bestScore:
                    bestScore = score        
                    bestMove  = k              
    if DEBUG:                
        print('{0}Exit minimax(): bestMove={1}, bestScore={2}'.format(layer*'+',bestMove,bestScore))                    
    return bestMove, bestScore

# Decide whether the three input are all the same
def isTripleSame(in1, in2, in3):
    if in1 == ' ' or in2 == ' ' or in3 == ' ':
        return False
    elif in1 == in2 and in1 == in3:
        return True
    else:
        return False

def gameJudge(board):
    if   isTripleSame(board[1],board[4],board[7]):
        gameOver = True;        winner   = board[1];
    elif isTripleSame(board[1],board[2],board[3]):    
        gameOver = True;        winner   = board[1];
    elif isTripleSame(board[1],board[5],board[9]):        
        gameOver = True;        winner   = board[1];
    elif isTripleSame(board[2],board[5],board[8]):        
        gameOver = True;        winner   = board[2];
    elif isTripleSame(board[3],board[5],board[7]):        
        gameOver = True;        winner   = board[3];
    elif isTripleSame(board[3],board[6],board[9]):        
        gameOver = True;        winner   = board[3];
    elif isTripleSame(board[4],board[5],board[6]):        
        gameOver = True;        winner   = board[4];
    elif isTripleSame(board[7],board[8],board[9]):        
        gameOver = True;        winner   = board[7];
    elif ' ' in board[1:10]:     
        gameOver = False;       winner   = ' ';
    else:
        gameOver = True;        winner   = ' ';
            
    return gameOver, winner
    
whoseTurn = 0;         #  0 : AI's turn;   1:  Human's turn.
board     = [' ']*10;  #  Note: '*' for string means concatenation.

drawBoard(board,1); # Draw the initial board with numbering

while True:
    startNewGame, aiAlgo = askGameStart()
    if not startNewGame:
        print('Bye-Bye! See you next time!');
        sys.exit();
    else:
        ai_agent_msg = \
            'Naive AI, try to win it' \
            if aiAlgo==0 else \
            'Unbeatable minimax-AI, believe it or not, you cannot win absolutely!'
        print('You will fight with: ', ai_agent_msg);
    
    # Initialization.
    gameOver = 0;
    board    = [' ',' ',' ',' ',' ',' ',' ',' ',' ',' '];
    # Decide who, either human or AI, play first.
    # 0: AI; 1: human.
    print('Who play first? [0: AI; 1: human; 2: guess first]');
    cmd = input()
    if not cmd.isdigit():
        whoseTurn = random.randint(0,1);
    else:
        if int(cmd)==0:
            whoseTurn = 0
        else:
            whoseTurn = 1    

    while(not gameOver):    
        if whoseTurn == 0:
            print('AI\'s turn')
            if aiAlgo == 0:
                nextMove = naiveAiNextMove(board);
            else:
                layer = 9 - board.count(' ')
                nextMove, score = nextMove_complete_minimax(board,True,0,layer)
                print('nextMove = {0}, score = {1}'.format(nextMove, score))
            board[nextMove] = GAME_ROLE[0];
            whoseTurn = 1;
        else:
            print('Human\'s turn')
            isValid  = 0;
            while(not isValid):
                isValid, nextMove = askHumanNextMove(board);
            board[nextMove] = GAME_ROLE[1];
            whoseTurn = 0;

        drawBoard(board);               
        gameOver,winner = gameJudge(board);     
    
    gameRsltDisplay(winner);
                             

 4. Resumen

        La implementación anterior es un algoritmo minimax completo, es decir, la búsqueda minimax se realiza en cada paso hasta el final del juego de ajedrez, para obtener la solución óptima global. Tal minimax-AI es invencible (invencible) en Tic-Toe-Tac, ya sea la primera o la segunda mano. Pero dado que Tic-Toe-Tac es muy simple, si ambos jugadores toman decisiones perfectas, definitivamente obtendrán un empate. Por lo tanto, dos IA de este tipo que jueguen entre sí siempre darán como resultado un empate.

        Para juegos más complejos, la búsqueda minimax completa no es factible. En este caso, se debe implementar una búsqueda de profundidad limitada. A continuación, considere la transformación de la búsqueda minimax de profundidad limitada, y use más la poda alfa-beta para reducir la complejidad de la búsqueda para lidiar con juegos más complejos de dos jugadores (problema de juego adversario de suma cero).

2023-01-07 20:11 Corregir un error en gameJudge().

En la siguiente publicación:

        TicTacToe: implementación de agentes basada en el algoritmo TD(0) de diferencia de series temporales y el marco completo de implementación de python

Implementé un agente basado en el método de diferencia temporal en el aprendizaje por refuerzo, optimicé y mejoré el agente minimax y realicé pruebas de batalla entre varios agentes para confirmar si las diversas implementaciones son correctas. Bienvenido a mirar y corregirme. A continuación, considere la implementación del agente basado en el algoritmo Q-Learning en el aprendizaje por refuerzo, así que permanezca atento.

[Referencia]

[1] Diagrama de algoritmos de inteligencia artificial, Tsinghua University Press 

[2]   Algoritmo Minimax_PG-aholic's blog

[3] El algoritmo MinMax más claro y fácil de entender y una explicación detallada de la poda alfa-beta

[4] Principio del algoritmo Minimax

Supongo que te gusta

Origin blog.csdn.net/chenxy_bwave/article/details/128591542
Recomendado
Clasificación