análisis y diseño de algoritmos ii – 2009 trabajo … · parte de un algoritmo de búsqueda...

21
Análisis y Diseño de Algoritmos II – 2009 Trabajo Práctico Especial Se propone implementar una serie de funciones de evaluación heurística que serán utilizadas como parte de un algoritmo de búsqueda parcial que intentará resolver el juego “Pacman”. Introducción al juego El juego se presenta como un laberinto, que contiene paredes que constituyen obstáculos para el movimiento, puntos que es posible comer y fantasmas (enemigos) que persiguen al Pacman. El objetivo es comer la totalidad de los puntos del laberinto sin ser alcanzado por los fantasmas. Figura 1. Diagrama mostrando los elementos que forman parte del juego. La versión del juego que utilizaremos tiene las siguientes características: Se pierde cuando el Pacman es alcanzado por un fantasma. Se gana cuando el Pacman consigue comer todos los puntos del escenario. Los fantasmas y el Pacman se desplazan a la misma velocidad. El escenario contiene únicamente puntos comunes (a diferencia del juego original que contiene además píldoras de poder, que nos permiten comer a los fantasmas). Los posibles movimientos de todos los personajes son hacia casilleros contiguos; en las direcciones hacia arriba, abajo, derecha e izquierda. Únicamente el Pacman puede tomar la decisión de no moverse, siempre que esté bloqueado por una pared en la dirección en la que se estaba desplazando.

Upload: nguyendan

Post on 25-Sep-2018

216 views

Category:

Documents


0 download

TRANSCRIPT

Análisis y Diseño de Algoritmos II – 2009

Trabajo Práctico Especial

Se propone implementar una serie de funciones de evaluación heurística que serán utilizadas como parte de un algoritmo de búsqueda parcial que intentará resolver el juego “Pacman”.

Introducción al juego

El juego se presenta como un laberinto, que contiene paredes que constituyen obstáculos para el movimiento, puntos que es posible comer y fantasmas (enemigos) que persiguen al Pacman. El objetivo es comer la totalidad de los puntos del laberinto sin ser alcanzado por los fantasmas.

Figura 1. Diagrama mostrando los elementos que forman parte del juego.

La versión del juego que utilizaremos tiene las siguientes características:● Se pierde cuando el Pacman es alcanzado por un fantasma.● Se gana cuando el Pacman consigue comer todos los puntos del escenario.● Los fantasmas y el Pacman se desplazan a la misma velocidad.● El escenario contiene únicamente puntos comunes (a diferencia del juego original que

contiene además píldoras de poder, que nos permiten comer a los fantasmas).● Los posibles movimientos de todos los personajes son hacia casilleros contiguos; en las

direcciones hacia arriba, abajo, derecha e izquierda.○ Únicamente el Pacman puede tomar la decisión de no moverse, siempre que esté

bloqueado por una pared en la dirección en la que se estaba desplazando.

● Los fantasmas que comienzan dentro de la casa, salen de ella a intervalos determinados de tiempo.

● Los fantasmas alternan entre dos modos de juego distintos: persecución y asustadizo, a intervalos predefinidos de tiempo. Luego de varios intervalos, los fantasmas mantienen el modo persecución hasta el final del juego, como se ve a continuación.

Figura 2. Tiempo de duración (en segundos) de los modos asustadizo (verde) y persecución (rojo) de los fantasmas.

En cada modo de juego, los fantasmas intentan alcanzar un casillero objetivo. Se presenta una tabla que indica cómo se determina el casillero objetivo de acuerdo al modo de juego:

Fantasma \ Modo de juego Persecución Asustadizo

BlinkyEl casillero objetivo es la posición actual del Pacman. El casillero objetivo es

la esquina superior derecha.

PinkyEl casillero objetivo se encuentra a cuatro pasos de la posición actual del Pacman, en la dirección en la que se está desplazando.

El casillero objetivo es la esquina superior izquierda.

Inky

A partir de la posición actual del Pacman se obtiene el casillero a dos pasos de distancia, en la dirección en la que se está desplazando. Luego, se traza una línea desde la posición de Blinky hasta dicho casillero, y se extiende el doble de longitud en esa dirección. El casillero objetivo, es donde termina la línea.

El casillero objetivo es la esquina inferior derecha.

Clyde

Cuando se encuentra a más de ocho pasos de distancia, el casillero objetivo es la posición actual del Pacman. Si se encuentra a menos de ocho pasos, el casillero objetivo es la esquina inferior izquierda (modo asustadizo).

El casillero objetivo es la esquina inferior izquierda.

Figura 3. Lógica de movimiento de los fantasmas en modo persecución.

Figura 4. Lógica de movimiento de los fantasmas en modo asustadizo.

7 20 7 20 5 20 5 ...

Fundamentos teóricos

Se plantea el problema de desarrollar algoritmos que obtengan la sucesión de movimientos que debe realizar el Pacman para jugar de la mejor forma posible. Para facilitar el desarrollo se provee la una parte de la implementación de estos algoritmos la cual se basa en la estrategia de búsqueda parcial.A continuación describiremos los fundamentos teóricos para entender por qué la misma resulta adecuada para resolver este problema en particular.

La propuesta para resolver el problema es utilizar el concepto de grafo para modelar la dinámica del juego. De esta forma cada configuración o estado del laberinto se considerará un vértice. Luego, teniendo en cuenta que los movimientos de los enemigos son determinísticos, consideramos que sólo los movimientos válidos que puede realizar el Pacman, a partir de un estado, constituirán los arcos hacia nuevos estados. La configuración de los nuevos estados será la obtenida a partir de procesar el movimiento del Pacman y cada uno de los movimientos que realizan los fantasmas hasta que el Pacman pueda moverse otra vez. (Ver figura 5)

