RYG: jugadas del ratón – validación de jugadas

Las condiciones para que una jugada del ratón sea válida son tres:

  • Que respete las reglas de movimiento, es decir, que sea en diagonal, hacia delante o hacia atrás, y moviendo una única casilla.
  • Que la casilla de destino esté dentro de los márgenes del tablero.
  • Que la casilla de destino esté libre, puesto que no hay capturas.

La primera condición se cumple siempre, puesto que la forma de generar los movimientos mediante dos tablas lo garantiza.

Las condiciones dos y tres de momento no están garantizadas. Hay que comprobarlas. Lo bueno es que son las mismas condiciones que necesitan los gatos, de modo que con una única rutina de validación podemos validar los movimientos de ratón y gatos.

La rutina de validación se llama “validaJugada”, y es una nueva rutina del fichero “GenJugadas.asm”:

Rutina validaJugada - params

Recibe como parámetros de entrada la nueva fila y columna, es decir, la posición de destino. Y devuelve si el movimiento es válido o no con $00 (válido) o $ff (no válido). Nada más empezar inicializamos la salida asumiendo que no será válido.

La validación de los límites del tablero se hace así:

Rutina validaJugada - límites

Es decir, verificamos si la nueva fila es -1 = $ff u 8. Lo mismo con la columna. Y, como las coordenadas válidas van desde 0 hasta 7, ambas incluidas, cualquier fila o columna que valga -1 u 8 están fuera del tablero. No es necesario verificar valores menores que -1 ni mayores que 8, puesto que las fichas sólo se mueven una casilla en cada movimiento.

Por otro lado, la verificación de si el destino está ocupado o no es así:

Rutina validaJugada - contenido

Es decir, se recupera el contenido de la posición de destino con la rutina “dameContenido” y se comprueba si es vacío.

Si cualquiera de las validaciones anteriores (límites o contenido) falla, la rutina termina con un “rts” y devuelve el valor $ff (jugada no válida). Si todas las validaciones se superan, la rutina devuelve $00 (jugada válida).

La rutina anterior tiene una limitación que de momento es aceptable: sólo trabaja sobre el tablero actual (que está implícito). Pero cuando construyamos el árbol de juego para que el C64 juegue por los gatos hará falta validar jugadas sobre un tablero arbitrario del árbol. Llegados ese momento habrá que mejorarla.


Código del proyecto: RYG03

RYG: jugadas del ratón – nuevo programa principal

Ahora ya sí, con las nuevas rutinas de apoyo, podemos ampliar el programa principal “RYG.asm”. Manteniendo lo anterior (inicializar y pintar el tablero actual), añadimos los siguiente:

Programa principal - movs ratón

Es decir:

  • Localizamos el ratón con la nueva rutina “dameRaton”.
  • Convertimos la posición del ratón de formato offset a formato (fila, columna) mediante el uso de la nueva rutina “dameFilaCol”.
  • Hacemos un bucle de 0 a 3 variando el número de jugada del ratón.
  • Solicitamos las jugadas 0 a 3 con la rutina “generaJugadaRaton”.
  • Pintamos las nuevas posiciones del ratón con “pintaHex”.

El resultado es éste:

Programa principal - movs ratón2

Como podéis observar, en cierto modo el programa ya sabe mover el ratón. Sabe que el ratón se podría mover a las posiciones:

  • (6, 2).
  • (6, 4).
  • (8, 4).
  • (8, 2).

Lo que no sabe todavía es que las posiciones (8, 4) y (8, 2) no son válidas, ya que están fuera del tablero. Este será el siguiente paso: validar las jugadas.


Código del proyecto: RYG02

RYG: jugadas del ratón – nuevas rutinas de apoyo

El siguiente paso sería ampliar el programa principal “RYG.asm” para generar y pintar las jugadas del ratón. Pero, antes de eso, o más bien al intentar hacerlo, surge la necesidad de desarrollar nuevas rutinas en “Tableros.asm”.

Rutina “dameRaton”:

La rutina “generaJugadaRaton” no mueve el ratón allí donde esté. Necesitan que le digan dónde está el ratón y, a partir de esta información, genera la nueva posición de salida.

