Historia de CE4 [para ZX Spectrum +3e]

Descripción del contenido de la página

Historia del desarrollo de CE4, un juego de aventuras de texto con gráficos, escrito en Vimclair BASIC para ZX Spectrum +3e.

Etiquetas:

2014-08-02

Parto de la versión A-00-201407262101 del desarrollo para SAM Coupé (en MBim).

Primeros cambios para convertir el código de MasterBasic en lo que finalmente se llamaría Vimclair BASIC.

Los let múltiples son separados en varios.

Las sustituciones hechas con el comando #vim son movidas a un fichero propio.

2014-08-03

Para pasar los gráficos de ZX Spectrum creados en Debian a un fichero DSK compilo el programa TAP tools (John Elliott, 27 March 2011).

Tengo que crear un TAP intermedio, pues el paso directo a DSK, con mkp3fs, no sirve porque los ficheros contenidos en el DSK provocan error de tipo al intentar cargarlos en pantalla.


#!/bin/sh
# make_dsk.sh

# Este programa forma parte del proyecto
#   CE4
# escrito en SinBasic para ZX Spectrum +3e.

# Este programa crea una imagen de disquete DSK para ZX Spectrum +3, con las
# pantallas creadas en Debian, para su posterior tratamiento.

# 2014-08-03

cd scr
tapcat -N -a 16384 ../ce4_scr0.tap 0*.scr
tapcat -N -a 16384 ../ce4_scr1.tap 1*.scr
tapcat -N -a 16384 ../ce4_scr2.tap 2*.scr 3*.scr
cd -
tap2dsk -180 -label ce4_scr0 -type dsk ce4_scr0.tap ce4_scr0.dsk
tap2dsk -180 -label ce4_scr1 -type dsk ce4_scr1.tap ce4_scr1.dsk
tap2dsk -180 -label ce4_scr2 -type dsk ce4_scr2.tap ce4_scr2.dsk


Tras preparar los gráficos en tres disquetes DSK, creo una imagen de disco duro HDF para albergar el proyecto:

  hdfmonkey create ce4.hdf 2100K ce4

Después arranco Fuse con ZX Spectrum +3e e interfaz DivIDE, asigno el HDF al disco duro 0 y lo formateo con tres particiones como máximo (al parecer una la usa el sistema).

  format to 0,2

Creo las dos particiones, del mínimo tamaño posible, 1 MiB:

  data new "program",1
  data new "graphics",1

Con estos parámetros no sobran espacio ni particiones en disco.

Creada la subrutina wt_, equivalente al procedimiento original; sus llamadas son convertidas a una sintaxis específica con corchetes, para facilitar su sustitución con #vim.

Anulados todos los local.

Problema: Fuse no reconoce el disco IDE especificado con un parámetro en el comando de arranque del emulador, hasta después de haber reiniciado la máquina. Esto impide arrancar el emulador con la cofiguración adecuada al programa, el disco IDE y el disquete, y hacer que el programa DISK del disquete arranque a su vez el programa principal en el disco IDE.

Una alternativa sería usar una imagen de disquete de mayor tamaño:

Para probar, creo una imagen DSK de 720 KiB con el comando correspondiente de la multiherramienta Tap Tools:

mkp3fs -720 -label ce4 ce4720.dsk emptyfile.txt

Fuse lo lee y graba bien habiendo cambido el formato de la unidad B a 80 pistas y doble cara.

Pero hay un problema: Fuse no tiene un parámetro para cambiar el número de pistas y de caras de la unidad de disco; hay que hacerlo desde el menú. Esto impide que el arranque sea autmático. En todo caso, tras escribir un programa de prueba (programa del disco ) compruebo que la carga de ficheros en Fuse es dos o tres veces más lenta desde una imagen de disquete DSK que desde una imagen de disco duro HDF.

