Finen per Imago

Descripción del contenido de la página

Juego de aventuras de texto escrito en Z80 para ZX Spectrum.

Proyecto durmiente. Iniciado en 1986-10-04. 60% completado.

Etiquetas:

Este proyecto de aventura conversacional es mi programa inacabado favorito, por el desafío que supuso su programación, por el tiempo que le dediqué (probablemente varios miles de horas, entre 1986 y 1994), y por la calidad técnica de la parte que está terminada.

El programa está escrito íntegramente en ensamblador de Z80, comentado con todo detalle línea a línea (más de 700 KiB de código fuente). Empecé a trabajar con el programa ensamblador GENS3 y cintas de casete; después seguí con una versión modificada del mismo programa, que funcionaba con cintas de microdrive; más tarde usé la interfaz de disquetes DISCiPLE. Y pronto, cuando el código fuente se hizo demasiado grande para trabajar en él con la ZX Spectrum, escribí mi propio ensamblador cruzado de Z80 para la QL, lo que fue un gran esfuerzo pero supuso un verdadero salto cuántico en el desarrollo.

El título

Sacred Heart

Originalmente el nombre del programa fue «Finis per Somnium», en latín, tomado de la portada del disco Sacred Heart de Ronnie James Dio (imagen tan inspiradora como su música, y que muestro aquí al lado). Cuando le di al esperanto un pequeño pero clave papel en la trama de la aventura, traduje el título a este idioma: «Finen per Imago».

La trama

La acción tiene lugar en un tiempo futuro en que los humanos han colonizado nuevos mundos en otros sistemas estelares. Imago es el nombre de un planeta conocido por los humanos, que tiene las siguientes características:

Por ello, para evitar el previsible daño causado por el choque de civilizaciones tan diferentes, los humanos, cuya conciencia ha despertado por fin, han evitado respetuosamente todo contacto con los habitantes de Imago. Pero un día una nave terráquea monoplaza, en misión rutinaria de reconocimiento, toma tierra en el planeta debido a una avería, en una zona desértica en la que sin embargo vive alguien... el protagonista de la aventura. Misión última: ayudar al visitante a reparar su nave, lo que no es baladí porque para empezar no solo no podemos comunicarnos con él (él sí con nosotros), sino que nos será difícil creer que aquel ser tan extraño no es nuestro enemigo.

Como se ve, la trama es una mezcla de algunos aspectos de la película «Planet 51» (sí, creo que los guionistas me robaron la idea telepáticamente), y de otros de la serie «V» pero al revés, ambientado todo ello en un entorno medieval en el que irrumpe un visitante procedente de un mundo más avanzado, al modo de la obra de Mark Twain Un yanqui en la corte del rey Arturo.

Pantallazos

Muestro algunos ejemplos de las pruebas realizadas con la última versión operativa del programa. En alguna de ellas incluso se imprimen textos que son marcas de depuración de errores.

Nunca decidí si el juego tendría gráficos. De tenerlos, cada uno sería un tercio de pantalla, en blanco y negro, comprimida en disquete de DISCiPLE. Hice un diseño alternativo sin gráficos, en que el pergamino ocupaba toda la pantalla.

Para las pruebas utilicé un borrador de gráfico común a todos los escenarios; solo cambiaba el color de la tinta y el número.

Finen per ImagoFinen per ImagoFinen per ImagoFinen per ImagoFinen per ImagoFinen per ImagoFinen per ImagoFinen per Imago

Características técnicas

Las características más destacadas del programa son las siguientes:

Código fuente

Mi intención es publicar el código fuente aun sin haber terminado el programa, pues creo que algunos de los algoritmos podrían ser útiles a alguien. De momento muestro las cabeceras de dos de los módulos principales, la base de datos y el analizador, la descripción de cuyos datos y parámetros permite hacerse una idea de su funcionamiento.

La base de datos

; datos_fijos_habitantes.asm

; Datos fijos de habitantes