Por tanto, necesitamos una rutina que localice el ratón dentro de un tablero. Esa rutina es la rutina “dameRaton” del fichero “Tableros.asm”. Su código es así:

Rutina dameRaton

La rutina funciona así:

  • Recibe un tablero como entrada.
  • Guardar el registro Y para recuperarlo al final.
  • Prepara el registro Y para que apunte al offset 21, que es el comienzo de la matriz, es decir, la posición (0, 0).
  • Prepara el puntero $fb – $fc para que apunte al comienzo del tablero.
  • Con el modo de direccionamiento indirecto – indexado, lee el contenido de la posición apuntada por Y. Mira si es un ratón ($01).
  • Si el contenido es un ratón, mueve Y a la salida (offset del ratón) y recupera el valor original de Y.
  • Si el contenido no es un ratón, incrementa Y y vuelve a intentarlo.
  • Todo esto hasta el offset 85, que es el final de la matriz. Si llegamos hasta aquí es que no hemos encontrado el ratón y tiene que haber un error. Se señalizaría con el offset $ff, que no es válido.

En definitiva, esta rutina localiza el ratón en el tablero, pero nos devuelve su posición en formato offset, es decir, como desplazamiento o número de bytes desde el comienzo del tablero. Esto tiene la ventaja de que es fácil de obtener, pero es poco práctico de manejar.

Es más práctico de manejar el formato (fila, columna), que de hecho es el formato que acepta “generaJugadaRaton”. Por tanto, necesitamos una rutina de conversión. Esa rutina es “dameFilaCol” y se describe más adelante.

Rutina “dameGato”:

Esta rutina hace lo mismo que “dameRaton” pero para un gato. La principal diferencia es que gatos hay cuatro, mientras que sólo hay un ratón. Por tanto, hay que indicarle qué gato estamos buscando (0 … 3).

La implementación es también muy parecida. La principal diferencia es que, cuando encontramos un gato ($ff), tenemos que mirar si es el que buscamos o no. Si es el gato que buscamos devolvemos su offset; si no lo es, seguimos buscando hasta dar con él.

Rutina “dameFilaCol”:

La rutina “dameFilaCol” convierte un offset en (fila, columna). Es complementaria a la rutina “dameOffset”, que hace justo lo contrario.

La rutina “dameOffset” se basaba en una tabla de datos. En función de la fila, obteníamos un offset de base (21, 29, 37, …) y luego le sumábamos la columna.

Para hacer una implementación similar de “dameFilaCol” necesitaríamos una tabla con tantas entradas como offsets posibles (21 – 84). En realidad, dos tablas, una para las filas y otra para las columnas. Se puede hacer, pero no es una solución muy elegante y consume bastante memoria (dos tablas de 64 posiciones cada una).

Por ello, se adopta otra solución. Supongamos que tenemos el offset 73:

Fila-col

Lo vamos comparando por orden contra el comienzo de cada fila, empezando por la última fila (fila 7) y terminando por la primera (fila 0). En realidad, empezamos comparando contra una fila inexistente, la fila 8, para detectar si el offset es mayor que el permitido:

  • Fila 8 (inexistente): ¿es el offset mayor o igual que 85? En el caso de 73, no. Pero en caso de serlo, la rutina devolvería fila = $ff y columna = $ff, es decir, una señal de error.
  • Fila 7 (última): ¿es el offset mayor o igual que 77? En el caso de 73, no.
  • Fila 6: ¿es el offset mayor o igual que 69? En el caso de 73, sí. Por tanto, devuelve fila 6 y columna 73 – 69 = 4. Es decir, devuelve (6, 4).

El proceso anterior hubiera continuado, caso de ser necesario, hasta la fila 0. De hecho, si el offset fuera menor a 21 (comienzo de la fila 0), también se devolvería una señal de error ($ff, $ff).

El código de la rutina es largo, pero muy repetitivo. Se muestra a continuación un extracto suficiente para entender su funcionamiento:

Rutina dameFilaCol

Con estas rutinas de apoyo (“dameRaton”, “dameGato” y “dameFilaCol”) ya es posible mejorar el programa principal y generar las jugadas del ratón. Esto lo haremos en la siguiente entrada.


