De los pixeles a las palabras; una palabra tras otra

El programa Alien-B está casi terminado. Sólo falta el final del efecto final que tiene lugar tras la inevitable invasión alienígena. A punto he estado de dividir el programa en partes, debido a la escasez de memoria; los 20 KiB y pico que Beta BASIC deja libres en la memoria principal de una ZX Spectrum (de 48 o 128 KiB), la mitad de lo que está disponible antes de cargar el lenguaje, siempre terminan siendo escasos. Por suerte creo que no hará falta dividir el programa: unos ajustes para ahorrar memoria en algunos de los puntos menos críticos para la velocidad parecen bastar. Dividir el programa haría más laborioso el desarrollo, lo que no merece la pena cuando el proyecto está a punto de terminar. Quiero terminarlo pronto, publicarlo, dejar que los alienígenas invadan lo que quieran y dirigir por fin la atención a otros proyectos que están esperando. Además, tras dedicar tanta atención últimamente a pixeles y colores, tengo ganas de ocuparme de «entes» diferentes y no menos interesantes: palabras y matrices, por ejemplo.

Así que para cambiar de enfoque, aunque no de lenguaje ni de plataforma, le acabo de dar un empujoncillo al proyecto La legionela del pisto, al que le calculo más de un año para ver la luz. Había algo muy concreto que no me convencía y queria perfeccionar: El procedimiento que lista los objetos presentes en el escenario preparaba una lista con ellos para después imprimirla completa. El motivo es que era necesario un recuento previo para conocer cuántos objetos había presentes y así poder construir una lista lingüísticamente correcta (con las comas y la conjunción en su sitio). Pero esto tenía el inconveniente de que se producía una molesta pausa antes de la impresión, mientras el procedimiento construía la lista, incluso con Fuse funcionando al 1500% (el máximo que alcanza en mi Asus Eee 4G).

Este es el código que no me convencía:

2040 DEF PROC listPresentEntsOLD
       REM old version!!!
2050   LOCAL a$,ent,i,i$,l$,m$,listed,n$,t$,thing,things
       LET a$="",listed=0,t$="",l$=CHR$ location
2060   SORT !location$(1 TO )(fLocation)
       LET i=INARRAY("location$(1,fLocation)",l$)
       IF NOT i THEN GO TO 2170
2070   DO
2080     LET i$=!location$(i,fLocation)
       EXIT IF i$<>l$
2090     LET i$=!location$(i,fEntId)
         JOIN i$ TO t$
         LET i=i+1
2100   LOOP UNTIL i>ents
2110   LET things=LEN t$
       IF NOT things THEN GO TO 2170
2120   ON RNDM(1)+1
         LET m$="Veo "
         LET m$="Puedo ver "
2130   FOR i=1 TO things
2140     LET ent=CODE t$(i)
         getEntName ent,n$
         getEntArticle ent,a$
         LET n$=(", " AND (listed AND (listed<>(things-1))))+(" y " AND (listed= (things-1) AND listed))+a$+" "+n$,listed=listed+1
         COPY n$ TO m$
2150   NEXT i
2160   tell m$+"."
2170 END PROC

Pensé que el algoritmo podía mejorarse para que los objetos se fueran imprimiendo según se fueran encontrando en la base de datos. El desafío era cómo saber si el objeto actual era el último o no, pues de ello dependía poder poner bien las comas, la latosa conjunción «y» y el punto final. La solución que ideé fue examinar por adelantado el siguiente elemento de la lista; así era posible saber si el elemento actual era o no el último sin necesitar hacer el recuento previo.

Este es el nuevo código, más eficaz, más sencillo y más brillante:

2180 DEF PROC listPresentEnts
2190   LOCAL a$,e$,ent,first,i,l$,m$,last,n$
       LET a$="",first=1,t$="",l$=CHR$ location
2200   SORT !location$(1 TO )(fLocation)
       LET i=INARRAY("location$(1,fLocation)",l$)
       IF NOT i THEN GO TO 2260
2210   DO
2220     LET e$=!location$(i,fEntId),ent=CODE e$,i=i+1,last=i>ents
         IF NOT last THEN
           LET last=!location$(i,fLocation)<>l$
2230     IF first THEN ON RNDM(2)+1
           tell "Veo"
           tell "Puedo ver"
           tell "Hay"
2240     getEntName ent,n$
         getEntArticle ent,a$
         tell ("y " AND last AND NOT first)+a$+" "+n$+",."(last+1)
         LET first=0
2250   LOOP UNTIL last
2260 END PROC

El resultado fue el previsto: los objetos eran listados uno tras otro, según se iban encontrando. Un pequeño problema de esta nueva versión es que no hay forma de no imprimir la coma tras el penúltimo elemento, o no la he encontrado todavía sin renunciar a la sencillez de la pasada única. Un problema mayor y esperado era que cada elemento se imprimía en su propia línea...

El procedimiento tell que estaba usando para imprimir los textos estaba basado en uno publicado en Beta BASIC Newsletter (número 3, página 12). Sólo le había hecho alguna pequeña mejora:

9300 DEF PROC tell t$,inkColor
9301   REM From Beta BASIC Newsletter #3 p.12
9302   DEFAULT inkColor=oInk
9303   LOCAL first,last,width,end,temp
       LET width=PEEK CHPL,end=LEN t$,first=1,temp=width