Escribo también el programa del disco para confirmar si los ficheros de atributos de todas las imágenes son idénticos, como parecía. Así es: papel blanco y tinta negra, sin brillo. Esto permite prescindir de esos 60 ficheros de 2 KiB.

2014-08-04

Fuse 1.1.1 arregla el fallo que impedía especificar un fichero HDF desde la línea de comandos.

convierto los if largos, recién implementados. queda por convertir los else if, que no están implementados; mientras, se podrían convertir en if anidados.

2014-08-05

adapto los else if, ya implementados, y traduzco exit if x a if x then exit do.

Hago muchos cambios hasta que BAS2TAP no da error. Más cambios debidos a errores de ejecución.

2014-08-06

Termino de separar el código que prepara las matrices de datos. Ahora un programa independiente creará las matrices y las grabará en disco. Así el programa principal ahorra memoria y tiempo de arranque. Todas las sustituciones hechas con #vim pasan también a un fichero propio, para que las usen ambos programas.

Modifico la rutina de entrada de textos y reescribo el analizador de comandos.

2014-08-07

Cambios, correcciones y mejoras diversos. Implementación de las funciones instr y trunc, escritas en Z80.

2014-08-08

Añado otra función es Z80, para guardar una cadena en memoria.

2014-08-09

Termino de implementar la rutina en Z80 de la función termn y adapto el código para usarla.

Reorganizo y mejoro las rutinas de impresión de párrafos justificados.

Arreglo los cálculos de las direcciones de carga de las rutinas en Z80, pues algunas se solapaban debido a una reordenación de los comandos.

2014-08-10

Tras modificar Vimprobable BASIC para que los comandos #vim se ejecuten antes de la limpieza del fichero, resultó que las comillas curvas usadas en CE4 para simular procedimientos con parámetros colisionaban, en las expresiones regulares de las sustituciones, con los caracteres especiales de BAS2TAP, que usan los mismos signos. Cambio las comillas curvas por rectas.

Para el selector de acciones implemento una simulación de on goto usando dos variables del sistema y el comando continue.

Implemento los GDU con los signos de puntuación y las letras propios del castellano.

2014-08-11

Ensamblo en un solo bloque todas las rutinas Z80 necesarias, junto con los GDU, para ahorrar tiempo de carga en el arranque y automatizar el cálculo de las direcciones. Hasta ahora había que modificar el programa principal cada vez que se modificaba una de las rutinas. Ahora, tomando el fichero de etiquetas creado por el ensamblador Pasmo, una instrucción del editor de línea sed lo convierte en un fichero de Vimclair BASIC con las directivas que harán las sustituciones de la fuente. Todo es automático.

Hay fallos variados en la rutina de entrada de textos, que aún es demasiado lenta (probablemente habrá que escribirla en ensamblador); en la impresión de textos (porque hay caracteres de control por ajustar); en el analizador de comandos; y el selector de acciones. Pero el flujo principal parece que funciona bien.

Modifico es sistema de carga de matrices: un nuevo programa herramienta las carga del disco y las graba en un programa en BASIC vacío. Esto permite que el programa principal cargue todos los datos con un comando MERGE, lo que es mucho más rápido.

Inicio la versión A-01.

Corrección: Las conversiones de 'isCarried' e 'isNotCarried' estaban intercambiadas.

Cambio: Elimino el identificador de acción inexistente 'noAction'; puede usarse un cero en su lugar, y es más rápido.

Problema: la rutina en ensamblador 'termn' no funciona bien.

2014-08-12

Arreglada la rutina en ensamblador, que se había estropeado debido a una optimización para ahorrar saltos. Arreglado también el problema con algunos identificadores de acción, los de movimiento, que estaban sin actualizar y cuyos valores eran diferentes en las cadenas de diccionario y en las sustituciones hechas en las fuentes.

Tras las últimas correcciones, ya se puede recorrer el mapa.

Versión A-02.

2014-08-13

El programa herramienta se renombra como ( en el disco de ZX Spectrum +3e).