Código del proyecto: RYG02

RYG: jugadas del ratón – rutina básica

El siguiente paso es generar las jugadas del ratón, es decir, empezar ya con el generador de jugadas.

Con el ratón va a jugar el humano, es decir, el usuario. Entonces, ¿por qué necesitamos generar sus jugadas? Pues porque en el árbol de juego habrá tableros que proceden de movimientos de los gatos y tableros que proceden de movimientos del ratón. Por tanto, el C64 no sólo tiene que saber mover los gatos; también tiene que saber cómo se mueve el ratón.

Para no mezclar el generador de jugadas con nada de lo anterior (“Tableros.asm”, “PintaTableros.asm”, “RYG.asm” y “Arbol.asm”) estrenamos un nuevo fichero: “GenJugadas.asm”.

La primera parte de este fichero contiene estas dos tablas para el ratón:

Movimientos ratón

La tabla “tblRatonF” nos da los cambios permitidos en la fila del ratón; la tabla “tblRatonC” nos da los cambios permitidos en la columna del ratón. Recorriendo ambas tablas con un índice común, combinando ambos cambios, y recordando que $ff es lo mismo que -1, los cambios permitidos son:

  • Índice 0 => (-1, -1).
  • Índice 1 => (-1, +1).
  • Índice 2 => (+1, +1).
  • Índice 3 => (+1, -1).

Es decir, si los pintamos gráficamente sobre un tablero:

RYG - movs ratón

No todos estos cuatro movimientos serán siempre válidos. En ocasiones, la casilla de destino estará ocupada por un gato o caerá fuera del tablero. Pero esto lo dejamos para un momento posterior. De momento sólo vamos a generar todas las posibilidades.

La segunda parte del fichero “GenJugadas.asm” tiene otro par de tablas (“tablaGatosF” y “tablaGatosC”), pero ahora dedicadas al movimiento de los gatos. Por tanto, no vamos a verlas de momento.

La última parte del fichero contiene la rutina “generaJugadaRaton”, que genera una jugada para el ratón. Esta rutina recibe como parámetros de entrada:

  • Fila del ratón.
  • Columna del ratón.
  • Número de jugada, numeradas desde el 0 hasta el 3.

Y devuelve como parámetros de salida:

  • La nueva fila del ratón.
  • La nueva columna del ratón.

El campo “gjrTempX”, como ya hemos comentado, no es más que un almacén temporal para el registro X, puesto que vamos a modificar su valor para acceder con él a la tabla de movimientos del ratón.

El código de la rutina “generaJugadaRaton” es así:

Rutina generaJugadaRaton

Es decir:

  • Guarda el valor del registro X.
  • Carga el número de jugada (0 … 3) en el registro X.
  • Usando el registro X (número de jugada) como índice, obtiene el cambio en la fila y lo suma a la fila actual del ratón. El resultado lo pone en la salida.
  • Usando el registro X (número de jugada) como índice, obtiene el cambio en la columna y lo suma a la columna actual del ratón. El resultado lo pone en la salida.
  • Recupera el valor original del registro X.

De este modo, variando el número de jugada desde el valor 0 hasta el valor 3 podemos generar todas las jugadas del ratón. Cuestión diferente es que sean válidas o no.


Código del proyecto: RYG02

RYG: programa principal

Hasta ahora tenemos:

Por tanto, lo siguiente es ya ponernos manos a la obra con el programa principal. Este programa lo definimos en el fichero “RYG.asm” y de momento hace muy poca cosa:

Programa principal

Es decir, el programa principal tiene un cargador BASIC en $801 (10 SYS 2064) y se carga a partir de $810. Hace esto:

  • Inicializa un tablero llamado “tableroActual”.
  • Pinta “tableroActual”.
  • Y termina con “rts”.

La etiqueta “tableroActual” apuntará al tablero actual, es decir, al tablero que contiene la situación actual de la partida. Como no vamos a llevar un registro de jugadas no nos hace falta una lista enlazada de tableros; nos llega con el tablero actual.

Esta etiqueta no está definida en “RYG.asm”, sino que está definida en otro fichero: el fichero “Arbol.asm”. En este fichero iremos metiendo más etiquetas (es decir, más variables), no sólo para el tablero actual, sino también para el árbol de juego.