;TABLA DE LOS DATOS DE LOS NOMBRES DE LOS HABITANTES

;Para elemento n:
;+0 dirección del nombre en el vocabulario
;+2 primer habitante que tiene el nombre n
;+3 número de habitantes con el nombre n

; datos_habitantes.asm

; DATOS DE HABITANTES
; ===================

;TABLA DE HABITANTES

;+0  flags
;    bits
;    0 ¿habitante INTERACTIVO?
;    1 ¿descrito antes?
;    2 ¿manipulado o conocido por el protagonista?
;    3 ¿vegetal?
;    4 ¿la localización es un habitante en lugar de un escenario?
;    5 ¿habitante ANIMADO? (todos los interactivos son animados)
;    6 ¿habitante CONTENEDOR? (sólo pueden ser contenedores los no animados)
;    7 ¿masculino?
;+1  número del adjetivo, o bien 255 si no necesita adjetivo
;+2  localización, o bien 0 (y el bit 4 de +0 reseteado) para indicar que no existe
;+3  peso (lo que abulta el llevarlo)
;    (si el peso es 255 es que se trata de un decorado, un habitante que no puede cogerse)
;+4  en animado:               fortaleza
;    en animado no-interactivo:además, alimento al morir, pues entonces se hace comestible
;    en animado interactivo:   además, peso máximo del inventario
;    en contenedor:            capacidad, peso máximo del contenido
;    en habitáculo:            dirección de entrada
;    en global:                tipo de escenario en que está (según +14 de TABESC)
;    en arma disparable:       habitante con que está cargado
;    en carga de disparable:   habitante al que puede cargar
;    en bebible y comestible:  alimento, fortaleza que suma a quien se lo come o bebe (o resta si es venenoso)
;    en encendible:            duración encendido
;+5  número del nombre (para hallar su dirección en TABNOM)
;+6  en no-animado:            resistencia, fortaleza material
;+7  dirección de la descripción
;+9  en interactivo:           peso del inventario
;    en contenedor:            peso del contenido
;    en habitáculo:            escenario al que conduce si entramos en él (y en el que hay que estar para salir)
;+10 bits
;    0 ¿ropa?
;    1 ¿abrible?
;    2 ¿habitante HABITÁCULO?
;    3 ¿leíble?
;    4 ¿comestible?
;    5 ¿bebible/líquido?
;    6 en bebible/comestible:  ¿venenoso?
;      en abrible:             ¿abrible con llave?
;    7 ¿encendible, fuente de luz? (todo encendedor es encendible)
;+11 bits
;    0 en ropa:                ¿puesto?
;      en abrible con llave:   ¿cerrado con llave? (si está cerrado)
;      en animado:             ¿atacado por el protagonista?
;    1 en abrib./cont./habit.: ¿abierto?
;                              (ha de emplearse tanto en contenedores como habitáculos, aunque no sean abribles)
;    2 ¿arma disparable?
;    3 en leíble:              ¿leído?
;      en encendedor:          ¿usado?
;    4 ¿arma blanca?
;    5 en arma disparable:     ¿cargado?
;      en animado:             ¿urgencia de movimiento?
;    6 ¿carga de disparable?
;    7 ¿habitante GLOBAL?
;+12 bits
;    0 ¿dañado por disparo?
;    1 en encendib./encended.: ¿encendido?
;    2 ¿dañado por arma blanca?
;    3 en contenedor:          ¿rígido?
;      en encendible:          ¿se destruye al agotarse?
;    4 en no-decorados:        ¿flota? (esto indica si se hunde o no al dejarlo en el lago o en el pantano)
;      (no-decorados son habitantes que pueden cogerse, es decir, que pesan menos de 255)
;    5 ¿apoyo?, ¿se pueden DEJAR cosas en él? (se dejan en el escenario, de momento, y se usa sólo en globales)
;    6 ¿sirve como llave en algún caso?
;    7 ¿encendedor? (por ejemplo, y más bien exclusivamente, cerilla)
;+13 en abrible con llave:     habitante con que se abre y cierra
;+14 bits
;    0 ¿debe ignorarse al describirse un escenario?
;    1 ¿debe evitarse mensaje evasivo en el primer  nivel de descripción del verbo EXAMINAR? (bit de rareza)
;    2
;    3
;    4
;    5
;    6
;    7

