text/gemini
# Juego de la Vida en ensamblador para MSX
elpamplinadecai@gmail.com
@ElPamplina@masto.es
Publicado el 18-5-2024
# Introducción histórica
Esta historia comienza cuando yo tenía 16 años, es decir, en un universo muy muy lejano. Yo era orgulloso poseedor de un Sony Hitbit MSX, lo cual era lo más parecido a ser un outsider de la informática, en un mundillo dominado por los Spectrum y los Amstrad. Los del MSX éramos los raritos dentro de los raros (el término friki todavía no se usaba entonces).
=> img/juego-vida/hitbit.jpg El Sony HB-75P
=> img/juego-vida/arranque.png Pantalla de arranque del MSX
=> img/juego-vida/caratula.png Pantalla de bienvenida del Sony HitBit con el programa organizador personal que incorporaba.
El MSX era fruto de un consorcio de fabricantes (parecido a lo que fue el VHS), entre los que estaban Sony, Phillips y muchos más. Tuvo poca penetración en Europa, y bastante más en Japón. Para esa primera versión del sistema apostaron por usar el mismo procesador de los Spectrum (el Zilog Z80), quizá con la esperanza de atraer a programadores de la competencia.
El software se lo encargaron a una joven empresa poco conocida, Microsoft, que por entonces estaba haciendo sistemas para IBM y podía aprovechar buena parte del desarrollo del lenguaje BASIC que estaba haciendo para el PC.
El desarrollo del MSX BASIC se realizó a partir del GW-BASIC que se solía usar en los primeros PC-DOS y MS-DOS, y la BIOS era una simplificación del sistema de los PC, que se podía ampliar con un subsistema extra denominado MSX-DOS para los equipos con unidades de disco. Por tanto, nuestros queridos MSX eran casi lo más parecido a un PC que se podía encontrar en el mercado doméstico.
## Mi camino hacia el código máquina
Al contrario que muchos, yo no había llegado al mundo de los ordenadores para jugar a marcianitos, aunque también, sino porque ya entonces me apasionaba la programación. Un bicho como mi MSX era el campo perfecto de experimentación. En un par de años ya me había hecho programas para casi todo lo que me había pasado por la cabeza, incluso había ganado un premio en una feria de ciencias de mi instituto con un juego sobre en aparato digestivo.
Con las 3000 pesetas del premio, un pastón para mi inexistente economía, me compré un libro especializado en ensamblador para el MSX. Eso del ensamblador era llegar al Shangri-La de la programación, el Sancta Sanctorum. Hasta ahora había programado usando el BASIC que traía instalado el MSX, y el ensamblador era la oportunidad de saltarme las limitaciones de rendimiento y acceso a memoria para sacarle todo el jugo a la máquina que tenía delante.
=> img/juego-vida/MSX-Lenguaje-Maquina-Data-Becker.webp Portada del libro. El mío debe estar en algún sitio en casa de mis padres, y seguro que no estaba tan estropeado.
El libro traía un programa ensamblador escrito en BASIC, para traducir las instrucciones a código máquina, el cual tecleé enterito solo para darme cuenta de que ¡no funcionaba! Intenté usar todos mis (muy limitados) conocimientos en programación para arreglarlo, pero no pude. Intenté escribir yo mismo un ensamblador, que quedó bastante bien, pero adolecía de las mismas lagunas que mi conocimiento técnico. No olvidemos que no había Internet ni nada parecido, y todo lo que yo sabía era por un libro y algunos artículos en revistas.
Sin eso, todo lo que aprendiera de código máquina no me servía para nada, así que me decidí a comprar el ensamblador que anunciaban en la revista MSX-Club, que estaba editado por la propia editorial de la revista (Manhattan Transfer S.A., un sugerente nombre).
=> img/juego-vida/MSX-Club-28.jpg Portada del número 28 de MSX-Club
La editorial tenía la curiosa política de no admitir contrarreembolsos, sino que había que enviar el importe en un cheque bancario. Ni corto ni perezoso, me fui a la Caja Postal y abrí mi primera cuenta corriente, solo para poder rellenar un cheque y pedir el programa. Ya veis lo picado que estaba.
Recibí la casete con el programa ensamblador, el cual resultó ser una maravillosa creación de software. Usaba el mismo entorno de programación del BASIC, pero con el lenguaje ensamblador en su lugar, lo cual era fantástico para quien, como yo, estaba acostumbrado a ese entorno.
=> img/juego-vida/RSC0002.webp Pantalla inicial del ensamblador RSC versión 1
Al creador, Ramón Sala, nunca lo he conocido, pero desde aquí le mando mi abrazo y mi admiración. Una obra maestra.
# El juego de la vida y los problemas de memoria
Por entonces había leído en algún sitio, no recuerdo dónde, acerca del Juego de la Vida de Conway, y me pareció un reto perfecto para programarlo en mi MSX.
=> https://es.wikipedia.org/wiki/Juego_de_la_vida Juego de la vida - Wikipedia
Rápidamente hice una primera versión en BASIC, pero que era inutilizable por su lentitud y también por la poquísima memoria que tenía disponible en dicho entorno.
El modelo de Hitbit que tenía contaba con sus orgullosos ¡80 KB! de memoria RAM, lo cual era mucho para la época, pero poco usable. De entrada, el venerable procesador Z80 manejaba direcciones de 16 bits, lo que significa que solo podía direccionar 64 KB de memoria. Para usar los 16 KB fuera del espacio de direcciones había que usar técnicas de manejo de entrada/salida (desconocidas para mí) solo al alcance del lenguaje ensamblador.
Por otra parte, la memoria ROM que albergaba el sistema y el intérprete de BASIC ocupaba sitio en ese espacio de direcciones, por lo que la RAM accesible en la práctica eran mucho menos de los 64 KB iniciales. El sistema se encargaba de recordártelo en el prompt inicial nada más arrancar: 28815 bytes free.
=> img/juego-vida/basic.png Pantalla inicial del MSX BASIC
Para colmo, la forma en que el BASIC maneja los datos no es la más eficiente posible en cuestión de memoria. El MSX BASIC solo tenía datos numéricos en coma flotante, nada de enteros ni mucho menos bits individuales. Eso significaba que si mi juego de la vida tenía que manejar una matriz, digamos, de 32x32, estaba ocupando en memoria el espacio para 1024 números en coma flotante, lo cual se traduciría seguramente en más de 4 KB de mi preciosa memoria. Si en su lugar hubiera usado matrices de cadenas (cosa que en ese momento no se me ocurrió), hubiera ido la cosa un poco mejor, aunque igualmente hacer un juego de la vida de diez o veinte generaciones hubiera sido inabarcable.
Una celda del juego de la vida en realidad ocupa muchísimo menos que un número en coma flotante, de hecho se puede representar con un solo bit (1=la celda está ocupada, 0=la celda está vacía). Así que me propuse hacer un juego de la vida en ensamblador donde manejara los bits individualmente. Eso estaba completamente fuera de mis conocimientos de programación y me estrellé estrepitosamente.
Como dije antes, sabiendo lo que ahora sé, me doy cuenta de que se podría haber aprovechado mejor la memoria haciendo un array de cadenas de caracteres, con un carácter (por ejemplo el asterisco) indicando una celda ocupada, y un espacio indicando la celda vacía. Esto hubiera ocupado más espacio que la codificación bit a bit, pero muchísimo menos que la representación numérica (concretamente, un byte por celda) y mucho más fácil de programar.
Con esta representación, los 15080 bytes libres del sistema me hubieran dado para almacenar hasta 14 generaciones de 32x32, lo cual hubiera sido suficiente para cantar victoria.
Al poco tiempo de aquella fiebre ensambladora, entré en la universidad a la carrera de informática, donde se programaba en Pascal y C, así que el MSX se quedó solo como consola para jugar los videojuegos que me pasaba algún amigo en una casete regrabada mil veces. El reto del juego de la vida en ensamblador se quedó inconcluso... HASTA HOY.
Cuarenta años después, me he encontrado por casualidad con un maravilloso emulador denominado openMSX y ahora puedo recrear mi viejo Hitbit en mi portátil. También he conseguido el viejo ensamblador de Ramón Sala (una versión mucho mejor llamada RSC II) en un repositorio de software retro, y me he decidido a terminar lo que empecé hace ocho lustros. ¡¡El JUEGO DE LA VIDA para MSX renacerá de sus cenizas!!
=> https://openmsx.org/ Web oficial de OpenMSX
## Midiendo la memoria
En aquellos tiempos no tenía nada claro qué espacio de memoria podía usar para almacenar las celdas de mi juego de la vida. Es algo que ninguno de los libros que pude consultar aclaraba. El libro de Data Becker empezaba sus programas en la dirección &HF000 (&H es el prefijo para los números hexadecimales en el MSX). Eso dejaba menos de 4 KB teóricos para almacenar, pero no explicaba cómo aprovechar mejor esos supuestos 15 KB que me decían que tenía.
Me faltaba un buen mapa de memoria, como el que hoy se puede encontrar en:
=> https://www.msx.org/wiki/The_Memory The Memory - MSX Wiki
Resumiendo, la dirección más baja de RAM libre (que depende de la cantidad de RAM del equipo), se define en la variable BOTTOM (dirección &HFC48). Estas variables del sistema son de 16 bits, y el Z80 es "little endian", por lo que los 8 bits menos significativos están en la dirección &HFC48 y los 8 más significativos en &HFC49.
En ordenadores con 32 KB o más de RAM, BOTTOM vale &H8000, señalando así la dirección más baja accesible de la RAM. Pero no podemos empezar a usar directamente esa memoria, porque hay que dejar sitio a la pila del sistema (donde se guardan, entre otras cosas, las direcciones de retorno a subrutinas). La cabeza de la pila (dirección más alta ocupada por esta) se guarda en la variable DSKTOP (&HF674).
Después de la cabeza de la pila, hay que dejar además espacio para la zona de almacenamiento de cadenas de caracteres (que son 200 bytes por defecto).
El final del espacio de cadenas (variable MEMSIZ, &HF672), va seguido de un espacio adicional reservado para el sistema y ¡por fin! la variable HIMEM (&HFC4A) delimita el inicio de la "tierra de nadie" que se puede usar para almacenar nuestros programas y datos en código máquina.
La dirección más alta que se puede usar es fija en todos los MSX (&HF37F). Desde &HF380 hasta &HFFFF hay un espacio de trabajo del sistema que por nuestro bien, en teoría, no debemos tocar.
Para establecer el valor de todas esas variables, usamos el comando CLEAR. Su primer argumento es el tamaño a reservar para cadenas de caracteres, y el segundo es el valor para HIMEM, primera dirección que queremos libre para poder usar. A partir de esos parámetros, el resto de áreas se establecen automáticamente. Cuanto más baja sea la dirección HIMEM usada, más baja estará la pila y más espacio nos quedará libre hasta el límite fijo en &HF380.
Haciendo pruebas con el emulador, he comprobado que el HIMEM más bajo que me hubiera permitido mi querido Hitbit es &H8600. El siguiente programa establece ese límite y muestra por pantalla como quedan el resto de variables:
```
5 CLEAR 200, &H8600
10 PRINT "BOTTOM (Inicio memoria libre): &H"; HEX$(PEEK(&HFC49)*256 + PEEK(&HFC48))
20 PRINT "DSKTOP (Base de la pila, inicio área de cadenas): &H"; HEX$(PEEK(&HF675)*256 + PEEK(&HF674))
30 PRINT "MEMSIZ (Fin área de cadenas): &H"; HEX$(PEEK(&HF673)*256 + PEEK(&HF672))
40 PRINT "HIMEM (Inicio área de usuario): &H"; HEX$(PEEK(&HFC4B)*256 + PEEK(&HFC4A))
50 PRINT "(fijo) Inicio área del sistema, fin área de usuario: &HF380"
```
El resultado es:
```
BOTTOM (Inicio memoria libre): &H8000
DSKTOP (Base de la pila, inicio área de cadenas): &H8320
MEMSIZ (Fin área de cadenas): &H83E8
HIMEM (Inicio área de usuario): &H8600
(fijo) Inicio área del sistema, fin área de usuario: &HF380"
```
Esto significa que estaría poniendo la pila en &8320-&H8000, dejándole unos exiguos 800 bytes. Mi campo de juego estaría en &H8600-&HF37F (28032 bytes). Por supuesto, esto es seguramente poco recomendable y espero no necesitar tanto para nuestro juego.
La pantalla del MSX tiene 40x24 caracteres en el modo 0. Por lo tanto, me planteo un tablero máximo de 24x24 y usar las 16 columnas de la derecha para mostrar información de progreso.
No es buena idea representar las celdas del juego con bits individuales, lo que mi mente calenturienta de adolescente pretendía, porque la cosa se complicaría demasiado, y además no es necesario. Voy a usar un byte completo para representar cada celda. Esto da 24x24 = 576 bytes por generación. Evitando ocupar el máximo de 28 KB, podemos plantear usar unos 20 KB, que daría un límite de 34 generaciones, que está muy bien. Esto fijaría el HIMEM de nuestro programa a partir de &HA500.
# Planteamiento del problema
Lo mínimo que necesita el programa en cada ciclo es tener en memoria una generación (ya completa) y espacio para la siguiente (el área de trabajo), para ir construyendo una a partir de la otra. Para simplificar, voy a ir guardando una generación tras otra, hasta que se agote la memoria (el programa se colgará si nos pasamos). En futuras versiones más refinadas se podrá hacer cíclico, e incluso que se pueda ir hacia atrás en las últimas generaciones.
De cada byte que representa una celda, me bastaría con controlar el estado de un único bit, pero para facilitar el uso del caracteres que se puedan representar y queden bien, utilizaré el asterisco (código ASCII 2A, 00101010 en binario) y el espacio (código hexadecimal 20, 00100000 en binario). Bastará con comprobar el bit 1 (segundo por la derecha) para saber si una celda está ocupada.
La rutina principal va a ser pasar por cada una de las celdas, usando un puntero que se irá incrementando y aplicarle un algoritmo que contará cuántas celdas adyacentes están vivas (esta es la parte complicada). Una vez calculado, se actuará en consecuencia, escribiendo en el área de trabajo la celda correspondiente (20=muerta, 2A=viva).
Las tres reglas de Conway las aplicaré de esta manera:
- Si tiene exactamente 3 celdas adyacentes vivas, la celda estará viva en la siguiente generación, sin importar su estado actual.
- Si tiene 2 celdas adyacentes vivas y su estado actual es viva, entonces seguirá viva en la siguiente generación.
- En cualquier otro caso, la celda estará vacía en la siguiente generación.
## Esqueleto de programa
Voy a empezar por el esqueleto, es decir, la parte que prepara la pantalla y muestra la generación cero. Después de eso, esperará a que se pulse una tecla y empezará a sacar generaciones. En cada pulsación de tecla se adelanta una generación, mientras que pulsar ESC nos sacará del programa.
La generación cero será en principio fija, definida con directivas DEFM en el código ensamblador. Más tarde haremos el programita BASIC para cargar cualquier tablero que se nos ocurra en las mismas direcciones.
### Manejo de pantalla
No voy a meterme en definir gráficos, sino que iré a lo práctico. En el modo de pantalla 0, el más simple, el mapa de caracteres (esquina superior izquierda) empieza en la dirección 0 de la memoria VRAM, ocupando 40 posiciones cada fila.
Bastará con ir moviendo los datos desde la RAM a las posiciones adecuadas de ese mapa. Eso se hace con una rutina estándar llamada LDIRVM que se ubica en la dirección fija &H005C de la BIOS.
Otras rutinas, como el reseteo y limpiado de la pantalla, etc. corresponden a posiciones fijas de la BIOS. Se pueden consultar todas las rutinas existentes en esta magnífica página:
=> https://map.grauw.nl/resources/msxbios.php MSX BIOS calls
### Primera versión funcional
El siguiente listado corresponde a la primera versión del esqueleto, consistente solo en cargar un tablero lleno de asteriscos como generación 0, pintar el título del programa y otras etiquetas de estado y terminar esperando la pulsación de una tecla.
=> img/juego-vida/esqueleto0001.png Pantalla del programa en esta fase
```
10 ORG &HA500
20 ; Llamadas del sistema (https://map.grauw.nl/resources/msxbios.php)
30 CHGMOD: EQU &H5F ; Cambiar modo de pantalla
40 ERAFNK: EQU &HCC ; Quitar teclas de funcion
50 LDIRVM: EQU &H5C ; Copiar bloque a VRAM
60 CHGET: EQU &H9F ; Esperar una tecla
70 POSIT: EQU &HC6 ; Posicionar cursor
80 CHPUT: EQU &HA2 ; Escribir caracter
90 ; Reiniciar pantalla (SCREEN 0)
100 LD A,0
110 CALL CHGMOD
120 CALL ERAFNK
130 ; Escribir estado
140 LD L,1 ; Linea 1
150 LD H,26 ; Columna 26
160 CALL POSIT
170 LD HL,TIT1
180 CALL IMPETI
190 LD L,2 ; Linea 2
200 LD H,26 ; Columna 26
210 CALL POSIT
220 LD HL,TIT2
230 CALL IMPETI
240 LD L,3 ; Linea 3
250 LD H,26 ; Columna 26
260 CALL POSIT
270 LD HL,TIT3
280 CALL IMPETI
290 LD L,5 ; Linea 5
300 LD H,26 ; Columna 26
310 CALL POSIT
320 LD HL,ETI1
330 CALL IMPETI
340 LD HL,0
350 CALL IMPNUM
360 LD L,7 ; Linea 7
370 LD H,26 ; Columna 26
380 CALL POSIT
390 LD HL,ETI2
400 CALL IMPETI
410 LD L,8 ; Linea 8
420 LD H,26 ; Columna 26
430 CALL POSIT
440 LD HL,ETI3
450 CALL IMPETI
460 ; Volcar generacion a pantalla
470 LD HL,GEN0 ; TODO hacer bucle y HL ira apuntando a cada generacion
480 CALL PINTAR
490 ; Final
500 CALL CHGET
510 RET
520 ; Rutina de pintado del tablero
530 PINTAR: ; viene HL apuntando a la casilla Dimension
540 LD A,(HL)
550 LD C,A
560 LD B,0
570 INC HL
580 LD DE,0
590 PUSH HL
600 PUSH BC
610 PUSH DE
620 PUSH AF
630 LINEA: CALL LDIRVM
640 ; Sumar 40 a DE para saltar a la siguiente linea de pantalla
650 POP AF
660 POP HL ; Intermediario para el valor de DE
670 LD BC,40
680 ADD HL,BC
690 LD D,H
700 LD E,L
710 POP BC
720 POP HL
730 ; Sumar dimension para siguiente linea del tablero
740 ADD HL,BC
750 PUSH HL
760 PUSH BC
770 PUSH DE
780 ; Decrementar A y bucle si !=0
790 DEC A
800 PUSH AF
810 CP 0
820 JR NZ,LINEA
830 POP AF
840 POP DE
850 POP BC
860 POP HL
870 RET
880 ; Rutina imprimir etiqueta en HL
890 IMPETI: LD A,(HL)
900 CP 255 ; Buscar fin de cadena
910 RET Z
920 INC HL
930 CALL CHPUT
940 JR IMPETI
950 ; Rutina imprimir numero decimal,
960 ; https://wikiti.brandonw.net/index.php?title=Z80_Routines:Other:DispHL
970 IMPNUM: LD BC,-10000
980 CALL NUM1
990 LD BC,-1000
1000 CALL NUM1
1010 LD BC,-100
1020 CALL NUM1
1030 LD C,-10
1040 CALL NUM1
1050 LD C,-1
1060 NUM1: LD A,"0"-1
1070 NUM2: INC A
1080 ADD HL,BC
1090 JR C,NUM2
1100 SBC HL,BC
1110 CALL CHPUT
1120 RET
1130 TIT1: DEFM "JUEGO DE"
1140 DEFB 255 ; Fin de cadena
1150 TIT2: DEFM "LA VIDA"
1160 DEFB 255
1170 TIT3: DEFM "por ElPamplina"
1180 DEFB 255
1190 ETI1: DEFM "Gen: "
1200 DEFB 255
1210 ETI2: DEFM "ESP: Seguir"
1220 DEFB 255
1230 ETI3: DEFM "ESC: Salir"
1240 DEFB 255
10000 ; Datos a rellenar
10010 GEN0: DEFB 24 ; El primer byte indica la dimension
10020 DEFM "************************"
10030 DEFM "************************"
10040 DEFM "************************"
10050 DEFM "************************"
10060 DEFM "************************"
10070 DEFM "************************"
10080 DEFM "************************"
10090 DEFM "************************"
10100 DEFM "************************"
10110 DEFM "************************"
10120 DEFM "************************"
10130 DEFM "************************"
10140 DEFM "************************"
10150 DEFM "************************"
10160 DEFM "************************"
10170 DEFM "************************"
10180 DEFM "************************"
10190 DEFM "************************"
10200 DEFM "************************"
10210 DEFM "************************"
10220 DEFM "************************"
10230 DEFM "************************"
10240 DEFM "************************"
10250 DEFM "************************"
```
Que haga falta semejante cantidad de código solo para pintar una carátula fija en pantalla nos hace a la idea de lo laborioso que es programar en ensamblador. Estamos usando las instrucciones más básicas y contamos con un reducidísimo conjunto de registros para operar.
# Cálculo de adyacentes
Al ser la memoria una estructura lineal, con solo una coordenada (la dirección de memoria), la localización de las celdas adyacentes por arriba y por debajo requiere ciertos cálculos. Concretamente, siendo Dim la dimensión del tablero, las coordenadas virtuales en el tablero X e Y (empezando por cero hasta Dim-1), y siendo B la dirección base de la primera celda, el cálculo de la posición de memoria de una celda concreta sería:
`P = B + X + Y*Dim`
Esto nos obliga a hacer varios cálculos para encontrar cada celda. Siendo 8 celdas adyacentes, serían 8 multiplicaciones y 24 sumas para localizarlas a todas. Vamos entendiendo por qué el programa BASIC se hace tan lento. No olvidemos que el Z80 no tiene operaciones nativas de multiplicación ni división (exceptuando los desplazamientos de bits).
Para evitar este engorro, se podría mantener un puntero separado por cada fila implicada, e ir incrementando poco a poco. Como la máquina solo posee tres registros dobles, es casi imposible mantener esos punteros sin llevarlos a memoria RAM, lo cual penalizaría el rendimiento.
Las operaciones de suma y resta sí se pueden hacer directamente en los registros de la máquina, por lo que me gusta mucho más la solución de usar un único puntero, idealmente el HL, y hacer HL-Dim para la fila anterior y HL+Dim para la posterior. Aún tendré que acceder a RAM para leer el valor de Dim, pero siempre serán menos que guardando tres punteros.
Algo que me va a traer de cabeza es el caso particular de las celdas en los bordes. Concretamente, cuando estamos en la primera o última fila o columna hay que ignorar las celdas adyacentes por cada lado. Algo que me puede obligar a hacer muchas más comparaciones y cálculos de los deseados. En un lenguaje de alto nivel no sería mucho problema, pero con el reducísimo juego de registros que tenemos hay que buscar una solución más imaginativa.
Algo que se aprende con la experiencia al programar es que si las condiciones de tu algoritmo son demasiadas, lo que tienes que hacer es quitar condiciones de partida. En este caso, se me ocurre rellenar los bordes exteriores el tablero con celdas vacías. De esta forma, gastaremos más memoria para guardar el tablero, pero el algoritmo no tendrá que preocuparse por los bordes.
Para distinguir este borde del verdadero tablero, lo rellenaré con otro carácter distinto del espacio, pero que coincida en que el bit 1 lo tenga a cero. Así será indistinguible para el algoritmo. Escojo el guión (ASCII 2D, 00101101).
Otro lugar especial del borde es la casilla justo después de la última celda. Es el punto donde termina el tablero y hay que salir del bucle principal. Lo marco con un símbolo dólar, que también tiene el bit 1 a 0 (ASCII 24, 00100100).
En resumen, un tablero vacío de 7x7 se guardaría así:
```
- - - - - - - - -
- -
- -
- -
- -
- -
- -
- $
- - - - - - - - -
```
Así he conseguido que todas las celdas tengan ocho vecinos válidos y no tengo que preocuparme de los bordes. Además puedo detectar muy fácilmente si he llegado al final del tablero. A cambio, gasto `Dimensión*4-4` bytes extra en almacenar el tablero, pero vale la pena. Con este nuevo modelo de tablero, mi MSX me va a permitir hasta 23 generaciones simultáneamente en memoria.
## Rutina principal
La rutina SIGGEN es el corazón del programa, y he puesto abundantes comentarios para que se entienda lo que hace.
Como suele ser habitual, la mayor parte de los comandos son operaciones LD moviendo datos de unos registros a otros, y muchos PUSH y POP para liberar los registros, usarlos para otros cálculos y luego recuperar sus valores. Recordemos que el Z80 solo tiene tres registros de 16 bits (BC, DE y HL), y con eso tenemos que apañarnos para manejar las direcciones de memoria. En realidad hay otro juego de registros auxiliares, pero no se pueden usar simultáneamente, lo cual les resta utilidad.
Otra curiosidad es que no todas las operaciones existen para todos los registros, y hay que usar alternativas. Por ejemplo, para hacer `LD HL,DE` tendremos que hacerlo por parejas: `LD H,D` y `LD L,E`.
Tuve que lidiar además con un problema extra que me trajo de cabeza hasta que me di cuenta de lo que pasaba con los puñeteros saltos que no se hacían bien. Resultó que el ensamblador RSC II tiene un bug por el cual, a partir de cierto momento que no supe concretar, no calcula bien las etiquetas y las coloca un byte adelantadas. Lo solucioné incluyendo una operación vacía NOP en cada etiqueta. Además procuré ahorrarme todas las etiquetas posibles, haciendo los saltos cortos con direcciones relativas (por ejemplo, `JR Z,1` para el equivalente a un IF que se salta la siguiente instrucción).
Este es el código de la rutina SIGGEN:
```
SIGGEN: NOP ; Calculo de la siguiente generacion
; HL=Posicion siguiente generacion
; C=Dimension
; DE=Posicion actual generacion
LD (HL),C ; Copio dimension
; Los siguientes dimension+3 bytes del borde van con guiones
LD A,3
ADD A,C
LD B,A
LD A,"-"
BUCLE1: NOP
INC DE ; Menos eficiente que sumar de golpe, pero mas facil
INC HL
LD (HL),A
DJNZ BUCLE1
INC C ; C se queda con dimension+1 por conveniencia
; El bucle "duro" empieza ahora, donde hay que optimizar mas
BUCLE2: NOP
INC DE ; Apunta ahora a la celda actual
INC HL ; Apunta a la celda futura
; Si estamos en un guion, lo saltamos
LD A,(DE)
CP "-"
JR Z,BUCLE2
; Si estamos en un $, es el final
CP "$"
JR Z,FINAL
; Contar cuantos vecinos tiene DE
; Fila superior: restamos dimension+3
PUSH HL
LD H,D
LD L,E
DEC HL
DEC HL
OR A ; Limpiar carry para la resta
SBC HL,BC
; Comprobar las tres celdas de la fila superior cuantas de ellas
; tienen activado el bit 1
LD A,0
LD B,3
BUCLE3: NOP
BIT 1,(HL)
JR Z,1 ; Salto la orden INC A
INC A
INC HL
DJNZ BUCLE3
; Vecinos de la misma fila.
; Sumo dimension+1 (C) para el vecino derecho
ADD HL,BC
BIT 1,(HL)
JR Z,1
INC A
; Vecino izquierdo
DEC HL
DEC HL
BIT 1,(HL)
JR Z,1
INC A
; Fila siguiente (sumo dimension+2)
ADD HL,BC
INC HL
LD B,3
BUCLE4: NOP
BIT 1,(HL)
JR Z,1
INC A
INC HL
DJNZ BUCLE4
POP HL
; Ya tenemos el conteo en A y la futura celda en HL.
; Aplicamos las tres reglas de Conway.
; 1) Si tiene exactamente 3 vecinos vivos, la celda estara viva
CP 3
JR NZ,SALTO1
LD (HL),"*"
JR SALIDA
SALTO1: NOP
; 2) Si tiene 2 vecinos vivos y su estado actual es viva, seguira viva
CP 2
JR NZ,SALTO2
LD A,(DE)
BIT 1,A
JR Z,SALTO2
LD (HL),A
JR SALIDA
SALTO2: NOP
; 3) En cualquier otro caso, esta muerta
LD (HL)," "
SALIDA: NOP
JR BUCLE2
FINAL: NOP
; Copiar la fila de borde inferior
LD B,C
INC B
INC B ; B=dimension+3
SALTO3: NOP
LD A,(DE)
LD (HL),A
INC HL
INC DE
DJNZ SALTO3
; DE=nueva actual generacion
; HL=nueva siguiente generacion
RET
```
# Cargador BASIC
Para facilitar la introducción de la generación cero, he preparado un programa lanzador en BASIC, que carga el código máquina mediante etiquetas DATA, y tiene espacio para definir la dimensión y el tablero inicial.
Una vez cargado todo en memoria, utiliza el comando USR para lanzar el código.
```
CLEAR 200,&HA500
SCREEN 0
WIDTH 40
FOR D=&HA500 TO &HA6A1
READ X
POKE D,X
NEXT D
READ DI
PRINT "DIMENSION: "; DI
POKE &HA6A6,DI
D=&HA6A7
FOR P=1 TO DI+2
POKE D,ASC("-")
D=D+1
NEXT P
FOR LIN=1 TO DI
READ L$
PRINT L$
POKE D,ASC("-")
D=D+1
FOR P=1 TO DI
POKE D,ASC(MID$(L$,P,1))
D=D+1
NEXT P
IF LIN=DI THEN POKE D,ASC("$") ELSE POKE D,ASC("-")
D=D+1
NEXT LIN
FOR P=1 TO DI+2
POKE D,ASC("-")
D=D+1
NEXT P
DEFUSR=&HA500
X=USR(0)
DATA 62,0,205,95,0,205,204,0,46,1,38,26,205,198,0,33,96,166,205
DATA 187,165,46,2,38,26,205,198,0,33,106,166,205,187,165,46,3,38
DATA 26,205,198,0,33,115,166,205,187,165,46,5,38,26,205,198,0,33
DATA 131,166,205,187,165,46,7,38,26,205,198,0,33,138,166,205,187
DATA 165,46,8,38,26,205,198,0,33,151,166,205,187,165,33,0,0,34
DATA 163,166,33,166,166,229,0,46,5,38,31,205,198,0,42,163,166,205
DATA 198,165,42,163,166,35,34,163,166,225,229,205,137,165,209,205
DATA 238,165,213,205,159,0,254,27,32,219,209,201,0,126,79,6,0,9,17
DATA 4,0,25,17,0,0,229,197,213,245,0,205,92,0,241,225,1,40,0,9,84
DATA 93,193,225,9,35,35,229,197,213,61,245,254,0,32,231,241,209
DATA 193,225,9,35,201,0,126,254,255,200,35,205,162,0,24,246,0,1,240
DATA 216,205,224,165,1,24,252,205,224,165,1,156,255,205,224,165,14
DATA 246,205,224,165,14,255,0,62,47,0,60,9,56,252,237,66,205,162
DATA 0,201,0,113,62,3,129,71,62,45,0,19,35,119,16,251,12,0,19,35
DATA 26,254,45,40,249,254,36,40,76,229,98,107,43,43,183,237,66,62
DATA 0,6,3,0,203,78,40,1,60,35,16,248,9,203,78,40,1,60,43,43,203
DATA 78,40,1,60,9,35,6,3,0,203,78,40,1,60,35,16,248,225,254,3,32
DATA 5,54,42,24,17,0,254,2,32,9,26,203,79,40,4,119,24,4,0,54,32,0
DATA 24,170,0,65,4,4,0,26,119,35,19,16,250,201,0,74,85,69,71,79,32
DATA 68,69,255,0,76,65,32,86,73,68,65,255,0,112,111,114,32,69,108
DATA 80,97,109,112,108,105,110,97,255,0,71,101,110,58,32,255,0,69
DATA 83,80,58,32,83,101,103,117,105,114,255,0,69,83,67,58,32,83
DATA 97,108,105,114,255
'Dimension
DATA 24
'Celdas
DATA " "
DATA " "
DATA " "
DATA " "
DATA " "
DATA " "
DATA " "
DATA " "
DATA " "
DATA " "
DATA " "
DATA " "
DATA " "
DATA " "
DATA " "
DATA " "
DATA " "
DATA " "
DATA " "
DATA " "
DATA " "
DATA " "
DATA " "
DATA " "
```
# Resultado
=> img/juego-vida/vida-inicial.png Pantalla inicial con tres "organismos", uno cíclico y dos móviles.
=> img/juego-vida/juegodelavida.mp4 Evolución de esos organismos en 23 generaciones.
# Descargables
=> descargas/juego-vida.asm Programa completo en ensamblador (GPLv3).
=> descargas/juego-vida.wav Cargador BASIC en formato audio para CLOAD (GPLv3).
# Retos
Las siguientes mejoras le vendrían muy bien al programa:
- Guardar las generaciones cíclicamente para que se pueda hacer tantas generaciones como se quiera.
- Que se pueda volver atrás tantas veces como generaciones haya en memoria simultáneamente (idealmente hasta 23).
- Utilizar la pantalla en modo gráfico para poder representar tableros mucho más grandes y con mejor aspecto visual.
This content has been proxied by September (3851b).