Mejoras y correcciones en la rutina de entrada de comandos.

Primeros cambios para mejorar la presentación de las salidas de un escenario, presentándolas en un párrafo con su puntuación correcta, en lugar de en forma de lista.

2014-08-14

Activada la descripción de escenarios, a la que faltaba por implementar la estructura on goto simulada con continue.

Corrección: Las matrices nounNumber() y thingNoun() eran las dos traducidas como n(). Esto corrompía la lista de cosas presentes.

Corrección: Uso variables diferentes para la salida de la rutina de entrada de comandos y para la entrada de las rutinas de impresión. De otro modo podrían ocurrir cosas extrañas, como ya se veía en los mensajes de depuración.

2014-08-26

Corrección del tamaño de la ventana de depuración.

2014-08-30

Corrección del método de listado gradual de cosas presentes; el método de listado completo queda anulado, porque crea un pequeño retraso.

Títulos de crédito justificados; antes se imprimían con un simple print.

Corrección del cálculo del recuento de salidas de cada escenario. Hacían falta unos paréntesis en la expresión.

2014-08-31

Primeros cambios para implementar artículos y adjetivos posesivos a los nombres de las cosas.

2014-10-15

Cambio en el mapa: entradas conectadas entre sí. Baño desconectado del exterior.

Nuevo objeto: la montaña de libros en la biblioteca.

Corrección: asignación de códigos de tres GDU.

Mejora: es posible mirar en las direcciones cardinales. Aún Falta implementar las descripciones adecuadas.

Nuevo algoritmo para la escritura de comandos, más rápido. Además permite usar vocales acentuadas (con «symbol shift» y la vocal).

Arreglado el fallo que impedía volver a escribir un espacio tras haberlo borrado.

2014-10-16

La letra «ñ» también se puede obtener, con «symbol shift» y «n».

Nuevas acciones «entrar» y «salir»; mapa ampliado con las nuevas direcciones (que no son listadas en la descripción del escenario); si no hay dirección de salida explícita en el mapa, se usa como destino el escenario previo.

2014-10-17

Mejora: se muestra el comando del jugador, intercalado con el texto de salida y en diferente color; hasta ahora los comandos desaparecián de pantalla tras haberlos introducido.

Simplificación: el cursor no cambia de color cuando se pulsa la tecla de borrar y no más hay texto que borrar.

2014-10-19

Mejora: Al mirar en una dirección se indica el nombre del escenario de destino, o que no hay salida; si hay una puerta cerrada, se especifica también. Para esto ha habido que guardar los nombres de escenarios en una matriz, pues hasta ahora se imprimían directamente en su rutina de descripción.

2014-10-20

Diversas mejoras y novedades, entre las cuales:

2014-10-21

Nuevo: Se puede elegir el modo de juego: con gráficos o sin ellos. Se usan los comandos «gráficos sí», «gráficos no» o, para cambiar el modo, simplemente «gráficos». En lugar de «gráficos» se puede usar la abreviatura «g».

2014-12-10

La matriz location() ya no se crea durante la inicialización del juego sino en , como el resto de matrices. Esto permite asignar las localizaciones predeterminadas, por ejemplo de entes no manipulables. El juego solo debe cargar esta matriz de nuevo antes de cada partida y configurar los casos especiales, por ejemplo los entes cuya localización es aleatoria.

2014-12-13

Primera versión de un sistema de entes ambiguos, que comparten nombre y deben ser calculados en tiempo de ejecución. Esto permite crear varios objetos con el mismo nombre.

Mejoras en el módulo del verbo mirar.

Nuevo mensaje si el vigilante se mueve en alguna parte del pasillo cuando el protagonista está también en él.

2014-12-14

Simplificación de los datos: el género y número del nombre predeterminado de cada cosa se toma ahora de la lista de nombres. No hace repetirlos en la lista de cosas.

Ampliación del sistema de artículos para construir nombres. Nuevas funciones relacionadas con ello.

