La legionela del pisto

Descripción del contenido de esta página

Proyecto de aventura conversacional para ZX Spectrum 128, escrita en Beta BASIC 4.0+D con formato BBim.

Iniciado en 2010-04-29.
Proyecto detenido.
40% completado.

La idea de iniciar este proyecto, una aventura conversacional para ZX Spectrum, surgió cuando, tras publicar Colegio Erevest, se me ocurrió escribir una nueva versión mejorada, con la perspectiva de los 25 años transcurridos y, por supuesto, con nuevas herramientas. En principio iba a ser solo una versión reescrita por completo para ponerla al día técnica y gráficamente, y de paso añadirle algunas novedades de contenido, pero pronto adquirió forma propia e independiente: nuevo argumento, nuevos escenarios (con gráficos a pantalla completa digitalizados en blanco y negro) y un ambiente de misterio y humor muy prometedor. Aun así guardará cierta relación con Colegio Erevest: será una especie de secuela tácita.

Herramientas de desarrollo

Empecé a escribir el programa en abril de 2010 en el novedoso lenguaje ZX BASIC (para ZX Spectrum), simultaneando el trabajo con una versión alternativa en el veterano Beta BASIC 4.0+D (para ZX Spectrum 128) para comparar los pros y contras de cada uno, pues no estaba muy familiarizado con ninguno de ellos.

Tras quince días de trabajo opté por Beta BASIC 4.0+D, por varios motivos:

En ZX BASIC todas esas funcionalidades tendría que escribirlas yo mismo desde cero (en el propio lenguaje o en ensamblador de Z80, que puede mezclarse de forma transparente con él). Las mayores ventajas de ZX BASIC son que es un lenguaje moderno, compilado, y en el que se puede trabajar sin depender del emulador de ZX Spectrum salvo para comprobar el resultado final.

Tras unos meses de desarrollo, cuando el proyecto ya estaba maduro, confirmé que la decisión había sido acertada: La ventaja de poder usar cómodamente el disco RAM de la ZX Spectrum 128, los 780 KiB de espacio del disquete de la interfaz Plus D, y sobre todo la versatilidad y robustez del lenguaje Beta BASIC 4.0+D compensaban con creces las limitaciones (por otra parte encantadoras) de tener que escribir el código en el entorno del emulador, casi como si usáramos la máquina original.

No obstante en 2011-09 convertí el código fuente para poder programar con BBim. Esto facilitó mucho el desarrollo a partir de 2012.

Las herramientas gráficas que estoy usando son GIMP y SevenuP.

El reto de Beta BASIC

Los principales retos que enfrento en este proyecto son la velocidad de ejecución y la memoria disponible; si no, no sería un auténtico «retro-reto». La velocidad de ejecución puede ser aumentada por el emulador, y de hecho el juego será inmanejable salvo que se aumente la velocidad al menos a un 1000%, pero la memoria es un grave problema con Beta BASIC.

Beta BASIC deja apenas 20 KiB libres en la ZX Spectrum. Por suerte ofrece la posibilidad de usar el disco RAM de la ZX Spectrum 128 para guardar matrices de datos que pueden ser manipuladas allí directamente, como si se tratara de ficheros de datos. Sin embargo eso no soluciona todo: de algún modo hay que pasar los datos a esas matrices antes del inicio del programa principal. No hay memoria suficiente para tener los datos en líneas DATA y pasarlos de ahí a las matrices-ficheros.

Por tanto, la primera solución fue escribir un programa de inicio que tomara los datos de líneas DATA y creara las matrices. En realidad hace falta un programa para cada matriz, pues los datos son muchos y un solo programa no deja memoria libre para tenerlos todos en DATA. Este método llega a tardar hasta cuarenta minutos (al 100% de velocidad de la máquina original) en leer, contar, procesar, preparar, indizar y guardar los datos en las matrices-ficheros.