Una vez que conseguimos modelar con un grafo el desarrollo del juego, vemos que es posible aplicar los algoritmos clásicos de recorrido (DFS, BFS) adaptados a la búsqueda del estado objetivo (es decir una configuración del laberinto donde el Pacman comió todos los puntos). Esta adaptación lleva a lo que se conoce como algoritmos de búsqueda por fuerza bruta, ya que exploran sistemáticamente los estados, generando los mismos en el orden que define cada tipo de recorrido. Vemos entonces que la base de estas estrategias de búsqueda son los recorridos simples sobre grafos a los cuales se les agrega una condición para detenerse en el momento en que se alcanza el estado objetivo.

DFS(EstadoJuego E, Solución S):Si E es el estado objetivo

Devolver la solución con los pasos ejecutados hasta el momentoSino

Si E no fue visitado anteriormenteMarcar a E como visitadoPara cada movimiento válido, M, del Pacman en E

Generar el nuevo estado E' ejecutando M y procesando los turnos de los enemigosAgregar M a SDFS(E', S)Si se encontró solución

Terminar y devolver la soluciónSino

Volver el estado actual a E, y sacar a M de S

Pseudocódigo de ejemplo de la búsqueda en profundidad aplicada a la resolución del problema.

Existen algoritmos de búsqueda más avanzados que utilizan información adicional, específica al contexto del problema, para orientar la búsqueda de la solución. Este tipo de algoritmos se denomina de búsqueda heurística, ya que su objetivo es reducir el tiempo del proceso al orientar la exploración de los estados esperando evaluar una cantidad menor que las estrategias por fuerza bruta. El hecho de que se los llame heurísticos se debe a que en general no es posible garantizar una mejora para todos los posibles escenarios. Uno de los algoritmos de búsqueda heurística más populares es el A* (A estrella).

La complejidad temporal de los algoritmos anteriormente mencionados está determinada por el tamaño del espacio de búsqueda (que es el nombre que se le da al grafo o árbol utilizado para modelar el desarrollo del juego). El tamaño del espacio de búsqueda se encuentra en el orden de bn, donde:

• b: es el factor de ramificación (número de acciones o movimientos promedio que se pueden realizar a partir de cada estado);

• n: es la cantidad de pasos de la solución.

Fig

ura

5. E

stad

os g

ener

ados

a p

arti

r de

los

dist

into

s m

ovim

ient

os d

el P

acm

an.

Búsqueda parcial

Una forma de caracterizar a los algoritmos de búsqueda es en base a la forma en la que organizan las etapas de planificación de la solución y la ejecución. Bajo este criterio, los algoritmos como el DFS, BFS y el A* se denominan algoritmos de búsqueda completa porque calculan la solución en un único paso de planificación, y luego se realiza la ejecución de la misma.

Figura 6. Modo de operación de los algoritmos de búsqueda completa.

Una limitación de este tipo de estrategias surge cuando el espacio de búsqueda es demasiado grande, haciendo que el tiempo de la planificación se vuelva inviable.

El juego que estamos tratando es uno de los casos donde la complejidad del problema lleva a un espacio de búsqueda muy grande. Aunque no contamos con el factor de ramificación ni la longitud de la solución exactos, sabemos las cotas inferiores de cada uno. El factor de ramificación es al menos 2 y la longitud de la solución óptima no puede ser inferior a 244 (el número de puntos), por lo que el espacio de búsqueda se encuentra en el orden de los 2244 estados.Es claro que los algoritmos de búsqueda exhaustiva no son una opción viable para la resolución de este problema, y si bien los algoritmos de búsqueda completa heurísticos podrían reducir considerablemente la cantidad de planificación, esto dependerá exclusivamente de la calidad de la función de evaluación heurística utilizada. Sin embargo, el trabajo para construir una función de evaluación, de la calidad requerida, puede ser muy dificultoso o impracticable.

Los algoritmos de búsqueda parcial alternan etapas de planificación (de tiempo acotado) y etapas de ejecución de lo que parecen ser los mejores pasos (basándose en la información limitada con la que se cuenta) para llegar a una solución.

Figura 7. Modo de operación de los algoritmos de búsqueda parcial.

Este es el tipo de algoritmos de búsqueda que se utilizará para la resolución de este trabajo. Surgen consecuencias importantes a partir de esta decisión:

• Dado que existen acciones que no se pueden revertir (por ejemplo aquellas que hacen que los fantasmas encierren al Pacman o lo coman) no siempre se hallará una solución.

• De encontrarse una solución, en general, la misma no será óptima.

Durante la etapa de planificación se determinará cuál de las acciones, que es posible realizar a continuación, podría llevar con probabilidad más alta a una resolución satisfactoria del problema. Es fundamental restringir la profundidad de búsqueda para poder responder dentro de los límites de tiempo impuestos. Si bien el tiempo de ejecución de los algoritmos de búsqueda es exponencial respecto a la frontera de búsqueda, la misma está acotada por una constante y por lo tanto también lo está el tiempo de ejecución. Dado que la profundidad de búsqueda es limitada, es necesario contar con un método para evaluar los estados no-objetivo. Es esencial para el funcionamiento de esta estrategia la utilización de heurísticas que permitan estimar el costo de alcanzar un objetivo desde un estado intermedio.

Planificación Ejecución

Planificación ...Ejecución Planificación Ejecución Planificación Ejecución

Uno de los principios más importantes de la búsqueda parcial es la utilización de la estrategia del menor compromiso para los movimientos. Es decir, la información obtenida en cada etapa de planificación sólo se utilizará para determinar el próximo movimiento. La razón es que luego de ejecutar la acción, se supone que la frontera de búsqueda se expandirá, lo cual puede llevar a una elección para el segundo movimiento diferente a la que arrojó la primera búsqueda.