9304   DO
9305     LET last=end
9306     DO WHILE temp<end
9307       LET last=temp
9308       DO UNTIL t$(last+1)=" " OR t$(last)="-"
9309         LET last=last-1
9310       LOOP
9311     LOOP UNTIL 1
9312     PRINT INK inkColor;t$(first TO last);
         IF (last-first+1)<>width THEN PRINT
9313   EXIT IF last>=end
9314     LET first=last+1+(t$(last+1)=" "),temp=first+width-1
9315   LOOP
9316 END PROC

Este procedimiento estaba diseñado para empezar a imprimir siempre en una línea nueva; era necesario modificarlo para que imprimiera los textos a partir de la posición actual del cursor. Fue laborioso lograrlo pero mereció la pena.

Escribí una nueva versión de tell desde cero con otro planteamiento: usar la variable S POSN (en la dirección 23688) del sistema de la ZX Spectrum, actualizada también por Beta BASIC, para conocer la columna de impresión en pantalla. Este es el algoritmo (con algunas instrucciones provisionales para su depuración):

 200 DEF PROC tell t$,inkColor
       REM First new version, spaces only.
 210   DEFAULT inkColor=oInk
 220   LOCAL c,first,width
       LET first=1,width=PEEK CHPL,t$=t$+" "
 230   FOR c=1 TO LEN t$-1
 240     REM
         PRINT #0;AT 0,0;USING$("000",PEEK SPOSN);"/";PEEK CHPL'"first=";first;"c=";c;"(";t$(c);")"
         PAUSE 0
         REM debug!!!
 250     DO WHILE t$(c+1)=" "
 260       LET s$=" "
           IF (c-first+2)>=PEEK SPOSN THEN PRINT
             LET s$=""
 270       PRINT PAPER RNDM(6)+1; INK 9;s$ AND (NOT PEEK SPOSN>width);t$(first TO c);
           LET first=c+2
 280     LOOP UNTIL 1
 290   NEXT c
 300 END PROC

Pero pensé que estaría bien considerar también las palabras compuestas que tienen un guión, para dividirlas y justificar mejor el texto. Para ello hice unos cambios:

9300 DEF PROC tell t$,inkColor
9301   DEFAULT inkColor=oInk
9302   LOCAL c,first,normal,width,word
       LET normal=1,first=1,width=PEEK CHPL,t$=t$+" "
9303   FOR c=1 TO LEN t$-1
9304     LET word=(t$(c)="-")+2*(t$(c+1)=" ")
9305     DO WHILE word
9306       LET s$=" "
           IF (c-first+2)>=PEEK SPOSN THEN PRINT
             LET s$=""
9307       PRINT INK inkColor;s$ AND (NOT PEEK SPOSN>width) AND normal;t$(first TO c);
           LET first=c+word,normal=AND(word,2)
9308     LOOP UNTIL 1
9309   NEXT c
9310 END PROC

El método consistió en calcular la variable word, que indica que se ha encontrado el final de una palabra, con ambas condiciones: espacio final de palabra o guión de palabra compuesta. Después bastó rehacer algunos cálculos usando esta variable y añadir la variable normal para recordar si la palabra anterior era «normal» en lugar de compuesta, pues de ello depende el espacio de separación previo a cada palabra.

Por último, para poder imprimir en una nueva línea como hacía antes el procedimiento tell, hacía falta un nuevo procedimiento, trivial:

9311 DEF PROC tellNL t$,inkColor
       DEFAULT inkColor=oInk
       PRINT
       tell t$,inkColor
     END PROC

Tras hacer nuevas pruebas concluí que la mayor lentitud de la versión de tell que comprueba también las palabras compuestas no se justificaba, pues estas palabras son muy infrecuentes. Eso sí, el algoritmo quedaba escrito para aplicarlo en otra ocasión en que hiciera falta en otro programa, con una máquina y un lenguaje más potentes.

Por último hice dos cambios: Primero, observé que con ciertos tamaños de caracteres y ventanas se producían descuadres en la justificación de los textos, aunque infrecuentes; en esas raras ocasiones la variable S POSN no contenía el valor que cabía esperar. Como solución de emergencia aumenté en dos caracteres el margen de comparación con el contenido de esa variable. Segundo, unas sencillas mediciones descubrieron lo que sospechaba: hacer un salto de línea era ligeramente más rápido que usar una estructura DO - LOOP. El resultado final quedó así:

9300 DEF PROC tell t$,inkColor
9301   DEFAULT inkColor=oInk
9302   LOCAL c,first,width
       LET first=1,width=PEEK CHPL,t$=t$+" "
9303   FOR c=1 TO LEN t$-1
9304     IF t$(c+1)<>" " THEN GO TO 9308
9305     LET s$=" "
         IF (c-first+4)>=PEEK SPOSN THEN PRINT
           LET s$=""
9306     PRINT INK inkColor;s$ AND (NOT PEEK SPOSN>width);t$(first TO c);
         LET first=c+2
9308   NEXT c
9309 END PROC

Las palabras entonces por fin fluyeron una tras otra, casi sin tropezones...

Páginas relacionadas

Por una conjunción copulativa; viajando en el tiempo
Apuntes dispersos de un (retro)programador. 18 de julio de 2010.
La legionela del pisto
[Proyecto durmiente:] Juego de aventuras de texto para ZX Spectrum 128, escrito en Beta BASIC 4.0+D con formato BBim.