TableroActual

Y dado que el árbol de juego va a crecer, se va a tomar una decisión de juego sobre él, se va a tirar, se va a generar otro árbol de juego, y así sucesivamente, necesita memoria libre que no esté ocupada por código. Por eso es importante que el fichero “Arbol.asm” vaya al final de la lista de construcción del proyecto (en CBM prg Studio menú Project > Properties):

Build

De hecho, deberíamos hacer una estimación, aunque sea aproximada, de la memoria que vamos a necesitar en función de la profundidad del árbol. Los gatos tienen un máximo de ocho movimientos y el ratón de dos, aunque no todos los movimientos son posibles siempre. Así:

  • 1 nivel => 85 bytes x 8 movimientos = 680 bytes.
  • 2 niveles => 85 bytes x 8 x 4 = 2720 bytes.
  • 3 niveles => 85 bytes x 8 x 4 x 8 = 21760 bytes.
  • 4 niveles => 85 bytes x 8 x 4 x 8 x 4 = 87040 bytes.

Es decir, todo parece indicar que, con esta estructura de datos, lo máximo a lo que vamos a poder llegar es a 3 niveles de profundidad. Para llegar a 4 niveles, es decir, a 8 x 4 x 8 x 4 = 1024 tableros, harían falta casi 87 KB que lógicamente no están disponibles. Pero sí sería posible diseñar una estructura de datos más compacta.

Bueno, el resultado del programa principal, de momento, es el ya conocido: un tablero inicializado y pintado por pantalla. Poco a poco vamos construyendo.


Código del proyecto: RYG01

RYG: pintado de tableros

Tanto para dotar al programa de una interfaz de usuario como para poder hacer tareas de depuración, se hace necesario pintar tableros por pantalla.

De momento optaremos por un pintado sencillo, en modo texto. Más adelante se podrá mejorar el aspecto con caracteres personalizados o, incluso, con sprites.

El pintado de tableros no es difícil. Básicamente consiste en utilizar algunas rutinas de la librería “LibText” que desarrollamos durante los proyectos del volumen I y el volumen II. Cuando se trata de pintar números o direcciones usamos “pintaHex” y cuando se trata de pintar cadenas de texto usamos “pintaCadena”.

Aunque no es difícil, sí es farragoso pintar un tablero entero. Por ello, la nueva rutina “pintaTablero”, que es larga y tiene subrutinas, la implementamos en un nuevo fichero “PintaTableros.asm”.

La rutina “pintaTablero” no tiene mucho misterio, así que sólo daré un par de pinceladas sobre su implementación. Básicamente se trata de dividir el trabajo en varias subrutinas:

Rutina pintaTablero

Cada una de estas subrutinas (“pintaSeparador”, “pintaDatosBasicos”, etc.) se encarga de una función más pequeña y más específica.

Quizás el mayor misterio esté en la subrutina “pintaTabla”, que es la que pinta la matriz de datos del tablero. Básicamente es un bucle que pinta la matriz fila por fila:

Rutina pintaTabla

Y, a su vez, pintar una fila es pintar las fichas (ratón, gato o vacío) de todas las columnas de esa fila. Y para obtener esa información nos apoyamos en la ya conocida “dameContenido”:

Rutina pintaFila

Por último, la rutina “pintaFicha” pinta una “R” para el ratón, una “G” para los gatos, y un punto para las casillas vacías:

Rutina pintaFicha

En resultado es algo así (de momento el tablero no tiene información de padre ni hijos, pero sí está inicializado con las piezas en su sitio):

Tablero pintado

Como digo, esto se puede convertir en algo más vistoso más adelante mediante el uso de caracteres personalizados, pero no vamos a empezar la casa por el tejado.


Código del proyecto: RYG01

RYG: rutinas para manejar tableros

Ahora que tenemos definida la pieza clave del proyecto, que es la estructura de datos para manejar tableros, resultado muy conveniente definir una serie de rutinas para manejar tableros (ver fichero “Tableros.asm”). De este modo, se pueden definir rutinas para:

  • Inicializar un tablero.
  • Fijar y recuperar sus datos básicos (nivel, turno y valor).
  • Fijar y recuperar su padre.
  • Fijar y recuperar sus hijos.
  • Fijar y recuperar la pieza de una posición (fila, columna).
  • Etc.