Sin embargo, para realizar una secuencia de decisiones no es suficiente con la información devuelta por el algoritmo de planificación. La estrategia básica de repetir el algoritmo de planificación para cada decisión resulta inadecuada al ignorar la información relacionada con los estados anteriores. El problema radica en que, al volver a un estado previamente visitado, se entrará en un ciclo infinito. Esto es algo que sucederá con frecuencia, debido a que las decisiones se basan en información limitada y por lo tanto direcciones que al principio parecían favorables pueden resultar equivocadas al reunir más información durante la exploración. En general, se desea evitar entrar en ciclos infinitos y a la vez permitir volver a estados ya visitados cuando parezca favorable.

Funciones de evaluación heurística

Las funciones de evaluación heurística (o simplemente heurísticas) permiten estimar el costo del camino óptimo entre dos estados. Esta definición es la utilizada en el contexto de problemas de búsqueda desde la perspectiva de un único jugador (o agente). Para juegos de dos jugadores también se utilizan heurísticas, pero las mismas son estimaciones de la utilidad de los estados teniendo en cuenta la perspectiva de ambos jugadores.En general, los estados evaluados serán un estado no terminal y el estado objetivo. En este tipo de juegos, donde las condiciones que determinan los estados objetivos permanecen invariables, el parámetro principal de la evaluación es el estado no terminal. La distancia estimada es la correspondiente a una configuración donde se alcanza el objetivo a través del cumplimiento de dichas condiciones.

Se llama estática a una función heurística que tiene como parámetro sólo la configuración de un estado. Dichas funciones tienen en cuenta ciertas variables para calcular la estimación de la distancia. Frecuentemente la función heurística es una combinación lineal de las variables consideradas, que se pesan de algún modo para obtener un valor en término de las unidades elegidas.Se puede extender a la función evaluación estática a través de una búsqueda limitada por el tiempo (o profundidad), en la cual se aplica a los estados de la frontera la evaluación estática sumándole el costo real que implica llegar a cada uno de ellos desde la raíz. De esta forma se obtiene una evaluación más precisa que una heurística estática ya que reduce la incertidumbre.A la hora de diseñar una función heurística se debe hacer un balance entre:

• el costo computacional de cada evaluación;• la calidad de la estimación retornada.

Objetivo del trabajo

El objetivo del trabajo es desarrollar funciones de evaluación heurística (estáticas) para que las utilice un algoritmo de búsqueda parcial que ya se encuentra disponible junto con el resto del juego descripto en la introducción. A su vez, se requiere un análisis de los resultados obtenidos al utilizarlas con distintos niveles de profundidad para la búsqueda realizada durante la planificación de cada acción.

El resto de las secciones explicarán las características de la aplicación, así como el soporte para el desarrollo de las funciones que se deben implementar. También se describirá en detalle el algoritmo de búsqueda utilizado. Por último, se presentarán los requisitos de entrega del trabajo.

Descripción de la aplicación

Al ejecutar la aplicación se nos presentará la siguiente pantalla, en la cual se detallan los modos de juego iniciales que podemos elegir.

Figura 8. Pantalla inicial del juego.

Modo de juego: Manual

En este modo de juego se podrá jugar controlando al Pacman mediante los siguientes controles: – Mover hacia arriba: flecha arriba– Mover hacia abajo: flecha abajo– Mover hacia la derecha: flecha derecha– Mover hacia la izquierda: flecha izquierda

Para obtener una experiencia de juego similar al juego original, el Pacman continuará ejecutando la última acción indicada, siempre que sea posible, es decir, mientras que un obstáculo no se interponga en su camino o se le indique que realice una nueva acción.Además, en este modo de juego, el Pacman comenzará su recorrido realizando un movimiento hacía la izquierda.

Modos de simulación

Al seleccionar cualquier de los dos modos de juego de simulación, que se explicarán a continuación, se presentará en primera instancia una pantalla que permitirá seleccionar la función de evaluación heurística a utilizar. Luego se presentará una pantalla que permitirá seleccionar la profundidad de la búsqueda.

Figura 9. Menú de selección de heurísticas. Figura 10. Menú de selecciónde profundidad de la búsqueda..

Modo de juego: Simulación completa

En este modo de juego se realizá una simulación completa del juego, intercalando las etapas de planificación y ejecución, como se explicó anteriormente, hasta que se termina el juego. Una vez terminada la simulación, el juego reproducirá las acciones obtenidas en las etapas de planificación permitiendo ver los movimientos elegidos. Además, se generará un archivo de texto con las acciones que formaron parte de la solución. El nombre del archivo se indicará por consola, como se ve en la pantalla siguiente.

Figura 11. Generación del archivo con la solución generada.

Modo de juego: Simulación interactiva

Este modo de juego es similar al anterior, solo que después de cada etapa de planificación se visualizará por pantalla la ejecución de la acción obtenida, junto con la consecuencia de realizar dicha acción.Como lo indica su nombre, este modo nos permite ver de forma interactiva como se realiza cada paso de planificación y su posterior ejecución.Al igual que el modo anterior, la solución encontrada se almacenará en un archivo de texto generado automáticamente.

Modo de juego: Reproducir la última solución

Al finalizar la ejecución de la solución en los modos de simulación completa o interactiva se presentará una nueva opción en el menú de selección de modo de juego. Ésta nueva opción permitirá volver a reproducir la última solución encontrada.

Figura 12. Pantalla inicial del juego con opción de reproducir la última solución.

Modo de juego: Reproducir archivo de solución

Por último, se permite otro modo de juego, en el cual se puede reproducir una solución previamente almacenada en un archivo de texto. Este archivo de texto debe contener una acción por línea, respetando el formato de los archivos de texto generados automáticamente por la ejecución de los modos de juego de simulación.

Para utilizar este modo de juego se debe indicar por parámetro el nombre del archivo de texto que contiene la solución. Si el archivo pudo ser cargado correctamente se presentará la siguiente opción.

Figura 13. Pantalla inicial del juego con opción de reproducir a partir de archivo de solución.

