Algoritmos de Juegos
Algoritmos de Juegos
Algoritmos de Juegos
2
ESCUELA TÉCNICA SUPERIOR DE INGENIERÍA INFORMÁTICA
Realizado por
Cristina Gómez Gómez
Tutorizado por
José Antonio Montenegro Montes
Departamento
Lenguajes y Ciencias de la Computación
UNIVERSIDAD DE MÁLAGA
MÁLAGA, Septiembre 2016
Fecha defensa:
El Secretario del Tribunal
3
4
Resumen: La asignatura Sistemas Inteligentes, impartida en los tres grados de
Ingeniería Informática en la Universidad de Málaga, es un primer contacto del alumno
de informática con la Inteligencia Artificial (IA).
La asignatura tiene como objetivo mostrar al alumno una serie de técnicas de IA,
explicando detalladamente el funcionamiento de los algoritmos más representativos
de cada técnica. Para conseguir tal fin se realiza una doble tarea, inicialmente los
algoritmos son explicados en clase y se realizan ejercicios de una duración apropiada
para poder ser realizados a mano y en el laboratorio ejecutamos los algoritmos sobre
problemas más complejos para ver la aplicación del algoritmo a problemas más
extensos.
Abstract: The course “Intelligent Systems”, taught in the three degrees of Computer
Science at the University of Málaga, is student's first contact with the Artificial
Intelligence (AI).
The course aims to show students a number of AI techniques, explaining in detail the
most representative algorithms of each technique. In order to achieve this purpose a
double task is carried out, initially algorithms are explained in class and some exercises
with an appropiate duration are made by hand and later, in laboratory, we run these
algorithms to solve more complex problems in order to see the application of the
algorithm to more extensive problems.
Until now, the course professors have developed a number of practices about some
algorithms. The general idea is to update practices with current problems.
The aim of this project is to develop a practice that allows the student to observe in
detail and track the Minimax and Expectimax algorithms. Specifically, the chosen game
is 2048.
Introducción ............................................................................................................... 9
7
3.4 Aplicación de algoritmos al juego .....................................................................33
Resultados .................................................................................................................42
Conclusiones .............................................................................................................53
Anexos técnicos.........................................................................................................56
8
1 Introducción
¿Qué tenía de especial para los primeros informáticos el ajedrez? ¿Por qué sigue
siendo de tanto interés la aplicación de la inteligencia artificial a juegos? Los juegos
son interesantes porque son demasiado difíciles para resolverlos. Por ejemplo,
actualmente la complejidad de árbol de juego del ajedrez se calcula en torno a 10 123.
Esto, además, nos hace darnos cuenta que la eficiencia debe ser importante, pues el
árbol de juego suele ser de dimensiones realmente considerables.
9
Otros de los juegos típicos de la Inteligencia Artificial es el Tres en raya pues, debido
a su simplicidad, es útil usarlo de manera de ejemplo o introducción, para entender
los algoritmos y otros conceptos usados.
Pese a ser los juegos anteriores los más conocidos, existen infinidad de juegos más
en los que se ha aplicado la Inteligencia Artificial, por ejemplo, el Scrabble, Bridge o
Jeopardy, entre otros.
Para ello, hasta el momento los profesores de la asignatura han desarrollado una serie
de prácticas sobre algunos algoritmos de la asignatura.
Por tanto, la idea general es actualizar las prácticas con problemas actuales.
10
1.4 Contenido y estructura de la memoria
La memoria está dividida en 6 capítulos y los anexos.
El presente capítulo es, en sí, una introducción a los demás. Por ello incluye el estado
del arte, y la motivación y objetivos del trabajo, así como la estructura de la memoria
descrita en esta misma sección.
El capítulo tercero define el juego 2048, primer de manera “informal” para entender su
funcionamiento y posteriormente en función de los conceptos formales tratados en el
segundo capítulo de manera que se puedan aplicar los algoritmos.
El quinto capítulo analiza los resultados obtenidos mediante tablas y gráficas. Los
datos obtenidos nos permiten realizar comparaciones entre los diferentes algoritmos
y heurísticas propuestas.
Finalizamos el proyecto con una sección dedicada a las conclusiones obtenidas sobre
el trabajo.
11
Inteligencia artificial
2 para Juegos
2.1 Definición formal de un juego
Muchos problemas pueden ser resueltos mediante técnicas de inteligencia artificial.
Para ello tenemos que modelar los distintos estados posibles del problema, de manera
que el propósito será alcanzar un estado objetivo partiendo de un estado inicial
aplicando unas reglas entre estados, también llamadas movimientos en el caso de los
juegos.
● Movimientos: son reglas que modifican los estados, es decir, permiten pasar de
un estado a otro. No siempre se podrán aplicar todos los movimientos, sólo se
podrán aplicar los movimientos cuyas precondiciones se cumplen. Por tanto,
serán de la forma precondición → acción, donde la acción indica como
modificar el estado actual para generar un nuevo estado y la precondición
impone restricciones sobre la aplicabilidad de la regla según el estado actual.
● El test terminal determina cuándo termina el juego. A los estados donde el juego
ha terminado se les llama estados terminales. Habitualmente un estado será
terminal si es un estado objetivo o si no existen reglas aplicables.
El estado inicial y los movimientos legales para cada jugador definen el árbol de juego.
Habitualmente, se consideran que los jugadores son MAX y MIN de manera que
inicialmente es el turno MAX, y hacen sus movimientos alternativamente. Por ello,
cada nivel del árbol se corresponderá a uno de los jugadores.
Incluso un juego simple como las tres en raya es demasiado complejo para dibujar el
árbol de juegos entero.
2.2 Algoritmos
Necesitamos algoritmos que nos permitan decidir qué movimientos aplicar en cada
situación.
En palabras más coloquiales, podríamos decir que este algoritmo se encarga de elegir
el mejor movimiento para el jugador suponiendo que su rival escogerá el peor
movimiento para el jugador.
2.2.1.2 Ejemplo
14
Ilustración 2: Ejemplo del algoritmo Minimax
Por tanto, para optimizar minimax puede limitarse la búsqueda por nivel de
profundidad o por tiempo de ejecución. Otra posible técnica es el uso de la poda alfa-
beta, descrita en la siguiente sección.
15
2.2.2 Algoritmo Alfa-beta
La poda alfa beta es una optimización del algoritmo minimax, pues reduce el número
de nodos evaluados en el árbol de juego de dicho algoritmo.
En la poda alfa-beta se utilizan dos parámetros que actuarán de cotas sobre el valor
minimax de un nodo (α,β):
α valor de la mejor elección para MAX (valor más alto) que hemos encontrado
en el camino hasta ahora
β valor de la mejor elección para MIN (valor más bajo) que hemos encontrado
en el camino hasta ahora
Si el nodo es MAX y v ≥ β
Se puede podar por debajo de dicho nodo.
Si el nodo es MIN y v ≤ α
Se puede podar por debajo del nodo.
16
if v ≥ β then return v
α := max(α, v)
return v
2.2.2.2 Ejemplo
17
b) Se genera el siguiente hijo de B, cuyo valor es 12. Como 12 es mayor que el β
actual, el valor de β no cambia.
c) Se genera el siguiente hijo del nodo B, con valor 8 y ocurre lo mismo que en
caso anterior. Como ya se han generado todos los hijos del nodo B, ya
podemos darle definitivamente el valor de β, en este caso su valor será 3. Al
establecer un valor para el nodo B, ya podemos saber que el valor del nodo A
es 3 o más, pues es un nodo MAX. Por tanto, estará en el rango [3, ∞], y α = 3.
d) Se generan el primer hijo de C, el segundo hijo del nodo raíz A. Su valor es 2,
por tanto, como C es un nodo MIN, sabremos que su valor definitivo será menor
o igual que 2, es decir, está en el rango [-∞,2]. Por ello, β = 2.
Como sabemos que el nodo A tendrá un valor superior a 3, porque el nodo B
es mejor, podemos descartar el nodo C, pues tendrá un valor menor que 3. Por
eso no es necesario que generemos sus hijos restantes, es decir, se poda el
árbol.
e) Pasamos a generar los hijos del nodo D, tercer hijo del nodo raíz. Su valor es
14, por tanto, como C es un nodo MIN, sabremos que su valor definitivo será
menor o igual que 14, es decir, está en el rango [-∞,14]. Por ello, β = 14.
Como el valor de D será menor de 14, el valor de A, como mucho podrá valer
14, por tanto, estará en el rango [3,14].
2.2.2.3 Eficiencia
Tal y como hemos indicado antes, la complejidad del algoritmo minimax es O(bm). Si
lográramos ordenar los sucesores, el algoritmo Alfa-Beta tendrá solamente que
examinar O(bm/2). Esto implica que Alfa-beta podrá alcanzar un nivel
aproximadamente dos veces mayor que Minimax en la misma cantidad de tiempo.
3) Profundización progresiva
4) Tablas de transposición
5) Procedimiento MTG(f)
Existen juegos que incluyen un elemento aleatorio, juegos estocásticos. Uno de los
juegos más comunes de este tipo es el Backgammon, pero también podemos incluir
el 2048 en este tipo de juegos, como veremos a partir de su descripción en el siguiente
capítulo.
Por ello nos son de interés algoritmos que tengan en cuenta esta aleatoriedad.
Dichos nodos “CHANCE” se intercalan con los nodos MIN y MAX. En dichos nodos,
en lugar de tomar el máximo o el mínimo de los valores de utilidad de sus hijos, como
se hace en el Minimax, se toma una media ponderada de los valores de sus hijos, en
la cual los pesos serán las probabilidades de los sucesores.
La forma de intercalar dichos nodos depende de cada juego. Cada turno del juego se
evalúa como un nodo “MAX”, que representa el turno del jugador, un nodo “MIN”, que
representa al adversario, o un nodo “CHANCE”, que representa un jugador o efecto
aleatorio.
Por ejemplo, consideremos un juego en el que cada ronda consta de un solo tiro de
dados y, a continuación, las decisiones tomadas por el primer jugador de la IA, y luego
otro oponente inteligente. El orden de los nodos en este juego se alterna entre "
CHANCE ", " MAX " y " MIN ".
19
Ilustración 4: Ejemplo del algoritmo Expectiminimax
2. En el nivel 3 tenemos nodos MIN, por lo que cada nodo seleccionará el menor valor
de los valores de sus hijos, siendo 2,4,0 y -2 los valores escogidos.
4. El nodo de nivel 1, la raíz, es de tipo MAX, por lo que seleccionará el mayor valor
de ambos, el 3.
Este ejemplo es bastante sencillo y, además las probabilidades son las mismas para
todos los hijos, lo que correspondería a una media simple, sin embargo, no siempre
es así, pues la probabilidad dependerá del juego en concreto.
20
2.2.3.1.1 Pseudocódigo
En el caso del expectimax, sólo tendremos nodos MAX y nodos CHANCE, no habrá
nodos MIN.
2.2.3.2.1 Pseudocódigo
22
Inteligencia artificial
3 aplicada al 2048
Una vez vista la forma de definir formalmente los juegos y los algoritmos usados para
resolverlos, pasamos a definir el 2048 basándonos en los conceptos anteriores y
posteriormente a aplicar los algoritmos vistos. Para ello, antes es necesario conocer
dicho juego.
El objetivo del juego es conseguir una casilla cuyo valor sea 2048, de ahí su nombre,
deslizando y combinando las casillas del tablero.
En su versión original, el tablero contiene 4x4 casillas las cuales, a su vez, contienen
un número, de manera que las casillas tendrán distintos colores dependiendo del
número que contengan.
Inicialmente el tablero contendrá sólo dos casillas, con valor 2 o 4. A partir de ellas
podremos hacer algún movimiento hacia alguna dirección.
Mediante las teclas de dirección del teclado podemos mover las casillas, deslizándolas
por la cuadrícula. De esta manera podemos mover las casillas deslizándolas hacia
posiciones vacías.
Después de realizar un movimiento aparece una casilla nueva en un lugar vacío del
tablero, que contendrá el número 2 (en un 90% de los casos) o el número 4 (en el 10%
restante).
23
El usuario ganará el juego si consigue obtener una casilla con el número 2048. Sin
embargo, si no se puede hacer movimientos, es decir, ya no quedan lugares vacíos y
no existen casillas adyacentes con el mismo valor, el juego termina.
https://github.com/gabrielecirulli/2048
Debido a que alcanzó una popularidad abrumadora han sido numerosas las diferentes
implementaciones, en múltiples lenguajes y las diferentes versiones diseñadas a partir
de este juego, en parte ambas posibilitadas por el fácil acceso al código original de
Gabriel Cirulli.
Como digo, existen múltiples implementaciones, desde las que representan el tablero
con un sólo número, hasta las que consideran que el tablero es una matriz. Este es
un claro ejemplo de que existen diferentes formas de representar el espacio de
estados de los juegos.
24
Debemos definir el conjunto de estado y los movimientos para el 2048.
- Conjunto de estados: matriz de 4x4 que contenga casillas cuyos números sean
las potencias de 2, excluyendo el 0.
En el estado inicial aparecen dos casillas, las cuales pueden contener los
valores 2 o 4.
- Movimientos: En este juego los movimientos del usuario son distintos a los del
computador. Los movimientos del usuario serán mover las casillas hacia arriba,
derecha, abajo o izquierda. Sin embargo, para evitar bucles infinitos en la
ejecución de algoritmos de búsqueda, considero que no siempre es posible que
el jugador realice los cuatro movimientos. Realizar un movimiento será posible
siempre que al realizarlo se deslicen las casillas en el tablero o se combinen,
de manera que “cambie” algo en el tablero. Por tanto, si realizar dicho
movimiento no supondría ningún cambio, ni se combinan celdas ni se deslizan,
ese movimiento no estaría disponible.
- Test terminal.
Es necesario también establecer una función de utilidad que dé un valor a los estados
terminales, de manera que podamos aplicar los algoritmos vistos en el primer capítulo.
25
3.3 Funciones de evaluación: heurísticas
En este proyecto usamos dicha técnica, es decir, limitamos la profundidad de la
búsqueda en el espacio de estados, por lo que es necesario definir una buena
heurística que nos permita un alto porcentaje de partidas ganadas.
La heurística será una función matemática que asocia un valor a cada nodo en función
de lo “bueno” o “malo” que sea dicho estado para alcanzar un estado objetivo a partir
de él.
● Score (puntuación):
Una forma muy intuitiva de evaluar el estado podría ser la puntuación actual,
pues cuando mayor sea más piezas habremos combinado. Sin embargo, no
nos vale con combinar casillas sin ninguna estrategia, debemos considerar
otras características deseables como las siguientes.
● Casillas vacías:
Otra medida de interés que nos puede ayudar a determinar cómo de bueno es
un estado es el recuento de las casillas libres o vacías. No nos interesa tener
pocas casillas vacías, pues en el momento que el tablero se llene y no
podamos combinar ninguna casilla perderemos el juego. Sin embargo, en
estados iniciales es más común tener muchas casillas vacías y en estados
más avanzados lo normal es que queden menos. Por ello podemos ponderar
el número de casillas vacías por la puntuación actual.
26
● Monotonía:
Uno de los trucos principales que la mayoría de jugadores sigue al jugar al
juego es colocar la casilla con mayor valor en una esquina y, a partir de ella ir
decreciendo su valor. Normalmente también evitaremos que las casillas con
valores más pequeños queden aisladas. Esta heurística nos permite tender a
tener un tablero más organizado. Para ello, sumamos los valores de las casillas
del tablero ponderados de la manera siguiente
7 6 5 4
6 5 4 3
5 4 3 2
4 3 2 1
Heurística 1:
Combinamos:
● la puntuación (score)
● la medida de la monotonía (suma de los valores de la matriz ponderados)
● Similitud: obtenemos la media de las diferencias, como lo que queremos es
minimizarlo, lo ponemos en negativo.
● log(Score)*Celdas vacías: tenemos en cuenta el número de celdas vacías, pero
ponderado con el logaritmo del score. Es bastante importante tener casillas
27
vacías, lo que nos da un mayor margen para realizar movimientos y continuar
jugando, sobre todo esto es de valorar en estados avanzados, pues en estados
iniciales es más común encontrarnos con muchas celdas vacías. Una manera
de valorar esto es ponderar el número de celdas vacías por el logaritmo de la
puntuación actual. Usamos el logaritmo para tener en cuenta la puntuación sin
que se alcancen valores muy altos ya que si esto ocurre prácticamente sólo se
tendría en cuenta esta característica.
Heurística 2:
En este caso no tenemos en cuenta el Score en la suma, por ello es más rápida que
la heurística anterior, pero según los resultados no obtenemos mejor solución que la
heurística 1. Es conveniente usarla cuando prima la velocidad.
Heurística 3:
Heurística 4:
Siendo log2(max) el logaritmo en base 2 del valor máximo del tablero. Utilizamos el
logaritmo por la misma razón que en los casos anteriores ya que si utilizáramos el
valor máximo en lugar de su logaritmo, dicho sumando alcanzaría valores altos lo que
provocaría que sólo éste fuera tomado en cuenta.
3.3.1.1 Ejemplos
Para entender mejor las heurísticas anteriores las calcularemos para varios estados.
28
Ejemplo 1
En primer lugar, vamos a calcular las características antes mencionadas para dicho
estado.
Score.
Se puede ver en la figura 6, la puntuación actual es 8.
Casillas vacías
El número de casillas vacías es 12.
Similitud
Esta medida calcula una media de la diferencia del valor entre las casillas
vecinas. Puesto que nos interesa que los números iguales o, al menos,
parecidos estén cercanos, se obtienen las distancias entre los vecinos de cada
celda, acumulando la media de las distancias para cada celda con sus vecinos.
|4−4|+ |4−2|+ |4−4| 2
o 𝐶𝑒𝑙𝑑𝑎 (0,0): = = 0.66666
3 3
|2−4|+ |2−2|+|2−2|+ |2−4| 4
o 𝐶𝑒𝑙𝑑𝑎 (0,1): = =1
4 4
|2−2|+|2−2| 0
o 𝐶𝑒𝑙𝑑𝑎 (0,2): = =0
2 2
|4−4|+ |4−2|+ |4−4| 2
o 𝐶𝑒𝑙𝑑𝑎(1,0): = = 0.66666
3 3
7
𝑆𝑖𝑚𝑖𝑙𝑖𝑡𝑢𝑑 = = 2.3333333
3
Monotonía
Suma de los valores del tablero ponderada mediante los valores de la tabla:
29
7 6 5 4
6 5 4 3
5 4 3 2
4 3 2 1
𝑀𝑜𝑛𝑜𝑡𝑜𝑛í𝑎 = 4 ∗ 7 + 4 ∗ 6 + 2 ∗ 6 + 0 ∗ 5 + 0 ∗ 5 + 2 ∗ 5 + 0 ∗ 4 + 0 ∗ 4
+ 0∗4 + 0∗4 + 0∗3 + 0∗3 + 0∗3 + 0∗2 + 0∗2 + 0
∗ 1 = 74
Heurísticas:
Heurística 1 = Score + Monotonía − Similitud + log(Score) ∗ Celdas vacías
= 8 + 74 − 2.333 + log(8) ∗ 12 = 104.6199651668247
≈ 104.62
Ejemplo 2
30
Score.
Se puede ver en la figura 7, la puntuación actual es 380.
Casillas vacías
El número de casillas vacías es 8.
Similitud
|64−64|+ |64−8|+ |64−16|+ |64−4| 164
o 𝐶𝑒𝑙𝑑𝑎 (0,0): = = 41
4 4
|8−64|+ |8−8|+|8−4|+ |8−16|+ |8−4| 72
o 𝐶𝑒𝑙𝑑𝑎 (0,1): = = 14.4
5 5
|4−8|+|4−4|+|4−2|+|4−4| 6
o 𝐶𝑒𝑙𝑑𝑎 (0,2): = = 1.5
4 4
|2−4|+|2−2| 2
o 𝐶𝑒𝑙𝑑𝑎 (0,3): = =1
2 2
|16−64|+ |16−8|+ |16−16| + |16−4|+ |16−4| 80
o 𝐶𝑒𝑙𝑑𝑎(1,0): = = 16
5 5
|4−64|+ |4−8|+ |4−4|+ |4−16|+ |4−4|+ |4−4| 76
o 𝐶𝑒𝑙𝑑𝑎 (1,1): = = 12.66666
6 6
|4−16|+ |4−4|+|4−4| 12
o 𝐶𝑒𝑙𝑑𝑎 (2,0): = =4
3 3
|2−2| 0
o 𝐶𝑒𝑙𝑑𝑎 (3,3): = =0
1 1
𝑆𝑖𝑚𝑖𝑙𝑖𝑡𝑢𝑑 = 90.56666666666668
Monotonía
𝑀𝑜𝑛𝑜𝑡𝑜𝑛í𝑎 = 64 ∗ 7 + 16 ∗ 6 + 8 ∗ 6 + 4 ∗ 5 + 4 ∗ 5 + 4 ∗ 5 + 0 ∗ 4 + 0 ∗ 4
+ 0∗4 + 2∗4 + 0∗3 + 0∗3 + 0∗3 + 0∗2 + 0∗2 + 2∗1
= 662
Heurísticas:
Heurística 1 = Score + Monotonía − Similitud + log(Score) ∗ Celdas vacías
= 380 + 662 − 90.56666666666668 + log(380) ∗ 8
= 998.9547033550967 ≈ 998.95
31
Ejemplo 3
Score.
Se puede ver en la figura 8, la puntuación actual es 9228.
Casillas vacías
El número de casillas vacías es 7.
Similitud
|1024−1024|+ |1024−32|+ |1024−16|+ |1024−4| 3020
o 𝐶𝑒𝑙𝑑𝑎 (0,0): = = 755
4 4
|32−1024|+ |32−32|+|32−4|+ |32−16|+ |32−4| 1064
o 𝐶𝑒𝑙𝑑𝑎 (0,1): = = 212.8
5 5
|4−32|+|4−4|+|4−2|+|4−4| 30
o 𝐶𝑒𝑙𝑑𝑎 (0,2): = = 7.5
4 4
|2−4|+|2−2| 2
o 𝐶𝑒𝑙𝑑𝑎 (0,3): = =1
2 2
|16−1024|+ |16−32|+ |16−16| + |16−4|+ |16−8| 1044
o 𝐶𝑒𝑙𝑑𝑎(1,0): = = 208.8
5 5
|4−1024|+ |4−32|+ |4−4|+ |4−16|+ |4−4|+ |4−8| 1064
o 𝐶𝑒𝑙𝑑𝑎 (1,1): = = 177.3333
6 6
|8−16|+ |8−4|+|8−8|+|8−2| 18
o 𝐶𝑒𝑙𝑑𝑎 (2,0): = = 4.5
4 4
|2−8|+ +|2−2| 6
o 𝐶𝑒𝑙𝑑𝑎 (3,0): = =3
2 2
|4−4| 0
o 𝐶𝑒𝑙𝑑𝑎 (3,2): = =0
1 1
𝑆𝑖𝑚𝑖𝑙𝑖𝑡𝑢𝑑 = 1369.9333
Monotonía
𝑀𝑜𝑛𝑜𝑡𝑜𝑛í𝑎 = 1024 ∗ 7 + 16 ∗ 6 + 32 ∗ 6 + 8 ∗ 5 + 4 ∗ 5 + 4 ∗ 5 + 2 ∗ 4 + 0
∗4 + 0∗4 + 2∗4 + 0∗3 + 0∗3 + 0∗3 + 4∗2 + 0∗2 + 0
∗ 1 = 7560
32
Heurísticas:
Heurística 1 = Score + Monotonía − Similitud + log(Score) ∗ Celdas vacías
= 9228 + 7560 − 1369.9333333333332 + log(9228) ∗ 7
= 15481.976650001723 ≈ 15481.98
¿De qué forma será dicho árbol? ¿Cuántos hijos tendrá cada nodo?
Un nodo MIN tendrá tantos hijos como casillas vacías multiplicado por dos, ya que,
tras tu turno, el computador podrá poner una ficha en cada posición vacía con valor 2
o con valor 4.
Un nodo MAX podrá tener, a lo sumo, cuatro hijos, correspondientes a los cuatro
movimientos posibles (arriba, derecha, abajo e izquierda). Sin embargo, si al realizar
un movimiento sobre un tablero no se produce ningún cambio, dicho movimiento se
considerará no válido para evitar ciclos.
33
Ilustración 9: Parte del árbol de juego de 2048
En el capítulo anterior hemos descrito además del algoritmo Minimax y la poda Alfa-
beta, el algoritmo expectimax, utilizado en juegos que incluyen un elemento aleatorio.
Sin lugar a dudas el 2048 es de este tipo de juegos, ya que la introducción de una
nueva casilla tras cada movimiento se realiza de manera aleatoria.
Por tanto, es posible utilizar el algoritmo expectimax en el juego 2048, pues en el turno
del jugador, estaríamos en un nodo MAX, y en el turno del ordenador, que añade un
valor (2 o 4) en una celda aleatoria, estaríamos en un nodo CHANCE.
Recordemos que el valor de un nodo CHANCE vendrá dado por una media ponderada
de los valores de sus sucesores en la cual los pesos serán las probabilidades de estos.
- Si se añade un 2:
1
𝑝𝑟𝑜𝑏𝑎𝑏𝑖𝑙𝑖𝑑𝑎𝑑 = ∗ 0.9
𝑁ú𝑚𝑒𝑟𝑜 𝑑𝑒 𝑐𝑎𝑠𝑖𝑙𝑙𝑎𝑠 𝑣𝑎𝑐í𝑎𝑠
- Si se añade un 4:
1
𝑝𝑟𝑜𝑏𝑎𝑏𝑖𝑙𝑖𝑑𝑎𝑑 = ∗ 0.1
𝑁ú𝑚𝑒𝑟𝑜 𝑑𝑒 𝑐𝑎𝑠𝑖𝑙𝑙𝑎𝑠 𝑣𝑎𝑐í𝑎𝑠
34
Hay otra cosa a tener en cuenta con el algoritmo Expectimax, pues la función de
evaluación heurística debe tener unos requisitos especiales. Con Minimax, la escala
de los valores no importa, sin embargo, Expectimax necesita utilizar valores grandes,
ya que, al realizar las medias ponderadas progresivamente hacia arriba, los valores
se van a ir reduciendo, de manera que si a los nodos terminales se les asocia un valor
muy pequeño el valor que llegará a la raíz será siempre 0. Por ello, como para Minimax
y Alfa-Beta, no importa la escala, podemos seleccionar funciones de evaluación que
asocien números grandes, de manera que las descritas en la sección anterior cumplen
esta propiedad.
35
Diseño de la
4 aplicación
El presente trabajo está diseñado de manera que pueda ser de utilidad en la
explicación de los contenidos de la asignatura Sistemas Inteligentes.
Por ello, la implementación del 2048 abarcada en este trabajo está elaborada en
función de la biblioteca AIMA, de manera que pueda servir como práctica de la
asignatura anteriormente citada.
Pasamos a mostrar el diagrama UML del proyecto. Sin embargo, por su tamaño es
imposible analizarlo en su totalidad, por lo que lo haremos por partes.
36
Ilustración 10: Diagrama UML del Juego
37
La figura 10 muestra las clases necesarias para modelar el juego en sí. La clase
Movimiento nos permitirá identificar y diferenciar las diferentes acciones del juego.
Una manera de optimizar dicho algoritmo era limitando la profundidad, por lo que he
creado las clases necesarias para usar los algoritmos Minimax y Alfa-beta con
limitación de la profundidad.
38
Otro de los algoritmos comentados, el Expectimax, también ha sido implementado con
el mismo objetivo.
En la figura 11 podemos ver las relaciones entre los algoritmos implementados. Las
clases AlphaBetaLimitado, MinimaxLimitado y Expectimax implementan la interfaz
AdversarialSearch, implementado los métodos de ésta. En ellas se ejecutan los
algoritmos descritos en el presente documento. Para ello hacen uso de un objeto de
la clase Game, pues necesitarán una partida. Sin embargo, en la clase Expectimax
será de tipo Game2048, pues se usan probabilidades que dependerán del juego en
concreto.
39
Ilustración 12: Diagrama UML de la interfaz gráfica
40
Con el fin de implementar la interfaz gráfica se han creado las clases de la figura 12.
La clase G2048App implemente la interfaz gráfica y contiene diferentes clases como
paneles, frames y ventanas de diálogo, necesarias en la aplicación y se hace del uso
de un KeyEventDispatcher, para recoger la entrada por teclado.
41
5 Resultados
Para la obtención de los resultados, se ejecutan 100 partidas para cada caso,
obteniendo el porcentaje de partidas ganadas y una media de duración de las partidas.
Las pruebas se han realizado para cada algoritmo, cada heurística y con límites de
profundidad desde 1 hasta 7 y las 100 partidas de prueba se han generado de manera
aleatoria, usándose las mismas partidas en todos los casos.
42
Alfa-Beta
Porcentaje de partidas Tiempo medio por partida
Heurística Profundidad
ganadas (s)
1 0 0.001794711
2 0 0.01560342
3 51 0.139458503
Heurística 1 4 40 0.573280988
5 62 2.706512032
6 69 12.069926438
7 73 43.486167867
1 0 0.001767663
2 0 0.014403909
3 3 0.10498618
Heurística 2 4 17 0.447963035
5 61 2.292564486
6 59 9.381812238
7 76 34.973646577
1 0 0.00100925
2 0 0.013722845
3 7 0.109398463
Heurística 3 4 35 0.685844045
5 32 3.055214546
6 56 16.89068889
7 59 59.879851354
1 0 0.001548159
2 0 0.012754492
3 30 0.133171757
Heurística 4 4 40 0.513441004
5 57 2.689431297
6 53 11.029131179
7 65 48.601640667
Tabla 2: Resultados del algoritmo Alfa-Beta
43
Minimax
Porcentaje de partidas Tiempo medio por partida
Heurística Profundidad
ganadas (s)
1 0 0.00190301
2 0 0.015617527
3 51 0.1881203765
Heurística 1 4 41 1.255284443
5 64 8.8477722355
6 65 63.884055994
7 75 376.912523012
1 0 0.00160323
2 0 0.014963952
3 5 0.137765874
Heurística 2 4 15 0.932783362
5 68 7.471190727
6 71 50.547010795
7 72 295.228987382
1 0 0.001080002
2 0 0.017641039
3 5 0.1457578025
Heurística 3 4 40 1.285479457
5 49 8.98162815
6 57 65.250421968
7 62 393.458308591
1 0 0.001565401
2 0 0.011273029
3 29 0.164911558
Heurística 4 4 38 1.043750885
5 54 8.753824515
6 62 56.27655640
7 66 364.923192912
Tabla 3: Resultados del algoritmo Minimax
44
Expectimax
Porcentaje de partidas Tiempo medio por partida
Heurística Profundidad
ganadas (s)
1 0 0.0056195
2 0 0.019205236
3 56 0.235435689
Heurística 1 4 84 1.921496353
5 87 11.918400834
6 94 107.034087053
7 97 646.254418221
1 0 0.00569638
2 0 0.018926718
3 12 0.185022204
Heurística 2 4 58 1.508970255
5 80 10.242626022
6 93 90.854049336
7 95 645.943074468
1 0 0.00338204
2 0 0.012725246
3 10 0.148787463
Heurística 3 4 54 1.656921287
5 57 10.20525689
6 78 91.598982443
7 87 645.666991384
1 0 0.00504987
2 0 0.014774837
3 63 0.224199239
Heurística 4 4 80 1.569982214
5 87 11.049808607
6 88 83.185857268
7 94 586.569852
Tabla 4: Resultados del algoritmo Expectimax
45
Evidentemente, el número de partidas ganadas dependerá de cada ejecución en
concreto, pues el computador actúa de manera aleatoria.
Sólo se ha realizado baterías de pruebas en aquellos casos cuya duración por partida
no es superior a 10 minutos aproximadamente. Sin embargo, si no pusiéramos esta
restricción, es decir, si estableciéramos un límite de profundidad mayor, los resultados
serían mejores, incrementándose el porcentaje de partidas ganadas.
Las próximas cuatro figuras nos muestran el porcentaje de partidas ganadas para
cada heurística en función del algoritmo usado y la profundidad de búsqueda.
Heurística 1
100
PORCENTAJE DE PARTIDAS GANADAS
90 97
94
80 87
84
70 75
73
60 69
62 64 65
50 56
40 51 51
30 40 41
20
10
0
3 4 5 6 7
PROFUNDIDAD
46
Heurística 2
100
PORCENTAJE DE PARTIDAS GANADAS
90 95
93
80
70 80
76
71 72
60 68
58 61 59
50
40
30
20
10 3 5 12 17 15
0
3 4 5 6 7
PROFUNDIDAD
Heurística 3
100
PORCENTAJE DE PARTIDAS GANADAS
90
80 87
70 78
60
62
50 57 57 59
54 56
40 49
40
30 35
32
20
10
7 5 10
0
3 4 5 6 7
47
Heurística 4
100
PORCENTAJE DE PARTIDAS GANADAS
90 94
80 87 88
80
70
60 65 66
63 62
50 57
54 53
40
40 38
30
30 29
20
10
0
3 4 5 6 7
Podemos ver cómo, sin lugar a dudas, para las cuatro heurísticas, el algoritmo que
mejor funciona, obteniendo un mayor número de partidas ganadas, es el espectimax
con diferencia.
Básicamente, con el expectimax podemos alcanzar los mismos resultados que con los
demás algoritmos estableciendo un límite de profundidad más pequeño.
48
Las siguientes figuras nos servirán para comparar entre las heurísticas.
Alfa-Beta
80
PORCENTAJE DE PARTIDAS GANADAS
70 76
73
69
60 65
62 61
57 59 59
50 56
51 53
40
40 40
30 35
30 32
20
10 17
3 7
0
3 4 5 6 7
PROFUNDIDAD
Minimax
80
PORCENTAJE DE PARTIDAS GANADAS
70 75
71 72
60 68 66
64 65
62 62
50 57
54
51 49
40
41 40 38
30
29
20
10 15
5 5
0
3 4 5 6 7
PROFUNDIDAD
49
Expectimax
100
PORCENTAJE DE PARTIDAS GANADAS
90 97 95
94 93 94
80 87 87 88 87
84
70 80 80 78
60
63
50 56 58 57
54
40
30
20
10 12 10
0
3 4 5 6 7
PROFUNDIDAD
Como vemos, una de las mejores heurísticas es la primera, pues destaca sobre las
demás en la mayoría de los casos. Cuando el límite de profundidad es más alto, las
heurísticas 1 y 2 están bastante igualadas. Sin embargo, cuando el límite de
profundidad es bajo (3 o 4), sin duda es mucho mejor la primera.
Vemos también que en la mayoría de los casos la peor heurística es la tercera, por lo
que no nos conviene usarla si lo que queremos es obtener un porcentaje alto de
victorias.
Como se puede intuir, el algoritmo Alfa-Beta, será el más rápido. Para comparar los
tiempos entre los diferentes algoritmos según la heurística podemos analizar las
siguientes imágenes.
50
Ilustración 20: Tiempo medio de las partidas para las diferentes heurísticas en función del algoritmo y la
profundidad
Se ve claramente que el algoritmo más rápido es, tal y como se esperaba, el Alfa-
Beta. Mientras que el Minimax y el Expectimax tienen unos tiempos muy similares,
siendo éste último algo más lento, pues tiene que hacer un número mayor de cálculos
correspondientes con las probabilidades.
51
Ilustración 21: Tiempo medio de las partidas para cada algoritmo
Por ejemplo, podemos ver que la heurística más costosa en cuanto al tiempo es la
heurística número 3, que además era la que peores resultados obtenía. Por lo que
esto nos puede indicar que es mejor descartar dicha heurística, lo que nos indica que
la monotonía es una característica muy importante a mantener en el tablero, ya que
esta heurística no la tiene en cuenta.
52
6 Conclusiones
Como bien comentábamos en la primera sección, los juegos siempre han sido un reto
para la Inteligencia Artificial desde sus inicios, aplicándose cada día tanto a los juegos
más novedosos como a los antiguos. Por ello, la asignatura Sistemas Inteligentes se
encarga de mostrar dichas técnicas, para lo que hace uso de prácticas de laboratorio.
Con el objetivo de actualizar dichas prácticas, se proponía aplicar todos los conceptos
estudiados al juego 2048 de manera que la aplicación sirviera como apoyo a la
enseñanza de éstos. Esto nos obligaba a definir varios conceptos como los estados,
los movimientos, la función de utilidad y una función de evaluación heurística. En este
caso han sido propuestas cuatro funciones de evaluación con el objetivo posterior de
probar su funcionamiento.
Por tanto, para aplicar las técnicas estudiadas a este juego, éste debe definirse de
manera formal, definiendo los conceptos antes mencionados, lo que hace necesario
un análisis del juego.
Una vez definido formalmente el juego, podemos aplicar los algoritmos, en este caso
el Minimax, la poda Alfa-Beta y el Expectimax, aplicable únicamente en juegos
estocásticos, que incluyan un elemento aleatorio como el 2048.
La aplicación creada es una forma de ver lo que nos permiten estos algoritmos. Sus
funcionalidades aparecen en el manual de usuario, en el Anexo B.
53
Por tanto, se cumplen los objetivos previstos, ya que se ha elaborado la aplicación
propuesta, de manera que sirva como práctica de la asignatura y todos los análisis y
definiciones que su desarrollo conlleva.
54
Referencias bibliográficas
[1] Russell, S. and Norvig, P. (2010). Artificial Intelligence: A Modern Approach (3th
ed.). Upper Saddle River, NJ: Prentice Hall.: http://aima.cs.berkeley.edu/
[2] What is the optimal algorithm for the game 2048?. Stackoverflow.com. [Fecha de
consulta: 10 agosto 2016] Disponible en:
http://stackoverflow.com/questions/22342854/what-is-the-optimal-algorithm-for-
the-game-2048
55
Anexos técnicos
Anexo A. Implementación
El presente trabajo está diseñado de manera que pueda ser de utilidad en la
explicación de los contenidos de la asignatura Sistemas Inteligentes.
Por ello, la implementación del 2048 abarcada en este trabajo está elaborada en
función de la biblioteca AIMA, de manera que pueda servir como práctica de la
asignatura anteriormente citada.
En este caso vamos a diferenciar dos tipos de movimientos: el que hace el usuario y
el que hace el ordenador.
1. Mover las fichas: puede ser en 4 direcciones distintas (arriba, derecha, abajo e
izquierda)
2. Añadir una ficha nueva.
56
o si el movimiento trata de mover las fichas, esta variable contendrá la
dirección
Por lo tanto, tenemos dos constructores, uno para cada tipo de movimiento.
Clase G2048State
Nos permite identificar el tablero de la partida y contiene los métodos necesarios para
su tratamiento.
57
Constructores
Se inicializa la tabla con 0 (casillas vacías) y se añaden dos nuevas con el método
nuevaCelda().
Hay otro constructor que recibe como parámetro una matriz de enteros, un tablero.
Nueva celda
Este método añade una celda en una posición vacía. Primero se obtiene una lista de
celdas vacías mediante el método getVacias(). Si dicha lista no está vacía podremos
añadir. Seleccionamos un índice aleatorio entre las vacías y se le añade 2 o 4, de
manera que el 2 tendrá una probabilidad de salir del 90% y el 4, un 10%.
58
Rellenar celda
Se recorre la tabla y se añade a una lista los índices de las casillas vacías.
59
Goal
Mover celdas
Este método realiza los movimientos que puede hacer el usuario en el tablero (arriba,
derecha, abajo e izquierda). Por simplificar, he realizado el algoritmo para hacer el
movimiento hacia la izquierda, de manera que si se requiere mover en otra dirección,
se gire el array y se use el mismo algoritmo en todos los casos.
int sumas = 0;
60
int indiceAux=0;
for(int j=1; j < NUM_TILES; j++) {
if(board[i][j]!=0) {
//Comprobamos lo que hay antes de la celda para ver si
podemos mover o sumar
int anterior = j-1;
while(anterior > indiceAux && board[i][anterior] ==0)
anterior--;
}
}
score+=sumas;
61
Rotar
Completo
Devuelve true si no hay celdas adyacentes con el mismo valor ni casillas vacías.
62
Terminal
En este juego hay dos posibilidades para que un estado sea terminal:
getActions
Devuelve una lista de movimientos que se pueden hacer desde el tablero actual.
Comprobamos, para cada dirección, si al realizar ese movimiento sobre una copia, el
tablero cambia, pues en ese caso sí se puede realizar el movimiento. En caso de que
el tablero se quede igual no podremos realizar dicho movimiento. Es decir, si al mover
hacia la izquierda no varía nada, no será un movimiento posible.
Los movimientos disponibles serán añadir un 2 o un 4 a cada una de las celdas vacías.
63
Move
Este método recibe una variable de tipo Movimiento, que indicará el movimiento a
realizar sobre el tablero.
En el caso de que la variable mover del objeto action sea true, llamaremos al método
moveCells con la dirección que almacena dicho objeto. En el caso de que sea false,
significa que tendremos que añadir una nueva celda, por lo que llamamos al método
rellenarCelda, en las coordenadas y con el valor que nos indica action.
64
Getters
getUtility()
65
Similitud
Puesto que nos interesa que los números iguales o, al menos, parecidos estén
cercanos, obtengo las distancias entre los vecinos de cada celda, acumulando la
media de las distancias para cada celda con sus vecinos.
Matriz valores
66
7 6 5 4
6 5 4 3
5 4 3 2
4 3 2 1
67
Heurísticas
68
Clone
isEqual
toString
69
Clase Game2048
En este caso, el estado vendrá dado por la clase G2048State. Las acciones será de
la clase Movimiento. Los jugadores (humano y ordenador) serán representados por
enteros.
70
Implementación de los algoritmos
Además, tal y como hemos comentado antes, el algoritmo Minimax es imposible de
utilizarlo en la práctica en problemas grandes, pues la complejidad es demasiado alta.
Una manera de optimizar dicho algoritmo era limitando la profundidad, por lo que he
creado las clases necesarias para usar los algoritmos Minimax, Alfa-beta y
Expectimax con limitación de la profundidad.
En el caso del algoritmo Expectimax, se usa la misma función maxValue del algoritmo
Minimax limitado y debe implementarse la función expValue, pues no es
proporcionado por AIMA.
Implementación gráfica
La clase G2048App contiene el código asociado a la interfaz gráfica. A su vez contiene
clases asociadas a los paneles, frames y dialogs de los que hace uso. Describo a
continuación lo más relevante del proyecto: la clase GPanel, encargada de dibujar el
tablero y el método solveAndPaint cuyo objetivo es resolver la partida e ir mostrando
la ejecución de la misma.
Clase GPanel
Método solveAndPaint()
Otro método relevante del proyecto es el solveAndPaint, cuyo objetivo es, como su
nombre indica, resolver y pintar el tablero a medida que va eligiendo los movimientos.
Por ello es necesario que se lance una hebra.
En cada iteración del bucle se llamará a repaint(), de manera que se volverá a dibujar
el tablero, actualizándose.
Existe las opciones de escribir los movimientos en un fichero, siempre que la opción
recorder, esté activada.
El código restante hace referencia a los diferentes elementos de las ventanas para las
distintas funcionalidades de la aplicación.
Hay varias clases correspondientes a los JFrames, JDialog y JPanel utilizados, que
hacen uso de los múltiples diversos componentes de Java Swing para la composición
de la interfaz como JMenu, JFileChooser, JButton, JComboBox,
JCheckBoxMenuItem, JLabel, JScrollPanel… entre otros.
Para recoger la entrada de teclado mediante las flechas en el juego, se hace uso de
la clase KeyEventDispatcher ya que KeyListener, utilizado en las prácticas de la
asignatura, no funciona al tener varios paneles. De esta manera también sirve de
ejemplo, pues es una manera alternativa de recoger la entrada por teclado.
Para empezar una partida nueva debemos clicar en el botón “Juego nuevo” en la parte
superior de la ventana (Figura 22). Antes de pulsar dicho botón, podemos ver en la
74
imagen 22, que las opciones de “Proponer” o “Resolver” están desactivadas ya que
primero debe iniciarse el juego.
Una vez hayamos pulsado sobre “Juego nuevo”, el tablero se inicializará y aparecerán
valores en dos de las casillas. A partir de ese momento ya sí se nos permite pulsar
en “Proponer” o “Resolver”, al igual que usar las flechas del teclado para jugar de la
manera tradicional al juego.
Para elegir la profundidad, tenemos otro desplegable (figura 25) en el cual podremos
elegir una profundidad entre 1 y 7, de manera que si queremos seleccionar una
profundidad mayor deberemos seleccionar la opción “Más” que abrirá un cuadro de
diálogo donde podremos escribir la profundidad deseada (figura 26).
Una vez elegidos los parámetros deseados, podremos pedir que se efectúe un solo
movimiento tomando la decisión aplicando el algoritmo elegido o pedir que se resuelva
el juego al completo. Para ello sirven los botones “Proponer” y “Resolver”.
76
Por ejemplo, tenemos el siguiente juego (figura 27) con los parámetros que aparecen
seleccionados:
77
Pulsamos “Proponer” y obtenemos:
Ilustración 28: Tablero obtenido tras pulsar la opción “Proponer” con los parámetros seleccionados
Ahora tenemos tres opciones para continuar la partida: continuar jugando mediante
las flechas, volver a pedir un movimiento o pedir que se resuelva por completo.
78
Si elegimos esta tercera opción debemos pulsar el botón “Resolver”. Tras pulsarlo, el
tablero comenzará a resolverse, de manera que la velocidad entre los movimientos
dependerá del algoritmo utilizado y la profundidad seleccionada. Finalmente se
obtendrá un tablero en el cual aparezca la casilla 2048 o en el que no se puedan
realizar más movimientos.
79
Ilustración 30: Menús para guardar partidas
Podemos elegir el nombre del fichero y dónde guardarlo, mediante “Elegir ubicación”
en el mismo menú (figura 30). En ese caso se abrirá un explorador de archivos que
nos permitirá elegir dónde vamos a guardar el archivo y el nombre de este (figura 31).
Cuando queramos reproducir una partida almacenada debemos pulsar “Elegir archivo”
dentro del menú “Reproducir” (figura 32).
80
Una vez pulsado se abrirá un explorador de archivos que nos permitirá seleccionar el
fichero que contenga la partida que deseamos reproducir. Al seleccionar el archivo de
la partida se abrirá una ventana nueva con el aspecto mostrado en la figura 33.
Veremos el tablero inicial, el primero que se grabó. En este caso se trata del tablero
inicial del ejemplo anterior.
Al clicar en el botón “Paso” se ejecutará un solo paso, tal y como indica su nombre. Si
queremos reproducir la partida al completo, podemos elegir la velocidad entre sus
pasos mediante la barra central y posteriormente pulsar en “Resolver”. Cuanto más a
la derecha desplacemos la barra más rápido se reproducirá. Si únicamente queremos
81
ver la solución obtenida debemos pulsar “Solución”. Además, a medida que se
reproduce la partida también veremos el Score actualizado.
Una vez seleccionado se abrirá una ventana nueva con el aspecto de la figura 36.
Como vemos en la figura 37, podremos seleccionar el algoritmo, la heurística, la
profundidad y el número de pruebas a realizar. Una vez seleccionadas las opciones
deseadas pulsamos “Iniciar pruebas”. Se realizarán tantas partidas como se haya
82
seleccionado, y se indicará si se ha ganado o perdido y el porcentaje de ganadas.
Cuando se concluyan todas, además aparecerá una media del tiempo de las partidas
en segundos.