Mejora de las sustituciones de identificadores en . Con ayuda de una función en Vim, una directiva #vim modifica las sustituciones de los identificadores. Esto evita tener que rescribir a mano los valores cada vez que se añade o elimina un objeto o acción.

Ejemplo del método usado hasta ahora:


#vim %substitute,\<theAll\>,7,gi
#vim %substitute,\<theBooks\>,8,gi
#vim %substitute,\<theDiningTables\>,9,gi
#vim %substitute,\<theDoor\>,10,gi


Cómo se hace ahora:


#vim source ce4.common.enum.vim
#vim let b:enum=1
#vim /ThingIds\[/,/\]ThingIds/substitute,{enum},\=Enum(),gi
#vim " ThingIds[ "
#vim %substitute,\<theAll\>,{enum},gi
#vim %substitute,\<theBooks\>,{enum},gi
#vim %substitute,\<theDiningTables\>,{enum},gi
#vim %substitute,\<theDoor\>,{enum},gi
#vim " ]ThingIds "


Como se ve, el código se modifica a sí mismo: las directivas #vim deseadas toman los valores sucesivos adecuados antes de ser ejecutadas. De esta forma no hay que preocuparse por los valores utilizados en las sustituciones, y las listas de identificadores pueden ser actualizadas muy cómodamente.

El código de es:


let b:enum=0
function! Enum(...)
  " Return the current value of 'b:enum' and increase it.
  " If a parameter is given, store it into 'b:enum' first.
  if a:0 " Any parameter?
    let b:enum=a:1 " Use the first parameter
  endif
  let l:output=b:enum
  let b:enum=b:enum+1
  return l:output
endfunction


2014-12-15

Nuevo: muros exteriores del edificio, uno por escenario.

Nuevo: más formas de obtener el inventario, con mirar y examinar (no muy útiles, pero solo costaba unos pocos octetos implementarlas y era ilógico que la orden «mira inventario» diera un error).

2015-02-22

Corregido el fallo de cálculo de los entes ambiguos: había que aumentar el ajuste del índice del salto, de +3 a +4:

    @onAmbiguousGoto
    let void=fn dpoke(OLDPPC,@onAmbiguousGoto):\
    poke OSPPC,complement-firstAmbiguous+4:\
    continue:\
      // The 'goto' list must be in alphabetical order:
      goto @unAmbiguousTableX:\
      goto @unAmbiguousWallX

Reorganizo los directorios del proyecto y creo un fichero Makefile para empezar a automatizar la actualización del código; el paso final, actualizar el fichero HDF, aún hay que hacerlo casi manualmente, con ficheros TAP leídos en el emulador.

2015-02-25

El extraño fallo que mostraba los caracteres gráficos desplazados una línea hacia abajo se corrige volviendo a copiar el código desde el fichero TAP a la unidad C: de ZX Spectrum. Parece que se había corrompido ese fichero.

Actualizo el fichero Makefile para reunir en un fichero DSK los tres TAP principales y modifico el programa de arranque para instalar los programas en C: desde este disquete, más rápido que hacerlo manualmente desde los TAP.

Arreglo un fallo pendiente desde hace tiempo: al pasar una puerta cerrada con llave, la cerradura quedaba abierta solo en el sentido del movimiento, pero no desde en sentido contrario. La información de cada cerradura está duplicada en el mapa, lo que es más eficaz que usar una matriz adicional.

2015-02-26

Primeros cambios en el código para implementar el primer puzle del juego y la gestión de las pausas en los textos justificados.

Reescribí el fichero Makefile para que cree todo el contenido del fichero DSK, incluido el menú de arranque, también mejorado. Esto permite instalar o actualizar el programa en el disco duro de ZX Spectrum +3e desde el disquete, muy cómodamente.

2015-02-27

Mejoro los ficheros de arranque del disquete. Continúo la implementación del puzle y la gestión de pausas en la impresión de textos.