Comandos generales

En todos los modos de juego se permite realizar un pausado y posterior reanudación del juego; para esto se utiliza la barra espaciadora.También se permite interrumpir cualquier ejecución del juego y retornar al menú selección del modo de juego, desde cualquier pantalla, mediante la tecla escape.

Salida por consola

Durante el desarrollo del proyecto se podrá utilizar la consola para visualizar mensajes de control. Estos mensajes se visualizarán en una consola asociada a la ventana principal de la aplicación como se ve en la imagen siguiente.

Figura 14. Consola para visualizar los mensajes de control.

Además, se podrá guardar la salida de la aplicación en un archivo de texto, redireccionando la salida. Por ejemplo:

C:\pacman\pacman.exe > salida.log

Importante: para poder ejecutar la aplicación desde una consola se debe copiar el último ejecutable creado desde el proyecto Code::Blocks (ubicado generalmente en bin\Debug), dentro del directorio raíz del proyecto. Esto se debe a que el ejecutable necesita la carpeta data (y los archivos dll).

Soporte provisto para el desarrollo de los algoritmos

Para la implementación de los algoritmos se dispone de un proyecto que consiste en la implementación completa del juego Pacman. El mismo incluye la lógica del juego (reglas, configuración y comportamiento de cada uno de los personajes), así como una interfaz de usuario que permite controlar al Pacman manualmente y también, a través de las soluciones obtenidas por los distintos algoritmos que se van a implementar.

Los tipos de datos principales que se proveen para la realización de los algoritmos son:● LogicObject: representa todos los jugadores y objetos del juego: el Pacman, los enemigos,

las paredes y los puntos.● GameSpace: permite acceder a los objetos del juego a través de sus coordenadas en el

laberinto.● GameState: guarda una configuración determinada del juego. Es decir es el estado en el que

se encuentran cada uno de los objetos del juego en un momento dado.● GameSimulator: Permite gestionar los estados por los que atraviesa el juego. En particular:

○ avanzar el juego hasta que nos toque el turno nuevamente;○ deshacer los cambios en el juego luego de un movimiento;○ llevar la simulación a un estado determinado que hayamos generado anteriormente.

La descripción completa de cada uno de los métodos que conforman estos tipos de datos principales se encuentra en la documentación HTML provista.

Constantes del juego

Las constantes utilizadas en el juego se encuentran en el archivo Pacman.h.

/* * Variables globales del juego */const unsigned int DOT_COUNT = 1001;const unsigned int DESTROYED_DOTS = 1002;const unsigned int SCATTERED_MODE = 1003;

/* * Elementos del juego. */const std::string PACMAN = "Pacman";const std::string DOT = "Dot";const std::string BLINKY = "Blinky";const std::string PINKY = "Pinky";const std::string INKY = "Inky";const std::string CLYDE = "Clyde";const std::string GHOST = "Ghost"; //Subtipo utilizado para identificar a los fantasmas.const std::string WALL = "Wall"; //Subtipo utilizado para identificar a las paredes que componen el laberinto.

/* * Atributos de los personajes del juego. */const unsigned int DIRECTION = 1000;const unsigned int INSIDE_HOUSE = 1001;

// Posibles direcciones de los personajes del juego.typedef enum {UP, DOWN, LEFT, RIGHT} Direction;

Acceso a los objetos lógicos del juego

Hay dos formas de acceder a los objetos lógicos del juego:• a través de su tipo o subtipo utilizando el método getObjects (en el caso de ser un único

objeto se puede utilizar getObject directamente) del juego (cuyo tipo es Game).• a través de sus coordenadas o posición, opcionalmente filtrando por el tipo o subtipo,

utilizando el método getObjects del espacio de juego (que es una instancia de la clase GameSpace). El espacio del juego es accesible desde el juego.

Los tipos y subtipos de los objetos lógicos están definidos en las constantes del juego.

Ejemplos

• Obtener el Pacman:

const LogicObject * pacman = game->getObject(PACMAN);

• Obtener todos los fantasmas del juego:

//Se define un conjunto de subtipos y se le agrega el subtipo GHOST.set<string> ghostSubtype;ghostSubtype.insert(GHOST);

list<const LogicObject *> ghosts;game->getObjects(LogicObject::Type(ghostSubtype), ghosts);

• Obtener los elementos del juego que están dentro de una región:

list<const LogicObject *> objects;game->getGameSpace().getObjects(Rectangle(1,1,5,4), objects);

• Obtener sólo los fantasmas que están dentro de una región:

//Se define un conjunto de subtipos y se le agrega el subtipo GHOST.set<string> ghostSubtype;ghostSubtype.insert(GHOST);

list<const LogicObject *> objects;game->getGameSpace().getObjects(Rectangle(1,1,10,10),

LogicObject::Type(ghostSubtype), objects);

Variables globales del juego

El juego mantiene ciertas características como variables globales. Las mismas se encuentran almacenadas en una estructura de variables, accesible desde el juego. Los identificadores de estas variables están definidos en las constantes del juego.

• Obtener los puntos restantes del juego:

game->getVariables().getInteger(DOT_COUNT);

• Obtener el modo en el que se encuentran los fantasmas:

game->getVariables().getBoolean(SCATTERED_MODE);

Atributos de los personajes del juego

Los objetos lógicos, al igual que el juego, mantienen sus características particulares como variables. En el caso de los objetos lógicos estas variables se denominan atributos. Los atributos están identificados por constantes del juego y pueden ser de cualquier tipo.

• Obtener la dirección del fantasma Pinky:

const LogicObject * pinky = game->getObject(PINKY);

Direction direction;pinky->getAttribute(DIRECTION, direction);

Otra forma equivalente es acceder a los atributos del juego y luego a la dirección mediante el método get del tipo de dato Variables:

const LogicObject * pinky = game->getObject(PINKY);