Esta librería de rutinas la iremos completando y perfeccionando según avance el proyecto y según vayan surgiendo nuevas necesidades.

Todas estas rutinas, en general, se apoyarán en el modo de direccionamiento indirecto – indexado. Configuraremos el puntero de página cero $fb – $fc apuntando al primer byte del tablero (nivel) y, a partir de ahí, usando el registro Y como índice, iremos al campo de nuestro interés para leer (instrucción “lda ($fb),y”) o escribir (instrucción “sta ($fb),y”):

Rutinas

Como sabemos, cuando un programa llama a una rutina, no hay garantía de que la rutina mantenga inalterados los registros del microprocesador (acumulador, registro X y registro Y). Pero como en este proyecto vamos a usar mucho los registros X e Y para tareas como recorrer los hijos de un tablero o los campos de una estructura de datos, nos interesa que las rutinas sí conserven estos registros. Por ello, aquellas rutinas que vayan a modificar X y/o Y tendrán un campo TempX y/o TempY donde resguardar los valores de los registros al empezar y recuperarlos justo antes de terminar.

Y como todas las rutinas enumeradas anteriormente son parecidas, nos limitaremos a explicar una rutina de cada tipo:

Rutina “fijaContenido”:

El código de la rutina es así:

Rutina fijaContenido

La rutina recibe como parámetros de entrada:

  • Un puntero al tablero, con sus bytes “lo” y “hi”.
  • La fila y columna cuyo contenido se quiere modificar.
  • El contenido o ficha a poner en (fila, columna), es decir, $00 para vacío, $01 para ratón, y -1 = $ff para gato.

Obsérvese que también hay un byte “fcTempY”. Esto no es un parámetro de entrada ni de salida. Simplemente es un campo en el que vamos a guardar temporalmente el valor del registro Y (“sty fcTempY”) para recuperarlo luego justo antes de terminar (“ldy fcTempY”). De este modo, aunque la rutina modifique el valor del registro Y, y lo modifica para usar el modo de direccionamiento indirecto – indexado (“sta ($fb),y”), la rutina o programa llamante no se verá afectado. El programa llamante puede confiar en que no le cambiarán el valor de Y.

Esquemáticamente, lo que hace la rutina es:

  • Resguarda el valor del registro Y, porque lo va a modificar.
  • Convierte las coordenadas (fila, columna) en un offset contado desde el comienzo del tablero. Este offset es el número de bytes que hay que contar desde comienzo del tablero (byte de nivel) para llegar al byte de la (fila, columna) que hay que modificar. Se obtiene con la rutina complementaria “dameOffset”.
  • Prepara el puntero $fb – $fc para que apunte al tablero.
  • Carga el offset en el registro Y.
  • Modifica el contenido de la posición (fila, columna) con “sta ($fb),y”.
  • Recupera el valor del registro Y.

Fácil, ¿no? Básicamente es usar el modo de direccionamiento indirecto – indexado. Se prepara un puntero al comienzo del tablero (indirección), se calcula el offset de la posición (fila, columna) respecto al comienzo del tablero, y se modifica la posición en ese offset (indexación).

Cuando lo que fija la rutina es el nivel, el turno, el valor, etc., no hace falta calcular ningún offset. La rutina correspondiente ya “conoce” por diseño de la estructura de datos cuál es el offset de esos campos. Por ejemplo, el nivel tiene offset 0, el turno tiene el offset 1, y el valor tiene el offset 2.

El cálculo del offset sólo es necesario cuando se quiere fijar o recuperar el contenido de una posición (fila, columna), es decir, cuando ya entramos en la matriz de datos.

Rutina dameOffset:

La matriz o tablero propiamente dicho empieza en el offset 21 de la estructura de datos. Ese byte correspondiente a la posición (0, 0) del tablero.