El analizador lingüístico

La primera parte es la rutina de entrada de textos:

;ANALIZADOR (I) ENTRADA

;Entrada
;  bit 0 de (IY-FLGS0)=¿queda frase por traducir?
;Local
;  HL=dirección en la zona FRASE del próximo carácter a guardar
;  B =contador de caracteres introducidos
;Salida
;  bit 2 de (IY-FLGS0)=retorno por agotamiento de pausa en espera de tecla, en lugar de por pulsar ENTER
;  (FRASE...)=frase tecleada por el jugador
;  B=número de caracteres de que consta la frase
;  HL=dirección siguiente a la del último carácter de la frase tecleado por el jugador

El segundo paso consiste en traducior el texto en los códigos de las palabras reconocidas en el vocabulario, con su tipo y su número:

;ANALIZADOR (II) TRADUCCIÓN

;Entrada
;  (FRASE...)=frase tecleada por el jugador
;  (FRASE)<>" ", es decir, que nunca empezará una frase con un espacio
;  (FRASE)<>44, es decir, que nunca empezará una frase con una coma
;  B=número de caracteres de que consta la frase
;  B<>0, es decir, siempre habrá algo en la frase
;  HL=dirección siguiente a la del último carácter de la frase tecleado por el jugador
;Salida
;  (ORDEN+n+0)=tipo de la palabra n
;  (ORDEN+n+1)=número de palabra n
;  IX=final+1 de la zona de la orden
;  (NUMPAL)=contadores de palabras actualizados

El tercer paso consiste en interpretar las palabras reconocidas y extraer de ellas los elementos de un comando, si no hay errores gramaticales:

;ANALIZADOR (III) INTERPRETACIÓN

;Entrada
;  Salida de TRADUC
;Local
;  B=investigación en curso y otros flags
;    bits
;    6 investigando objeto
;    4 investigando complemento
;    0 la última palabra reconocida fue un adverbio
;  C=palabras encontradas y reconocidas en la orden
;    bits
;    7 verbo
;    6 objeto
;    5 adjetivo del objeto
;    4 preposición
;    3 complemento
;    2 adjetivo del complemento
;    0 adverbio
;Salida
;  (VERBO...ADVERB)=palabras reconocidas
;  C=idem C Local
;  (SINTAX)=idem C Local
;  (ESTVER...)=datos del verbo traídos desde TABVER

El cuarto paso consiste en «nombrar» los elementos encontrados en el papel de objeto y/o complemento preposicional, eligiendo el adecuado de la base de datos en función del adjetivo indicado, si es el caso, y el nivel de accesibilidad exigido por el verbo. Este paso permite interactuar con objetos homónimos sin necesidad de especificar el adjetivo que los diferencia salvo que sea necesario.

;ANALIZADOR (y IIII) NOMBRA

;Entrada
;  Salida de INTERP
;Salida
;  (OBJETO),(COMPLE)=habitantes concretos según sus nombres y posibles adjetivos

Este módulo se divide en varias rutina interesantes:

;INVESTIGAR UN HABITANTE CONCRETO, BIEN EL OBJETO O EL COMPLEMENTO DE LA ORDEN

