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.
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:
- Permite crear y manipular matrices de datos (de hasta 64 KiB) en el disco RAM.
- Permite gestionar módulos de programa en el disco RAM.
- Permite usar ventanas y cambiar el tamaño de la letra, entre otras funcionalidades gráficas muy útiles ya presentes en Beta BASIC 3.0.
- Permite usar disquetes de la interfaz de disco Plus D.
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:
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