La segunda solución fue convertir estos programas que preparan los datos en programas herramienta que hicieran la misma tarea pero guardaran el resultado en el disquete. Así, el programa principal tan solo tendría que copiar los ficheros desde el disquete al disco RAM. Beta BASIC no ofrece una forma directa de copiar matrices-ficheros desde el disco RAM al disquete y luego a la inversa, pero puede hacerse mediante la apertura o creación previas del fichero en disquete con OPEN y el uso de LIST o INPUT en cada caso, como en los siguientes procedimientos:


DEF PROC ramToDisk f$
  CLOSE #*4
  OPEN #4;d*;f$RND
  LIST #4;!f$
  CLOSE #*4
END PROC 

DEF PROC diskToRam f$
  eraseRD f$
  CLOSE #*4
  OPEN #4;d*;f$RND
  INPUT #4;!f$
  CLOSE #*4
END PROC 

En un artículo ya he escrito sobre las fortalezas y debilidades de Beta BASIC, de modo que no las repetiré aquí. Baste decir que las debilidades del lenguaje hacen de este proyecto un reto muy interesante, y sus fortalezas un reto muy atractivo.

En el fondo el objetivo del proyecto, aparte del reto, no puede ser más nostálgico y más retro: escribir un programa con las mismas herramientas que hubiera podido usar en 1987, si aparte de la interfaz DISCiPLE hubiera tenido la versión 128 de la ZX Spectrum y Beta BASIC 4.0+D (que ya existían entonces). No incluyo las herramientas gráficas; mi pasión por la programación retro no llega al extremo de crear gráficos con programas de diseño para ZX Spectrum...

Pantallazos

Por supuesto los textos y los gráficos son provisionales, pero los siguientes pantallazos (tomados en noviembre de 2010) permiten hacerse ya una idea del aspecto que tendrá el programa:

Entrada Pasillo Texto Sala de visitas Sala de visitas

Listados

Hasta la conclusión y publicación del programa completo, mostraré solamente algunos ejemplos del código. Los extractos siguientes corresponden al estado del programa en 2011-03, antes de migrar al formato BBim. Hay muestras de código más reciente en el historial de desarrollo.

Preparación de parte de los datos

Un ejemplo de las operaciones que hay que realizar para preparar los datos anticipadamente en el formato adecuado, para que el programa principal no tenga que hacer otra cosa que copiar las matrices-ficheros desde el disquete al disco RAM.


REM Ents

DEF PROC makeData
  LOCAL actions,a$,entI,entId,exit,i,i$,gender,name,number,entFields,syntax,w$,wordI
  LET lockedDoor=100
  LET verb=SGN PI,noun=2,noArticle=4,noObject=8,needsObject=16
  countData actions
  calculateFields
  DIM !word$(words,wordFieldsLength)
  DIM !ent#(ents,entFields)
  DIM !location$(ents,locationFieldsLength)
  PRINT "Datos sinta"+CHR$ 8; OVER 1;"'cticos."
  RESTORE 920
  LET wordI=NOT PI,entI=NOT PI
  DO 
    READ w$
  EXIT IF NOT LEN w$
    translate w$
    LET wordI=wordI+1,!word$(wordI,fWord TO )=w$
    READ i$,name,syntax,gender,number
    IF i$(1)="=" THEN 
      LET entId=VAL i$(2 TO )
    ELSE LET entI=entI+1,entId=entI,!ent#(entId,fEntGender)=gender,!ent#(entId,fEntNumber)=number
      KEYIN " LET "+i$+"="+STR$ entId
    IF NOT name THEN LET name=nextText
      newText w$
    IF AND(syntax,verb) THEN READ a$
      LET !word$(wordI,fActionStart TO fActionEnd)=a$
    LET !word$(wordI,fEntId)=CHR$ entId,!word$(wordI,fSyntax)=CHR$ syntax,!word$(wordI,fWordGender)=CHR$ gender,!word$(wordI,fWordNumber)=CHR$ number,!ent#(entI,fName)=name
  LOOP 
  PRINT "Datos de ubicaciones."
  RESTORE 1970
  FOR i=1 TO ents
    LET !location$(i,fEntId)=CHR$ i,!location$(i,fLocation)=CHR$ 0
  NEXT i
  DO 
    READ ent
  EXIT IF NOT ent
    READ location
    LET !location$(ent,fLocation)=CHR$ location
  LOOP 
  PRINT "Datos de escenarios."
  RESTORE 2100
  DO 
    READ ent
  EXIT IF NOT ent
    FOR i=fNorth TO fDown
      READ exit
      LET !ent#(ent,i)=exit
    NEXT i
    LET !ent#(ent,fIsLocation)=1,!ent#(ent,fKnown)=1
    READ i$
    LET !ent#(ent,fGraphic)=VAL ("0"+i$)
  LOOP 
  PRINT "Entidades conocidas."
  RESTORE 2590
  DO 
    READ ent
  EXIT IF NOT ent
    LET !ent#(ent,fKnown)=1
  LOOP 
  ramToDisk "ent#"
  ramToDisk "location$"
  ramToDisk "word$"