A partir del offset 21, cada fila añade otros 8 bytes de offset. Es decir: 21, 29, 37, 45, 53, 61, 69 y 77. Creamos una tabla con estos datos y accedemos a ella usando como índice el registro X (previo cargar en él el número de fila, claro).

Eso nos da el offset del comienzo de cada fila. Luego basta sumar la columna. Para sumar no hay que olvidarse de borrar antes el acarreo con “clc”.

Fácil, ya tenemos la rutina “dameOffset”:

Rutina dameOffset

Ya hemos comentado otras veces que, en ensamblador, es mucho más fácil resolver las fórmulas matemáticas mediante tablas de datos que mediante operaciones aritméticas (offset = 21 + 8 * fila + columna).

Rutina “dameContenido”:

Esta rutina es completamente análoga a la rutina “fijaContenido”, sólo que en vez de escribir datos con “sta ($fb),y” los lee con “lda ($fb),y”. No necesita mucha más explicación.

Y el resto de rutinas del fichero “Tableros.asm”, al menos las rutinas de esta primera versión del proyecto, son todas muy parecidas. Todas utilizan el modo de direccionamiento indirecto – indexado.

La librería de rutinas se irá mejorando con otras nuevas con el avance del proyecto. Y las iremos comentando.


Código del proyecto: RYG01

RYG: estructura de datos del tablero

Lo primero que hace falta para empezar a trabajar es una estructura de datos que permita almacenar un tablero, ya que los tableros van a ser nuestra materia prima.

Como hemos comentado anteriormente, lo más básico es tener una matriz de 8 x 8, pero hacen falta más cosas:

  • El nivel: la profundidad del tablero en el árbol de juego.
  • El turno: si le toca mover al ratón o a los gatos.
  • El valor: el resultado de la función de evaluación o del procedimiento minimax, según sea una hoja o un tablero intermedio.
  • El padre: el tablero del que procede este tablero.
  • Los hijos: los tableros que proceden de este tablero.
  • La matriz: el tablero propiamente dicho.

De este modo, podemos diseñar una estructura de datos así (ver fichero “Tableros.asm”):

Estructura datos tablero

En total son 85 bytes que se reparten así:

  • Nivel: Es el nivel de profundidad que ocupa el tablero en el árbol de juego. Con un byte será suficiente.
  • Turno: Es el turno, es decir, si le toca mover a ratón ($01) o gatos (-1 = $ff). Nuevamente, con un byte será suficiente.
  • Valor: Es el valor que la función de evaluación o el procedimiento minimax asignan al tablero. Será suficiente con un byte.
  • Padre: El tablero en curso procederá de un tablero padre. Ese tablero padre será otra estructura de 85 bytes. Con tener la dirección de memoria de su primer byte (su comienzo) será suficiente. Para esa dirección necesitamos dos bytes, el byte “lo” y el byte “hi”.
  • Hijos: El tablero en curso podrá dar lugar, como mucho, a ocho tableros hijo (un ratón puede moverse a un máximo de cuatro casillas, y cada gato a un máximo de dos). Por tanto, necesitaremos ocho direcciones de memoria para llevar control de toda la progenie. Como en el caso del padre, cada dirección tendrá su byte “lo” y su byte “hi”.
  • Tablero: Es una matriz de 8 x 8 para especificar la posición de las piezas sobre el tablero. Equivale a una secuencia de 8 x 8 = 64 bytes. Codificaremos con $00 las casillas vacías, con $01 el ratón, y con -1 = $ff los gatos.

En definitiva, un tablero será una estructura de datos de 85 bytes que vendrá especificada por la dirección de memoria de su primer byte (el nivel). Conociendo la dirección de ese primer byte, y utilizando el modo de direccionamiento indirecto – indexado, es posible acceder a cualquiera de los campos del tablero:

Tableros en memoria

En particular, usando el puntero al padre y los punteros a los hijos es posible navegar por el árbol de juego, tanto hacia arriba (hacia la raíz) como hacia abajo (hacia las hojas).

Cuando una dirección no esté definida (ej. el padre o algún hijo), usaremos la dirección $0000 para indicarlo.

