En el contexto de la teoría de juegos combinatoria , que normalmente estudia juegos secuenciales con información perfecta , un árbol de juegos es un gráfico que representa todos los estados posibles del juego dentro de dicho juego. Estos juegos incluyen juegos muy conocidos como el ajedrez , las damas , el Go y el tres en raya . Esto se puede utilizar para medir la complejidad de un juego , ya que representa todas las formas posibles en que puede desarrollarse un juego. Debido a los grandes árboles de juego de juegos complejos como el ajedrez, los algoritmos diseñados para jugar esta clase de juegos utilizarán árboles de juego parciales, lo que hace que el cálculo sea factible en las computadoras modernas. Existen varios métodos para resolver árboles de juegos. Si se puede generar un árbol de juego completo, se puede utilizar un algoritmo determinista , como la inducción hacia atrás o el análisis retrógrado . Se pueden utilizar algoritmos aleatorios y algoritmos minmax como MCTS en los casos en que no sea factible un árbol de juego completo.
Para comprender mejor el árbol de juego, podemos considerarlo como una técnica para analizar juegos adversarios, que determinan las acciones que realiza el jugador para ganar el juego. En teoría de juegos, un árbol de juego es un gráfico dirigido cuyos nodos son posiciones en un juego (p. ej., la disposición de las piezas en un juego de mesa) y cuyos bordes son movimientos (p. ej., mover piezas de una posición en un tablero a otra). ). [1]
El árbol de juego completo para un juego es el árbol de juego que comienza en la posición inicial y contiene todos los movimientos posibles desde cada posición; el árbol completo es el mismo árbol que se obtiene a partir de la representación del juego en forma extensiva . Para ser más específicos, el juego completo es una norma para el juego en la teoría de juegos. Lo cual puede expresar claramente muchos aspectos importantes. Por ejemplo, la secuencia de acciones que las partes interesadas pueden tomar, sus elecciones en cada punto de decisión, información sobre las acciones tomadas por otras partes interesadas cuando cada parte interesada toma una decisión y los beneficios de todos los resultados posibles del juego. [2]
El diagrama muestra los dos primeros niveles, o capas , en el árbol de juego del tres en raya . Las rotaciones y reflejos de posiciones son equivalentes, por lo que el primer jugador tiene tres opciones de movimiento: en el centro, en el borde o en la esquina. El segundo jugador tiene dos opciones para responder si el primer jugador jugó en el centro; en caso contrario, cinco opciones. Etcétera.
El número de nodos de hoja en el árbol del juego completo es el número de formas posibles de jugar el juego. Por ejemplo, el árbol de juego del tres en raya tiene 255.168 nodos de hojas.
Los árboles de juegos son importantes en la inteligencia artificial porque una forma de elegir el mejor movimiento en un juego es buscar en el árbol de juegos utilizando cualquiera de los numerosos algoritmos de búsqueda de árboles , combinados con reglas tipo minimax para podar el árbol . Es fácil buscar en el árbol de juegos del tres en raya, pero los árboles de juegos completos para juegos más grandes como el ajedrez son demasiado grandes para buscarlos. En cambio, un programa de ajedrez busca en un árbol de juego parcial : normalmente tantas jugadas desde la posición actual como puede buscar en el tiempo disponible. Excepto en el caso de los árboles de juego "patológicos" [3] (que parecen ser bastante raros en la práctica), aumentar la profundidad de la búsqueda (es decir, el número de capas buscadas) generalmente mejora las posibilidades de elegir el mejor movimiento.
Los juegos de dos personas también se pueden representar como árboles y-o . Para que el primer jugador gane una partida, debe existir un movimiento ganador para todos los movimientos del segundo jugador. Esto se representa en el árbol y/o usando disyunción para representar los movimientos alternativos del primer jugador y usando conjunción para representar todos los movimientos del segundo jugador.
Con un árbol de juego completo, es posible "resolver" el juego, es decir, encontrar una secuencia de movimientos que el primer o el segundo jugador puedan seguir y que garanticen el mejor resultado posible para ese jugador (generalmente una victoria o un lazo). El algoritmo determinista (que generalmente se denomina inducción hacia atrás o análisis retrógrado ) se puede describir de forma recursiva de la siguiente manera.
El diagrama muestra un árbol de juego para un juego arbitrario, coloreado usando el algoritmo anterior.
Generalmente es posible resolver un juego (en este sentido técnico de "resolver") usando solo un subconjunto del árbol del juego, ya que en muchos juegos no es necesario analizar un movimiento si hay otro movimiento que es mejor para el mismo jugador ( por ejemplo, la poda alfa-beta se puede utilizar en muchos juegos deterministas).
Cualquier subárbol que pueda usarse para resolver el juego se conoce como árbol de decisión , y los tamaños de los árboles de decisión de diversas formas se utilizan como medidas de la complejidad del juego . [4]
Se pueden utilizar algoritmos aleatorios para resolver árboles de juegos. Hay dos ventajas principales en este tipo de implementación: rapidez y practicidad. Mientras que se puede realizar una versión determinista de la resolución de árboles de juegos en Ο ( n ) , el siguiente algoritmo aleatorio tiene un tiempo de ejecución esperado de θ ( n = 0,792 ) si cada nodo en el árbol de juegos tiene grado 2. Además, es práctico porque los árboles de juegos son aleatorios. Los algoritmos son capaces de "frustrar a un enemigo", lo que significa que un oponente no puede vencer al sistema de árboles de juego conociendo el algoritmo utilizado para resolver el árbol de juego porque el orden de resolución es aleatorio.
La siguiente es una implementación del algoritmo de solución de árbol de juego aleatorio: [5]
def gt_eval_rand ( u ) -> bool : """Devuelve Verdadero si este nodo se evalúa como ganador; de lo contrario, Falso""" si u . hoja : devolver u . gana de lo contrario : niños_aleatorios = ( gt_eval_rand ( niño ) para niño en orden_aleatorio ( u . niños )) si u . op == "O" : devuelve cualquiera ( niño_aleatorio ) si u . op == "Y" : devuelve todo ( niños_aleatorios )
El algoritmo hace uso de la idea de " cortocircuito ": si el nodo raíz se considera un operador " O ", una vez que se encuentra un Verdadero , la raíz se clasifica como Verdadero ; por el contrario, si el nodo raíz se considera un operador " Y ", una vez que se encuentra un False , la raíz se clasifica como False .
[6]