Direction direction;pinky->getAttributes().get(DIRECTION, direction);

• Determinar si Pinky está dentro de la casa o no:

const LogicObject * pinky = game->getObject(PINKY);

bool insideHouse;pinky->getAttribute(INSIDE_HOUSE, insideHouse);

Otra forma equivalente es acceder a los atributos del juego y luego al valor buscado mediante el método getBoolean del tipo de dato Variables.Este tipo de acceso es útil cuando el atributo a obtener es de un tipo básico conocido.

const LogicObject * pinky = game->getObject(PINKY);pinky->getAttributes().getBoolean(INSIDE_HOUSE)

Implementación del algoritmo de búsqueda parcial

A continuación se presenta un pseudocódigo del proceso de búsqueda, de forma simplificada:

Mientras no termine el juego:– Realizar la etapa de planificación para obtener la próxima acción a

ejecutar.– Procesar la acción obtenida, actualizando el estado del juego hasta que

se pueda realizar otra acción asegurándose de que no se produzcan ciclos.

– Almacenar la acción como parte de la solución.

En el modo de juego de simulación interactiva, además de ejecutar la acción y almacenarla como parte de la solución, se actualizará la visualización del juego al mismo tiempo que se ejecuta la búsqueda. En este modo, se reemplaza el control manual por un control automático que ejecuta un ciclo del algoritmo para determinar cada paso. De esta forma se puede ver el tiempo de respuesta del algoritmo para tomar una decisión y su impacto en la fluidez del juego.Esto contrasta con el modo de simulación completa que guarda la solución después de llevar a cabo el proceso de búsqueda anterior. Luego se muestra la solución, iniciando el juego con la visualización y un control automático para el Pacman que ejecuta las acciones directamente, evitando la planificación (igual a lo que sucede en el modo de reproducción de la solución desde archivo).

El proceso de búsqueda parcial descripto anteriormente será implementado a través del A* de Tiempo Real (Real Time A* o RTA*). Este algoritmo de control es el encargado de evaluar las acciones mediante funciones de evaluación heurística, lo cual se denomina etapa de planificación, como ya se mencionó. De esta forma decide que acción se llevará a cabo a continuación (en la etapa de ejecución). Adicionalmente el algoritmo implementa una estrategia que evita que se caiga en ciclos infinitos permitiendo volver a estados ya visitados sólo cuando es conveniente.

Durante la etapa de planificación se suelen utilizar funciones de evaluación de estados extendidas mediante búsqueda (cuyo tiempo será acotado a través de un límite en la profundidad). Existen varias modificaciones a los algoritmos de búsqueda heurística completa para poder utilizarlos como funciones de evaluación. De todos ellos se optó por el Minimin que realiza un proceso similar al del algoritmo Minimax modificado para la evaluación desde el punto de vista de un único jugador.Hay dos parámetros que afectan el comportamiento de este algoritmo, la profundidad, que determina la frontera de búsqueda, y la función de evaluación heurística estática, utilizada para evaluar los estados en la misma. Como ya se mencionó, es posible configurar estos parámetros de forma dinámica ya que se presentan como opciones antes del inicio de la simulación.

La característica más importante a destacar, de la implementación, es que ambos algoritmos son métodos de una clase llamada PacmanRTAPathfinder (cuyo código se encuentra en los archivos PacmanRTAPathfinder.h y PacmanRTAPathfinder.cpp). Los mismos utilizan el soporte de simulación provisto por la clase GameSimulator, la cual provee las funciones necesarias para manipular los estados del juego al nivel de abstracción requerido para que la implementación de los algoritmos sea sencilla.

A continuación se presentará la implementación de ambos algoritmos, junto con una breve descripción del funcionamiento y características principales.

Real-Time A* (RTA*)

RTA* es un algoritmo para controlar la fase de ejecución de la búsqueda parcial y es independiente del algoritmo de planificación. El algoritmo guarda el valor de cada estado visitado durante la etapa de ejecución en una tabla de hash. A medida que la búsqueda avanza se actualizan estos valores utilizando técnicas derivadas de la programación dinámica.

La propiedad más importante de RTA* es que, en espacios finitos y si el estado objetivo se puede alcanzar, se garantiza que se encontrará una solución. Además, el espacio requerido es lineal respecto al número de movimientos realizados, ya que sólo lleva una lista de los estados previamente visitados. El tiempo de ejecución también es lineal respecto a los movimientos ejecutados. Esto se debe a que, aunque el tiempo de planificación es exponencial respecto a la profundidad de búsqueda, el tiempo está acotado por una constante al limitar la profundidad de búsqueda.

El proceso de búsqueda que realiza el algoritmo es el siguiente:• A los estados que no fueron visitados se les aplica la función de evaluación heurística,

posiblemente extendida mediante una búsqueda.• Para los estados que se encuentran en la tabla se utiliza el valor heurístico f guardado en ella.• El vecino con el menor valor f es elegido para ser el nuevo estado actual y la acción para

alcanzar ese estado es ejecutada.• El estado anterior se guarda en la tabla y se le asocia el segundo mejor valor f de los vecinos

más el costo para regresar al mismo desde la nueva posición.

Los primeros dos puntos se corresponden a la etapa de planificación mientras que los últimos dos están relacionados con la etapa de ejecución de la búsqueda parcial.

La implementación del RTA* se encuentra repartida en los métodos processCycle y getAction de la clase PacmanRTAPathfinder. El método processCycle coordina el proceso de búsqueda parcial. El mismo invoca a la función getAction, que lleva a cabo la planificación, y luego realiza los pasos necesarios para la ejecución.

El método processCycle

Consiste en una iteración del ciclo de búsqueda, es decir, una etapa de planificación seguida de la ejecución de una acción. Para determinar una solución completa sólo se requieren invocaciones sucesivas hasta que se termine el juego:

void getSolution(PacmanPathfinder * pathfinder, list<Action::Type> & solution){

while (!pathfinder->isGameOver()) {solution.push_back(pathfinder->processCycle());

}}

Como ya se dijo, primero se realiza la planificación a través de la función getAction. Así se determina la siguiente acción a realizar (bestAction). Además de la acción se obtienen la evaluación heurística del estado al que se pasará al realizar la misma (bestF), así como el valor heurístico de la segunda mejor opción (secondBestF).

Algunas aclaraciones relevantes a esta parte del algoritmo:• Cuando la profundidad de búsqueda es lo suficientemente grande, es posible encontrar que,

independientemente de lo que se haga, se va a perder el juego. Esto puede ocurrir aún cuando falta realizar varias acciones para perder en forma efectiva. El problema es que la planificación, al no considerar que es deseable prolongar el juego, devuelve la primer acción evaluada y como consecuencia es frecuente que se pierda antes de tiempo.La solución implementada consiste en detectar esta situación (bestF >= LOSE_VALUE) y volver a realizar una planificación limitando la profundidad de búsqueda a un nivel. De este modo, se reemplazará el primer resultado obtenido por una acción que permita seguir jugando (si es que existe).

• Otra verificación necesaria concierne al valor heurístico del estado vecino asociado a la segunda mejor acción encontrada. Hay casos en los que hay una sola opción viable y el resto (incluyendo la segunda mejor) lleva a perder el juego. Como el valor que se asocia al estado actual es el segundo mejor f encontrado, si se guarda el mismo cuando ocurre esta situación se estará guardando una evaluación incorrecta del mismo (ya que volver al mismo nos llevará a perder).Para no incurrir en este error, se agregó al algoritmo una verificación que detecta el problema (secondBestF >= LOSE_VALUE) y corrige el valor de la segunda mejor evaluación heurística de los estados vecinos, reemplazándolo por el mejor.

Para llevar acabo la simulación de la ejecución de las acciones en el juego, registrar los estados visitados y asociarles los valores heurísticos que requiere el RTA*, se utiliza un simulador principal llamado controlSim (instancia de la clase GameSimulator). Éste es el simulador principal del proceso (más adelante se verá que durante la planificación se utiliza un segundo simulador). El mismo es un atributo de la clase PacmanRTAPathfinder, porque se debe mantener su estado entre las diferentes invocaciones al método.

Antes de ejecutar la acción que llevará a un cambio de estado se debe guardar el valor heurístico que reflejará el costo de volver al estado actual (controlSim.setStateValue(..., secondBestF + 1)).

Por último, se realiza la acción (controlSim.getPlayer()->performAction(...)) y se actualiza el juego para llevarlo al próximo estado donde es posible tomar una decisión (controlSim.update()).

Action::Type PacmanRTAPathfinder::processCycle(){

unsigned int bestF, secondBestF;

Action::Type bestAction = getAction(maxDepth, bestF, secondBestF);

if (bestF >= LOSE_VALUE) {bestAction = getAction(1, bestF, secondBestF);

}

if (secondBestF >= LOSE_VALUE) {secondBestF = bestF;

}

const GameState * controlState = controlSim.getCurrentState();

unsigned int visitedId = controlSim.getVisited(controlState);if (visitedId == 0) {

controlSim.addVisited(controlState);controlSim.setStateValue(controlState->getId(), secondBestF + 1);

} else {controlSim.setStateValue(visitedId, secondBestF + 1);

}

controlSim.getPlayer()->performAction(bestAction);controlSim.update();

return bestAction;}

El método getAction

Es la parte del algoritmo donde se determina la siguiente acción a ejecutar. La misma utiliza la función de evaluación heurística extendida mediante búsqueda para obtener la acción que llevará al Pacman a un estado más cercano a ganar el juego.El proceso de planificación requiere de la simulación del juego al igual que la ejecución, como ya se explicó antes. Si bien se desean mantener los estados visitados entre las búsquedas realizadas por la función de evaluación, los mismos no se deben mezclar con los estados visitados durante el proceso de ejecución. Es por eso que para la planificación se utiliza un simulador llamado planSim, que se sincroniza al comienzo de esta etapa con el simulador principal controlSim.

La planificación consiste en realizar la evaluación de los estados vecinos al estado actual.Para generar estos estados, primero se obtienen las acciones del Pacman (player->getActions(...)). Luego, para cada acción se verifica si se puede realizarla en el estado actual del juego(player->canPerformAction(..)); en caso afirmativo ejecuta la misma (player->performAction(..)) y actualiza el estado del juego (planSim.update()). Una vez realizada la evaluación de una configuración del juego es necesario volver al estado previo (planSim.undoUpdate()) para ejecutar desde allí la siguiente acción.

Para la evaluación de los estados se tiene en cuenta si los mismos ya fueron visitados o no durante el proceso de ejecución (realizado por processCycle que guarda esta información en el controlSim), ya que el algoritmo RTA* requiere un tratamiento particular para cada caso:

• Si el estado no fue visitado (controlSim.getVisited(...) == 0) se utiliza la función de evaluación heurística extendida mediante búsqueda (minimin).

• Si el estado ya fue visitado (controlSim.getVisited(...) != 0) se recupera el valor heurístico asociado al estado (controlSim.getStateValue(...)).De esta forma, es que el RTA* toma en consideración los ciclos y permite retornar a los estados visitados en forma controlada.

Action::Type PacmanRTAPathfinder::getAction(unsigned int depth, unsigned int & bestF, unsigned int & secondBestF)

