elpamplinadecai@gmail.com
@ElPamplina@masto.es
Publicado el 18-5-2024
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).
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.
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.
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).
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.
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.
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.
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.
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!!
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:
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.
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.
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.
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:
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.
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.
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.
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
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 " "
Pantalla inicial con tres "organismos", uno cíclico y dos móviles.
Evolución de esos organismos en 23 generaciones.
Programa completo en ensamblador (GPLv3).
Cargador BASIC en formato audio para CLOAD (GPLv3).
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.