END PROC 

DEF PROC readFields REF counter
  LOCAL i$
  LET counter=NOT PI
  DO 
    READ LINE i$
  EXIT IF i$="END"
    LET counter=counter+1
    KEYIN " LET "+i$+"="+STR$ counter
  LOOP 
END PROC 

DEF PROC calculateFields
  PRINT "Campos de datos."
  RESTORE 900
  readFields entFields
  REM Fields of !ent#()
  LET fEntId=1,fSyntax=fEntId+1,fHomonimes=fSyntax+1,fWordGender=fHomonimes+1,fWordNumber=fWordGender+1,fActionStart=fWordNumber+1,fActionEnd=fActionStart+actionLength-1,fWord=fActionEnd+1,wordFieldsLength=fWord+wordLength-1
  REM Fields of !word$(). The order doesn't matter, except that fEntId mustbe the first one, and fWord the last one.
  LET fLocation=fEntId+1,locationFieldsLength=fLocation
  REM Fields of !location$(). The fEntId, already defined for the !word$() array, is the first one also in this array. 
END PROC 

DEF PROC countData REF actions
  LOCAL a,a$,i,i$,syntax
  RESTORE 920
  PRINT "Recuento y examen de datos."
  LET words=NOT PI,actions=NOT PI,ents=NOT PI,actionLength=INT PI,wordLength=INT PI
  DO 
    READ i$
  EXIT IF NOT LEN i$
    LET i=LEN i$
    IF i>wordLength THEN LET wordLength=i
    LET words=words+1
    READ i$,i,syntax,i,i
    IF i$(1)<>"=" THEN LET ents=ents+1
    IF AND(syntax,verb) THEN LET actions=actions+1
      READ a$
      LET a=LEN a$
      IF a>actionLength THEN LET actionLength=a
  LOOP 
END PROC 

La rutina de impresión de textos

La información necesaria para calcular el espacio restante disponible en una ventana hay que extraerla de las variables internas de Beta BASIC; y algunos efectos solo pueden lograrse modificando estas variables. Esto hace que el código sea más oscuro de lo que cabría esperar.


DEF PROC tell t$,inkColor
  DEFAULT inkColor=oInk
  LOCAL c,first,s$,width,chw
  LET first=SGN PI,width=PEEK CHPL,t$=t$+" ",chw=PEEK PCHW
  FOR c=1 TO LEN t$-1
    IF t$(c+1)<>" " THEN GO TO 9307
    LET s$=" "
    IF (PEEK WXCOORD+(c-first+2)*chw)>PEEK WRLIMIT THEN 
      POKE WXCOORD,PEEK WLLIMIT
      PRINT 
      LET s$=""
    PRINT INK inkColor;s$ AND (PEEK WXCOORD>PEEK WLLIMIT);t$(first TO c);
    LET first=c+2,c=c+1
  NEXT c