{GameSimulator planSim;planSim.setGame(controlSim.getGame());planSim.setPlayer(controlSim.getPlayer());planSim.setSimulatorController(controlSim.getSimulatorController());

LogicObject * player = planSim.getPlayer();list<Action::Type> playerActions;player->getActions(playerActions);

bestF = MAX_F;secondBestF = MAX_F;Action::Type bestAction = player->getDefaultActionType();

list<Action::Type>::iterator action = playerActions.begin();while (action != playerActions.end()) {

if (player->canPerform(*action)) {player->performAction(*action);planSim.update();

unsigned int f = MAX_F;

unsigned int visitedId = controlSim.getVisited(planSim.getCurrentState());

if (visitedId == 0) {f = minimin(planSim, depth, 1);

} else {f = controlSim.getStateValue(visitedId);

}

if (f < bestF) {secondBestF = bestF;bestF = f;bestAction = *action;

} else if (f < secondBestF) {secondBestF = f;

}

planSim.undoUpdate();}action++;

}

return bestAction;}

Minimin

El algoritmo Minimin realiza una búsqueda en profundidad asignando un valor heurístico a cada estado encontrado en la frontera de búsqueda, determinada por el límite en la profundidad. Estos valores se suben hasta la raíz de la búsqueda (es decir, al estado inicial evaluado). Para ello, cada nodo intermedio determina su valor retornando el valor del menor de sus hijos. Al final de la búsqueda, el valor de la raíz será el correspondiente a la menor de las evaluaciones de los nodos de la frontera.El valor heurístico asignado a los estados de la frontera debe ser una estimación de la distancia que existe a un estado donde se gana el juego, partiendo del estado raíz y pasando por el estado evaluado (f). Para reflejar correctamente este valor, a la evaluación heurística estática de los estados (h) de la frontera de la búsqueda se les debe sumar el costo real del camino que se recorrió para llegar a ellos desde la raíz (g).

La función recursiva correspondiente al algoritmo es el método minimin de la clase PacmanRTAPathfinder. El mismo toma como parámetro principal, al estado del juego a evaluar gestionado por un simulador (planSim). Adicionalmente se pasan la profundidad de búsqueda máxima que es posible alcanzar (depth), así como el nivel actual o costo del camino acumulado (g).

Como se ve en el código que sigue, hay dos condiciones de corte indispensables, las cuales son:• El estado evaluado corresponde a un final del juego (planSim.isGameOver()).

En este caso se puede determinar directamente el valor real de dicho estado. Si se ganó el juego, el valor a devolver es la cantidad de pasos acumulados desde la raíz dada por g. Por el contrario, si se perdió se sabe que a través de dicho estado nunca se alcanzará una resolución satisfactoria y se retorna un valor muy grande dado por la constante LOSE_VALUE.

• Se alcanzó el límite de la profundidad permitida (g >= depth).Como ya se dijo, este es el caso general donde se determina el valor heurístico f = h + g de los estados de la frontera de búsqueda. Aquí es donde se utiliza la función de evaluación heurística estática que estima la distancia a un estado objetivo (h) y se le suma el valor del nivel actual (g).

Luego de la verificación de las condiciones de corte, se realizan dos controles de ciclos:• Ciclos en el camino actual de la búsqueda (planSim.isInPath(currentState)).

En principio no sería necesaria debido a que en algún momento se terminará la búsqueda debido al límite en la profundidad (y en el caso del juego considerando la profundidad en la que se dan dichos ciclos no es práctica). De todos modos, se agrega por completitud.En este caso se corta la búsqueda y se evita que la evaluación de dicho estado entre en consideración al devolver un valor muy grande dado por la constante MAX_F.

• Ciclos a estados ya expandidos (planSim.getVisited(currentState) != 0).Cuando las profundidades de búsqueda son lo suficientemente grandes o bajo ciertas condiciones (como cuando los fantasmas están asustados) es frecuente llegar a un mismo estado a través de secuencias de acciones (o caminos de búsqueda) diferentes. Esto se evidencia más aún si no se realiza la poda de acciones que se explica más adelante.Aquí se tomó la decisión de hacer un compromiso entre la memoria utilizada y el costo de procesar partes del espacio de búsqueda ya exploradas. Es así que se incorporó una tabla de transposiciones (transposition table) donde se almacenan los valores heurísticos (planSim.setStateValue(.., minF - g)) de los estados ya expandidos por la búsqueda. Cuando se determina que el estado actual ya fue visitado por otro camino, se usa el valor guardado previamente (planSim.getStateValue(..) + g) como una aproximación a la evaluación heurística (mejor que h + g, pero menos precisa que f) que se obtendría al explorar nuevamente dicho estado.

El resto del proceso consiste en expandir el estado actual, a partir de la generación y evaluación (recursiva) de los estados que pueden alcanzarse al ejecutar las acciones (player->perfomAction(...)) y actualizar el juego utilizando el simulador (planSim.update()). Como puede observarse, durante el ciclo de evaluación de los estados hijo se va seleccionando el menor; de esta forma, al finalizar la expansión se obtendrá el mínimo, que es el valor heurístico asociado al estado.

Por último, es importante mencionar que se realiza una poda en las acciones (implementada en el método pruneActions), para reducir el espacio de búsqueda. Las acciones que se eliminan (sólo para la búsqueda de la planificación) son la acción que permitiría retroceder (esto depende la dirección del Pacman en cada estado) y la posibilidad de detenerse (en las intersecciones del laberinto).Con la poda de acciones se favorece una exploración más rápida, en detrimento de la precisión de las evaluaciones obtenidas (debido a que con menos combinaciones de acciones se están generando muchos menos estados en la frontera de búsqueda). Sin embargo, una consecuencia de la poda es que, en el mismo tiempo, se puede llegar a profundidades mayores por lo que, en general, se está permitiendo obtener evaluaciones mejores (en este juego en particular, es preferible anticipar una secuencia de pasos más larga que todas las posibles combinaciones de caminos).

unsigned int PacmanRTAPathfinder::minimin(GameSimulator & planSim, unsigned int depth, unsigned int g)