;Entrada
;  HL=dirección del nombre del habitante, OBJETO o COMPLE
;  HL+1=ADJOBJ o ADJCOM, evidente y respectivamente
;  (HL)=nombre del habitante (número de su nombre en el vocabulario, que sirve para hallar sus datos en TABNOM)
;  (HL+1)=adjetivo del habitante, si es que lo lleva en la frase
;  A=¿existe adjetivo del nombre en la frase? (si es distinto de cero es que sí hay adjetivo)
;  bit 4 de (IY-FLGS0)=¿estamos estudiando el complemento? (si no, es el objeto, claro)
;Local
;  E=¿existe adjetivo del nombre en la frase? (si es distinto de cero es que sí hay adjetivo)
;  C=posible adjetivo (si no hay adjetivo, no cuenta, claro)
;  B=contador habitantes con ese nombre
;  A=número del habitante en estudio
;Salida
;  (HL)=habitante concreto más probable al que se refiere la orden (si no es posible hallarlo, error)

;BUSCAR UN HABITANTE, DE ENTRE LOS QUE TIENEN ESE NOMBRE, QUE TENGA EL MISMO ADJETIVO QUE EL DE LA ORDEN

;Entrada
;  A=número del primer habitante en TABHAB con ese nombre
;  B=número de habitantes en TABHAB que tienen ese nombre
;  C=adjetivo a buscar

;RUTINA PARA BUSCAR UN HABITANTE, DE ENTRE LOS QUE TIENEN ESE NOMBRE, QUE TENGA EL ADJETIVO C

;Entrada
;  A=número del primer habitante en TABHAB con ese nombre
;  B=número de habitantes en TABHAB que tienen ese nombre
;  C=adjetivo a buscar
;Local
;  H=número de habitante en curso

;BUSCAR HABITANTE, DE ENTRE LOS QUE TIENEN ESE NOMBRE, AL QUE LA ORDEN PUEDA REFERIRSE CON MAYOR PROBABILIDAD
;(puesto que en la orden no se especifica adjetivo alguno)

;Entrada
;  A=número del primer habitante en TABHAB con ese nombre
;  B=número de habitantes en TABHAB que tienen ese nombre

;BUSCAR HABITANTE CON ESE NOMBRE QUE ESTÉ EN EL ESCENARIO ACTUAL, PERO NO EN EL INVENTARIO

;Entrada
;  (PRIHAB+1)=número del primer habitante con ese nombre
;  (HABNOM+1)=número de habitantes que tienen ese nombre
;Local
;  A=habitante en estudio
;  B=habitantes que quedan por estudiar
;  C=indicador de prioridad de las condiciones de búsqueda (0=máxima prioridad)

;BUSCAR HABITANTE CON ESE NOMBRE QUE ESTÉ EN EL INVENTARIO

;Entrada
;  (PRIHAB+1)=número del primer habitante con ese nombre
;  (HABNOM+1)=número de habitantes que tienen ese nombre
;Local
;  A=número de habitante en estudio
;  B=habitantes que quedan por estudiar
;  C=indicador de prioridad de las condiciones de búsqueda (0=máxima prioridad)
;  E=para preservar A

;BUSCAR OBJETO CON ESE NOMBRE DENTRO DEL COMPLEMENTO

;Entrada
;  (PRIHAB+1)=número del primer habitante con ese nombre
;  (HABNOM+1)=número de habitantes que tienen ese nombre
;Local
;  A=habitante en estudio
;  B=habitantes que quedan por estudiar
;  C=indicador de prioridad de las condiciones de búsqueda (0=máxima prioridad)
;  E=para preservar A

Errores

El analizador lingüístico informa del error exacto que encuentra, como se ve en la tabla de errores:


;TRATAMIENTO DE ERRORES POSIBLES DURANTE LA INTERPRETACIÓN

ERRI14 CALL ERRORI
       DEFB 0                  ;"nombre"
       DEFM "y"
       DEFB 15                 ;"preposición"
       DEFB 1                  ;"no"
       DEFB 14                 ;"concuerdan en género"
       DEFB 255
ERRI00 CALL ERRORI
       DEFB 1                  ;"no"
       DEFB 2                  ;"reconozco"
       DEFB 3                  ;"ningún"
       DEFB 4                  ;"verbo"
       DEFB 255