END PROC 

DEF PROC tellNL t$,inkColor,indent
  DEFAULT inkColor=oInk,indent=1
  IF PEEK WYCOORD<>PEEK WTLIMIT OR PEEK WXCOORD<>PEEK WLLIMIT THEN 
    POKE WXCOORD,PEEK WLLIMIT
    IF NOT (PEEK WYCOORD>=(PEEK WBLIMIT+PEEK PCHH)) THEN PRINT 'STRING$(indent," ");
    ELSE POKE WYCOORD,PEEK WYCOORD-PEEK PCHH
      POKE WXCOORD,PEEK WLLIMIT+indent*PEEK PCHW
  tell t$,inkColor
END PROC 

El analizador lingüístico

La primera versión de trabajo solo reconoce dos palabras válidas en cada frase. Es un borrador para poder probar el programa hasta completar las bases de datos.


REM Parser

DEF PROC command REF t$
  DO 
    accept t$
  LOOP UNTIL LEN t$
END PROC 

DEF PROC parser c$
  LOCAL a$,entId,i,i$,known,r$,w$,word
  LET c$=SHIFT$(2,c$)+" ",known=0,r$=""
  DIM w$(wordLength)
  DIM w(maxKnown)
  DIM e(maxKnown)
  SORT !word$(1 TO )(fWord TO )
  
  REM Get known words
  
  DO 
    LET i=INSTRING(1,c$," "),w$=c$( TO i-1),c$=c$(i+1 TO ),word=INARRAY("word$(1,fWord TO )",w$)
    IF word THEN LET known=known+1,w(known)=word
  LOOP UNTIL NOT LEN c$ OR known=maxKnown
  
  REM Get the syntax elements
  
  LET action=0,object=0,syntaxError=0
  LET i=1
  DO WHILE i<=known
    LET i$=!word$(w(i)),r$=r$+i$(fWord TO )
    trim r$
    LET r$=r$+" "
    LET e(i)=CODE i$(fEntId),syntax=CODE i$(fSyntax)
    
    REM Action?
    IF NOT AND(verb,syntax) THEN GO TO 850
    IF action AND action<>e(i) THEN IF AND(syntax,noun) THEN GO TO 880
    ELSE tellError tError2Actions,r$
    EXIT IF 1
    LET action=e(i),actionSyntax=syntax,a$=!word$(w(i),fActionStart TO fActionEnd)
    GO TO 910
    
    REM Object?
    IF NOT AND(noun,syntax) THEN GO TO 910
    IF NOT action THEN tellError tErrorVocative,r$
    EXIT IF 1
    IF object AND object<>e(i) THEN tellError tError2Objects,r$
    EXIT IF 1
    IF AND(actionSyntax,noObject) THEN tellError tError1Object,r$
    EXIT IF 1
    LET object=e(i),objectSyntax=syntax
    
    LET i=i+1
  LOOP 
  
  REM Result
  
  IF NOT action THEN tellError tErrorNoAction,r$
  IF action AND AND(actionSyntax,needsObject) AND NOT object THEN tellErrortErrorNoObject,r$
  IF action AND NOT syntaxError THEN tellNL r$,iInk
    KEYIN "action_"+a$
END PROC 

Páginas relacionadas

Glosario

BASIC
Beginner's All-Purpose Symbolic Instruction Code (código polivalente de instrucciones simbólicas para principiantes)
BBim
Beta BASIC improved (Beta BASIC mejorado)
GIMP
GNU Image Manipulation Program (programa de manipulación de imágenes de GNU)
KiB
kibiocteto (1024 octetos)
RAM
Random Access Memory (memoria volátil de lectura y escritura)
SBim
S*BASIC improved (S*BASIC mejorado)
Vim
Vi IMproved (Vi mejorado)