2015-02-28

Mejoras en la gestión de las ventanas de texto. Continúo con la gestión de pausas en la impresión de textos.

2015-03-01

Escribo las funciones fn_lookup16 y fn_lookup8 para mi librería DEFFNder. Estas funciones permiten usar tablas de selección en BASIC, por ejemplo para hacer mucho más rápida, con un solo salto de línea, la gestión de objetos en las acciones y otras estructuras de selección similares, en las que no es posible o conveniente hacer un cálculo, como el siguiente ejemplo:

  if currentLocation=theEntranceA then let complement=thePotA:exit proc
  if currentLocation=theEntranceB then let complement=thePotB:exit proc
  if currentLocation=theEntranceC then let complement=thePotC:exit proc
  if currentLocation=theEntranceD then let complement=thePotD:exit proc
  if currentLocation=theEntranceE then let complement=thePotE

Que con una tabla de selección que así:

  let complement=fn lookup8("\
    {theEntranceA}{thePotA}\
    {theEntranceB}{thePotB}\
    {theEntranceC}{thePotC}\
    {theEntranceD}{thePotD}\
    {theEntranceE}{thePotE}",currentLocation)

Pero surge un problema: +3e se reinicia al intentar mostrar en el editor esa cadena de caracteres no imprimibles empotrados. Compruebo que lo mismo ocurre con todos los modelos de ZX Spectrum 128, que tienen editor de pantalla completa.

Una solución es crear las tablas de decisión en memoria. Pero para no duplicar la información, debe hacerlo un programa herramienta. Otra solución es crear las cadenas en constantes, para que no salgan en el listado. Para ahorrar nombres de variables y memoria, una sola cadena podría contener todas las tablas de decisión necesarias.

2015-03-03

Cambios en el control del desbordamiento de texto en pantalla. A falta de una forma de saber el número de línea actual de una ventana, uso el número aproximado de caracteres que puede contener la ventana, con texto justificado.

Mejoras en los textos relacionados con el primer puzle.

La memoria libre es de unos 4 KiB al inicio del programa, tras cargar los datos desde el disco, y baja a solo 400 B durante el juego. El código de depuración ocupa unos 2 KiB. Hay que empezar a ahorrar memoria: puedo convertir algunas matrices numéricas en cadenas (el mapa, entre otras), y mover textos del programa a ficheros.

2015-03-04

Me estaba volviendo loco intentando averiguar por qué el valor de textWinChars era tres veces el esperado:

  let textWinChars=int(textWinHeight*textWinWidth*textWinCharSize/8*.75)

Al final descubrí que BAS2TAP, el conversor usado por defecto para el paso final de vbas2tap, tiene un fallo: no calcula bien la mantisa binaria de los números con punto decimal. Esto hace que su valor no coincida con su representación textual, a menos que la línea donde están sea editada posteriormente en el editor de ZX Spectrum, que lógicamente corrige el problema al volver a crear la representación interna de la línea a partir de su texto. Esto es un fallo grave que hay que comunicar al autor, Martjin van der Heide. El conversor alternativo zmakebas no tiene este fallo. Otra solución es usar la función val con los números decimales, algo que en todo caso había que hacer para ahorrar memoria.

Primera prueba para guardar los textos en ficheros: un programa herramienta crea un fichero con todos los textos, de esta forma:

open #txtFile,"o>c:texts.dat"
restore @texts
do
  read t$
  if t$=chr$ 0 then \
    close #txtFile: \
    exit proc
  print #txtFile;t$;
loop
stop

En el fichero, los párrafos están separados por retornos de carro, intercalados entre los data.

El siguiente paso fue crear rutinas alternativas de impresión que tomaran la cadena apuntada en el fichero. Desgraciadamente el comando input # aún no está bien implementado en ZX Spectrum +3e, y en su lugar hay que leer octeto a octeto con next #. De ello se encarga esta rutina:

@txt2p
  // Get the next string from the current position of the texts file
  let p$=""
  do
    next #txtFile,i:\
    if i=13 then return
    let p$=p$+chr$ i
  loop

Es problema es que este método es muy lento. Tarda varios segundos en obtener un párrafo. No sirve.

La segunda idea fue mejor: crear una matriz por cada texto y grabarla en el disco por anticipado, para que el programa principal la cargue solo cuando lo necesite. El nuevo programa herramienta que hace esta función es ce4.text_maker.vbas y su núcleo es:

#include ce4.fn_txtfile.vbas

defproc makeTexts

  save "c:":\
  restore @texts
  do
    let t$="":\ // new text
    read fileId:\
    if not fileId then exit proc
    do
      read i$:\
      if i$=chr$ 13 then exit do
      let t$=t$+(" " and len t$)+i$
    loop
    dim a$(len t$):\
    let a$=t$:\
    save fn txtFile$(fileId) data a$():\
    print ".";
  loop

endproc

Con ayuda de la directiva #vim es posible crear varios comandos, análogos a los que ya se usan para imprimir textos justificados, que lean el texto del disco a partir de su número de identificación; por ejemplo:

  _echoTxt_[1]

Las subrutinas correspondientes son puntos de entrada alternativos a las ya existentes:

@_echoTxt_
  // input: t (text id of a text file)
  load fn txtFile$(t) data a$():let p$=a$
@_echo_
  // Print a whole justified paragraph in the current window.
  // input: p$
  print #win;"{-8}{-8}";chr$ winJustificationOn;p$ // XXX TMP
  goto @possibleScroll

Por último, la función que convierte identificadores de texto en ficheros, que aún se podrá simplificar para ahorrar tiempo de proceso, si fuera necesario:

def fn txtFile$(n)=\
  // File name of the given text id
  "txt"+str$ n+".dat"

La velocidad de ejecución es buena, más que suficiente para la impresión de textos, sobre todo porque en la mayor parte de los casos se imprimen párrafos enteros.

Traduzo las interfaces de los programas herramienta y del menú de arranque al español.

2015-03-05

Primeras pruebas de descripciones de escenario cortas y largas, dependiendo de si se trata de la primera visita.

2015-03-06

Escribo una función que devuelve la línea de impresión, en coordenadas de pantalla, de una ventana de texto, extraída del área de información de canales del sistema, gracias a la información aportada por el autor de ZX Spectrum +3e, Garry Lancaster:

deffn channel(n)=\
  // Address of a channel information area
  fn dpeek(CHANS)+fn dpeek(STRMS+2*(n+3))-1:\

deffn winLine(n)=\
  // Screen current line position of a window (0-23)
  peek (fn channel(n)+18)/8:\

Sin embargo, tampoco es suficiente para controlar el flujo de texto en las ventanas, pues éste se imprime por bloques, lo que impide el cálculo preciso por líneas. Una solución definitiva sería usar una rutina mía de justificación de textos, en lugar de la incorporada en las ventanas del sistema.

2015-03-08

Escribo varias alternativas al sistema interno de impresión justificada de textos, pero ninguna es adecuada, por diversos motivos. La última alternativa examina la cadena letra a letra, en lugar de por bloques, y es demasiado lenta:

@jprint

  // This method works fine only with 8-pixel width characters; and it's too slow.

  if not fn winFreeCols() then print #win
  let t$=t$+" "
  let textLength=len t$
  let textStart=1
@jprintLine
  if textStart>=textLength then return
  for i=textStart to textLength
    if t$(i)=" " then
      if i-textStart=fn winFreeCols() then
        print #win;t$(textStart to i-1); // without the trailing space
      else
        if i-textStart>fn winFreeCols() then print #win // new line
        print #win;t$(textStart to i); // with the trailing space
      endif
      let textStart=i+1:\
      gosub @possibleScroll:\
      goto @jprintLine
    endif
  next i
  return