ERRI01 CALL ERRORI
       DEFB 2                  ;"reconozco"
       DEFB 5                  ;"más de un"
       DEFB 4                  ;"verbo"
       DEFB 255
ERRI02 CALL ERRORI
       DEFB 2                  ;"reconozco"
       DEFM "m#s de un"
       DEFB "a"+128
       DEFB 15                 ;"preposición"
       DEFB 255
ERRI03 CALL ERRORI
       DEFB 2                  ;"reconozco"
       DEFB 5                  ;"más de un"
       DEFB 0                  ;"nombre"
       DEFB 7                  ;"principal"
       DEFB 255
ERRI04 CALL ERRORI
       DEFB 2                  ;"reconozco"
       DEFB 5                  ;"más de un"
       DEFB 0                  ;"nombre"
       DEFB 9                  ;"de"
       DEFB 8                  ;"complemento"
       DEFB 255
ERRI05 CALL ERRORI
       DEFB 2                  ;"reconozco"
       DEFB 5                  ;"más de un"
       DEFB 11                 ;"adjetivo"
       DEFB 13                 ;"del"
       DEFB 0                  ;"nombre"
       DEFB 7                  ;"principal"
       DEFB 255
ERRI06 CALL ERRORI
       DEFB 2                  ;"reconozco"
       DEFB 5                  ;"más de un"
       DEFB 11                 ;"adjetivo"
       DEFB 13                 ;"del"
       DEFB 0                  ;"nombre"
       DEFB 9                  ;"de"
       DEFB 8                  ;"complemento"
       DEFB 255
ERRI07 CALL ERRORI
       DEFB 11                 ;"adjetivo"
       DEFB 9                  ;"de"
       DEFB 0                  ;"nombre"
       DEFB 7                  ;"principal"
       DEFB 12                 ;"tras"
       DEFB 6                  ;"adverbio"
       DEFB 255
;RRI08 CALL ERRORI
;      DEFB 11                 ;"adjetivo"
;      DEFB 9                  ;"de"
;      DEFB 0                  ;"nombre"
;      DEFB 9                  ;"de"
;      DEFB 8                  ;"complemento"
;      DEFB 12                 ;"tras"
;      DEFB 6                  ;"adverbio"
;      DEFB 255
ERRI09 CALL ERRORI
       DEFB 1                  ;"no"
       DEFB 2                  ;"reconozco"
       DEFB 3                  ;"ningún"
       DEFB 0                  ;"nombre"
       DEFB 7                  ;"principal"
       DEFB 255
ERRI10 CALL ERRORI
       DEFB 1                  ;"no"
       DEFB 2                  ;"reconozco"
       DEFB 3                  ;"ningún"
       DEFB 0                  ;"nombre"
       DEFB 9                  ;"de"
       DEFB 8                  ;"complemento"
       DEFB 12                 ;"tras"
       DEFB 15                 ;"preposición"
       DEFB 255
ERRI11 CALL ERRORI
       DEFB 1                  ;"no"
       DEFB 2                  ;"reconozco"
       DEFB 3                  ;"ningún"
       DEFB 0                  ;"nombre"
       DEFB 7                  ;"principal"
       DEFB 10                 ;"antes"
       DEFB 13                 ;"del"
       DEFB 6                  ;"adverbio"
       DEFB 255
ERRI12 CALL ERRORI
       DEFB 1                  ;"no"
       DEFB 2                  ;"reconozco"
       DEFB 3                  ;"ningún"
       DEFB 0                  ;"nombre"
       DEFB 9                  ;"de"
       DEFB 8                  ;"complemento"
       DEFB 10                 ;"antes"
       DEFB 13                 ;"del"
       DEFB 6                  ;"adverbio"
       DEFB 255
ERRI13 CALL ERRORI
       DEFB 2                  ;"reconozco"
       DEFB 5                  ;"más de un"
       DEFB 6                  ;"adverbio"
       DEFB 255
