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