La legionela del pisto

Descripción del contenido de la página

Juego de aventuras de texto para ZX Spectrum 128, escrito en Beta BASIC 4.0+D con formato BBim.

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

Etiquetas:

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 fichero.

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 fichero.

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 fichero 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:

EntradaPasilloTextoSala de visitasSala de visitas

Código fuente

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 fichero 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.

9300 DEF PROC tell t$,inkColor
9301   DEFAULT inkColor=oInk
       LOCAL c,first,s$,width,chw
       LET first=SGN PI,width=PEEK CHPL,t$=t$+" ",chw=PEEK PCHW
9303   FOR c=1 TO LEN t$-1
9304     IF t$(c+1)<>" " THEN GO TO 9307
9305     LET s$=" "
         IF (PEEK WXCOORD+(c-first+2)*chw)>PEEK WRLIMIT THEN
           POKE WXCOORD,PEEK WLLIMIT
           PRINT
           LET s$=""
9306     PRINT INK inkColor;s$ AND (PEEK WXCOORD>PEEK WLLIMIT);t$(first TO c);
         LET first=c+2,c=c+1
9307   NEXT c
9308 END PROC
9310 DEF PROC tellNL t$,inkColor,indent
       DEFAULT inkColor=oInk,indent=1
9311   IF PEEK WYCOORD<>PEEK WTLIMIT OR PEEK WXCOORD<>PEEK WLLIMIT THEN
         POKE WXCOORD,PEEK WLLIMIT
         IF NOT 0 THEN PRINT 'STRING$(indent," ");
         ELSE POKE WYCOORD,PEEK WYCOORD-PEEK PCHH
           POKE WXCOORD,PEEK WLLIMIT+indent*PEEK PCHW
9312   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

 400 DEF PROC command REF t$
       DO
         accept t$
 420   LOOP UNTIL LEN t$
     END PROC
 430 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 )
 440
       REM Get known words

 450   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
 460   LOOP UNTIL NOT LEN c$ OR known=maxKnown
 470
       REM Get the syntax elements

 480   LET action=0,object=0,syntaxError=0
 490   LET actionSyntax=0,objectSyntax=0
 500   LET i=1
 510   DO WHILE i<=known
 530     LET i$=!word$(w(i)),r$=r$+i$(fWord TO )
         trim r$
         LET r$=r$+" "
 550     LET e(i)=CODE i$(fEntId),syntax=CODE i$(fSyntax)
 560
         REM Action?
 570     IF NOT AND(verb,syntax) THEN GO TO 610
 580     IF action AND action<>e(i) THEN IF AND(syntax,noun) THEN GO TO 640
         ELSE tellError tError2Actions,r$
         EXIT IF 1
 590     LET action=e(i),actionSyntax=syntax,a$=!word$(w(i),fActionStart TO fActionEnd)
 600     GO TO 670
 610
         REM Object?
 620     IF NOT AND(noun,syntax) THEN GO TO 670
 630     IF NOT action THEN tellError tErrorVocative,r$
         EXIT IF 1
 640     IF object AND object<>e(i) THEN tellError tError2Objects,r$
         EXIT IF 1
 650     IF AND(actionSyntax,noObject) THEN tellError tError1Object,r$
         EXIT IF 1
 660     LET object=e(i),objectSyntax=syntax
 670
         LET i=i+1
 680   LOOP
 690
       REM Result

 700   IF NOT action THEN tellError tErrorNoAction,r$
 710   IF action AND AND(actionSyntax,needsObject) AND NOT object THEN tellErrortErrorNoObject,r$
 720   IF action AND NOT syntaxError THEN tellNL r$,iInk
         KEYIN "action_"+a$
 730 END PROC

Páginas relacionadas

Historial de desarrollo de La legionela del pisto
Historial de desarrollo del proyecto La legionela del pisto, una aventura conversacional en Beta BASIC 4.0+D (con formato BBim) para ZX Spectrum.
BBim
Utilería para escribir programas para ZX Spectrum en BBim (formato mejorado de Beta BASIC) con el editor Vim.
Por una conjunción copulativa; viajando en el tiempo
Apuntes dispersos de un (retro)programador. 18 de julio de 2010.
De los pixeles a las palabras; una palabra tras otra
Apuntes dispersos de un (retro)programador. 29 de noviembre de 2010.
Apuntes sobre Beta BASIC 4.0+D
Relación de características destacadas, limitaciones, fallos y trucos de Beta BASIC 4.0+D (para ZX Spectrum 128 con interfaz +D).
Colegio Erevest
Tres versiones de una aventura conversacional escrita en Sinclair BASIC para la Sinclair ZX Spectrum, ambientada en un colegio de curas nada simpáticos, escritas entre 1985 y 1986.
CE4
Juego de aventuras de texto.