ERRI15 CALL ERRORI
       DEFB 2                  ;"reconozco"
       DEFB 5                  ;"más de un"
       DEFB 11                 ;"adjetivo"
       DEFB 255
ERRI16 CALL ERRORI
       DEFB 1                  ;"no"
       DEFB 2                  ;"reconozco"
       DEFB 3                  ;"ningún"
       DEFB 0                  ;"nombre"
       DEFB 7                  ;"principal"
       DEFB 10                 ;"antes"
       DEFB 13                 ;"del"
       DEFB 4                  ;"verbo"
       DEFB 255
ERRI17 CALL ERRORI
       DEFB 1                  ;"no"
       DEFB 2                  ;"reconozco"
       DEFB 3                  ;"ningún"
       DEFB 0                  ;"nombre"
       DEFB 9                  ;"de"
       DEFB 8                  ;"complemento"
       DEFB 10                 ;"antes"
       DEFB 13                 ;"del"
       DEFB 4                  ;"verbo"
       DEFB 255
ERRI18 CALL ERRORI
       DEFB 0                  ;"nombre"
       DEFB 7                  ;"principal"
       DEFM "y s"
       DEFB "u"+128
       DEFB 11                 ;"adjetivo"
       DEFB 1                  ;"no"
       DEFB 14                 ;"concuerdan en género"
       DEFB 255
ERRI19 CALL ERRORI
       DEFB 0                  ;"nombre"
       DEFB 9                  ;"de"
       DEFB 8                  ;"complemento"
       DEFM "y s"
       DEFB "u"+128
       DEFB 11                 ;"adjetivo"
       DEFB 1                  ;"no"
       DEFB 14                 ;"concuerdan en género"
       DEFB 255

Estado y futuro del proyecto

El desarrollo principal tuvo lugar entre 1986 y 1992, con algunos breves retoques en 1994. Desde entonces el proyecto durmió durante 10 16 años (sí, en hexadecimal; o lo que es lo mismo, 10000 2 años en binario).

En 2010-03 hice los cambios necesarios en los ficheros fuente originales (que compilaba en su día en QL con QL80) y logré compilarlos con Pasmo. Después de tanto tiempo, fue como ver resucitar el programa.

Para reiniciar el desarrollo tendría que encontrar aún el origen de algunos fallos de ejecución nuevos (probablemente fruto de las diferencias entre los evaluadores de expresiones de los dos ensambladores) y familiarizarme de nuevo con las fuentes.

Pero es probable que nunca continúe el desarrollo de este programa. Con otros proyectos más inmediatos en lenguajes de alto nivel, el esfuerzo de volver a trabajar en Z80 sería desproporcionado al objetivo. En su momento Z80 era para mí la única opción para programar ciertos algoritmos muy complejos que pudieran funcionar muy rápidamente en una ZX Spectrum real; además de para poder comprimir gran cantidad de información en los 48 KiB de la máquina. Actualmente ni la velocidad ni la memoria son condicionantes para obtener el mismo resultado o mejor en una plataforma más moderna y en un lenguaje de alto nivel. Incluso existen alternativas más cómodas para programar algo similar para ZX Spectrum.

En 2011 consideré las siguientes opciones para dar continuidad al proyecto:

En 2015 la lista de opciones para retomar el proyecto es muy diferente:

En 2015-02 empecé una transformación profunda de las fuentes: modernización de la «maquetación» del código para hacerlo más legible y facilitar el desarrollo con Vim, renombrado de las etiquetas con nombres largos y significativos... Quizá cuando lo vea todo preparado no parezca tan descabellado volver a retomar el desarrollo donde se quedó.

En cualquier caso, como he mencionado, publicaría las fuentes completas en Z80, por si a alguien le son de utilidad.

Páginas relacionadas

QL80
Ensamblador de Z80 escrito en SuperBASIC para la Sinclair QL.
GENS3 con Beta Disk
Programa en Sinclar BASIC para usar la unidad de disco Beta Disk con el programa GENS3.