{if (planSim.isGameOver()) {

if (planSim.getGameOverResult() == GameSimulator::WIN) {return g;

} else {return LOSE_VALUE;

}} else if (g >= depth) {

return (*h)(planSim.getGame()) + g;} else {

const GameState * currentState = planSim.getCurrentState();

if (planSim.isInPath(currentState)) {return MAX_F;

} else {unsigned int visitedId = planSim.getVisited(currentState);

if (visitedId != 0) {return planSim.getStateValue(visitedId) + g;

} else {LogicObject * player = planSim.getPlayer();list<Action::Type> playerActions;player->getActions(playerActions);

pruneActions(player, playerActions);

unsigned int minF = MAX_F;

list<Action::Type>::iterator action = playerActions.begin();while (action != playerActions.end()) {

if (player->canPerform(*action)) {player->performAction(*action);planSim.update();

unsigned int f = minimin(planSim, depth, g + 1);

if (f < minF) {minF = f;

}

planSim.undoUpdate();}action++;

}

planSim.addVisited(currentState);planSim.setStateValue(currentState->getId(), minF - g);

return minF;}

}}

}

Implementación de los algoritmos para las funciones de evaluación heurística

Cada uno de los algoritmos deberá ser implementado cumpliendo la siguiente interfaz:

unsigned int h(const Game * game)

Parámetros:• game: permite acceder a los objetos lógicos, el espacio del juego y las variables globales

necesarias para analizar la configuración del estado actual del juego.

En los archivos PacmanHeuristics.h y PacmanHeuristics.cpp, se encuentran definidas las funciones de evaluación cuya implementación por defecto deberá reemplazarse con los algoritmos propios.

unsigned int h1(const Game * game){

return game->getVariables().getInteger(DOT_COUNT);}

unsigned int h2(const Game * game){

...}

...

Para asociar cada opción del menú de selección de heurística con la función que implementará dicha heurística, se utiliza un vector de pares (configurado en el archivo main.cpp) el cual se pasa como parámetro a la función runPacman encargada de iniciar la aplicación. Cada par contendrá un puntero a una función y un nombre a visualizar en la pantalla (que también se utilizará para generar el nombre del archivo de la solución).De esta forma se puede construir dinámicamente el menú de selección de heurísticas de acuerdo al número de funciones implementadas.

vector<pair<HeuristicEvaluation, string> > heuristics;

heuristics.push_back(make_pair(&h1, "Heuristica1"));heuristics.push_back(make_pair(&h2, "Heuristica2"));heuristics.push_back(make_pair(&h3, "Heuristica3"));heuristics.push_back(make_pair(&h4, "Heuristica4"));heuristics.push_back(make_pair(&h5, "Heuristica5"));heuristics.push_back(make_pair(&h6, "Heuristica6"));

Para que esto funcione correctamente se realiza una definición de tipos, indicando que una función de tipo HeuristicEvaluation será aquella que cumpla con la interfaz de las funciones h descripta. Es decir, deberá retorna un valor unsigned int y recibir por parámetro una referencia constante a un objeto de la clase Game.

typedef unsigned int (*HeuristicEvaluation)(const Game * game);

Requisitos de la entrega

Fecha y lugar de entrega: Martes 1° de diciembre de 2009, en el Laboratorio 1 del Pabellón de Cs. Exactas de 13hs a 16hs.Fecha y lugar de la defensa: A confirmar. Se publicará en el página de la cátedra en una fecha posterior a la entrega del trabajo.

Importante: Si bien no se va a realizar una primera entrega, se recomienda que dos semanas después del comienzo del trabajo (semana del 16 y 18 de noviembre) se entregue (o discuta con el ayudante asignado) un borrador que detalle, por lo menos, de que forma van a cumplir con cada uno de los puntos requeridos en el informe.

Implementación

Se deberá entregar un proyecto que compile correctamente el código de los algoritmos y permita generar la aplicación para la prueba de los mismos. Así mismo, se solicita la entrega de un ejecutable de ese proyecto.Se deberán implementar como mínimo seis funciones de evaluación heurística distintas.Se considerarán distintas a dos funciones de evaluación que utilicen las mismas variables pero con cambios significativos en el peso, factor o relación entre dichas variables.

Informe

Se deberá entregar un informe impreso que abarque los siguientes contenidos:● Resumen del trabajo realizado, comentando brevemente el contexto del problema, el

objetivo del trabajo y principalmente las tareas que involucró el desarrollo del mismo.● Implementación

○ Código y explicación breve de cada función de evaluación heurística y del algoritmo Minimin.

○ Explicación de cualquier decisión de implementación que considere relevante.● Explicación detallada de las heurísticas

○ Explicar las hipótesis bajo las cuales se basaron las heurísticas implementadas.○ Explicar los pasos y pruebas que determinaron la forma específica de cada función de

evaluación (variables, pesos o factores, combinación de las mismas, etc).● Análisis y presentación de resultados

○ Cada función de evaluación deberá ser probada con, al menos, tres valores distintos de profundidad que se consideren significativos (se deberá justificar la elección).

○ Realizar un análisis empírico de las funciones de evaluaciones, considerando métricas como tiempo de planificación, espacio de búsqueda total, calidad de la solución y cualquier otro parámetro que considere relevante.

○ Presentar de la forma más clara posible (gráficos, tablas, etc) los resultados generales y particulares de cada prueba realizada.

○ Realizar una comparación e interpretación de los resultados presentados, incluyendo una conclusión en base a las hipótesis en las cuales se basaron las evaluaciones heurísticas.

○ Incluir una explicación del comportamiento del Pacman de acuerdo a cada función de evaluación utilizada. Mencionar si este comportamiento justifica o no la hipótesis de la evaluación heurística.

● Conclusión○ Conclusiones extraídas del trabajo.○ Opinión particular y grupal del trabajo práctico especial (metodología de trabajo, tema,

contenidos, etc).