Más adelante meteremos una pequeña variación en esta estructura de datos porque no es muy práctico que ocupe 85 bytes. Sería mucho más práctico que ocupara 88 bytes porque, al ser un múltiplo de 8 (88 = 11 x 8), se facilita mucho la visualización de los tableros en el depurador de CBM prg Studio durante la fase de depuración del programa. Para esto, llega con meter un relleno o prefijo de tres bytes que, además, tiene la ventaja de marcar la frontera en memoria entre tableros si se elige un patrón fácilmente reconocible de forma visual.


Código del proyecto: RYG01 

El ratón y los gatos (RYG)

El ratón y los gatos es un juego de tablero sencillo. Se juega con un tablero clásico de 8 x 8 casillas. Hay dos bandos:

  • Los gatos, que consta de cuatro peones negros.
  • El ratón, que consta de un único peón blanco.

Normalmente los gatos se ponen en la parte superior del tablero y se mueven hacia abajo, y el ratón se coloca en la parte inferior y se mueve hacia arriba, aunque lógicamente esto depende de cómo queremos orientar el tablero. Ambos tipos de piezas, ratón y gatos, se colocan sobre casillas del mismo color, supongamos que sobre casillas blancas.

La situación inicial de la partida es como sigue:

RYG - tablero inicial

Empieza moviendo el ratón, que puede moverse en diagonal hacia adelante o hacia atrás, siempre una casilla:

RYG - movs ratón

Los gatos pueden moverse uno por turno, y nuevamente en diagonal y una casilla, pero sólo hacia delante (es decir, hacia abajo). No pueden retroceder:

RYG - movs gatos

No hay capturas, es decir, los gatos no pueden comerse al ratón, ni mucho menos el ratón comerse a los gatos 🙂 .

El objetivo del juego es que los gatos rodeen al ratón, y lo dejen sin posibilidad de moverse, o que el ratón llegue hasta la fila superior del tablero. En el primer caso ganan los gatos; en el segundo gana el ratón:

RYG - objetivos

Así de simple. Pero el reto es programarlo en ensamblador del C64…

Como los gatos tienen pinta de malos, dejaremos que el C64 juegue por los gatos. El humano será el ratón e intentará esquivarlos.

Elección de un juego de tablero

Ya tenemos las bases del diseño:

  • Matrices para codificar los tableros.
  • El tablero actual para llevar control de dónde está la partida.
  • Un generador de jugadas (o tableros).
  • El árbol de juego para decidir las jugadas del C64.
  • Una función de evaluación.
  • Un procedimiento minimax.

No implementaremos un registro de jugadas, aunque no sería complejo, porque no vamos a dar funcionalidades como rectificar jugadas ni grabar / cargar partidas. Y tampoco implementaremos un procedimiento de poda.

De todo lo anterior, las partes más complejas son:

  • El generador de jugadas.
  • La función de evaluación.

El generador de jugadas es tanto más complejo como complejo sea el juego. En el caso del ajedrez, que hay muchos tipos de piezas, muchos movimientos posibles, y muchas reglas, el generador de jugadas será necesariamente complejo. En el caso de las damas, que hay menos tipos de piezas, menos movimientos posibles, y menos reglas, el generador de jugadas será más sencillo. Y en el caso de Othello / Reversi, cuyo movimiento principal consiste en colocar una ficha sobre una casilla vacía y flanqueando al contrincante, el generador de jugadas será sencillo.

La función de evaluación también es compleja, pero no tanto porque sea difícil de programar, sino porque es difícil definir criterios que midan bien si los tableros son buenos o malos para los bandos. Los criterios materiales son más claros (tantos puntos por pieza de tal tipo); pero los criterios posicionales son difíciles de identificar y definir. Y son tanto más difíciles como complejo sea el juego.

En resumen, un juego como el ajedrez por supuesto es abordable, incluso con un C64, y con mucho éxito. A los hechos me remito (véase Colossus Chess). Pero su complejidad hace que esté fuera del alcance este blog. Un juego como las damas o el Othello / Reversi sí son abordables al nivel amateur.

En todo caso, como el objetivo es ejemplificar todos estos conceptos, y además en ensamblador del C64 (ya de por sí complejo), vamos a optar por un juego de tablero sencillo: el ratón y los gatos.

En la siguiente entrada describiremos el juego.