Historial del desarrollo inicial de Nuclear Waste Invaders

Descripción del contenido de la página

Historial del desarrollo inicial (solo hasta 2016-11) del programa Nuclear Waste Invaders, un matamarcianos para ZX Spectrum 128 escrito en Forth.

2016-02-14

Tomo una copia del código original para Jupiter ACE, escrito por Dancresp en 2013, y empiezo a hacer los primeros cambios.

2016-02-15

Termino de cambiar el formato del código: lo paso a minúsculas, indento las palabras y las estructuras de control, divido las líneas a 63 columnas y añado los comentarios de cabecera de bloque. También pongo nombres en inglés a todas las palabras que estaban en español, y pongo nombres significativos a las variables que eran solo letras. Asimismo añado comentarios de pila.

Con estos cambios el código queda listo para el siguiente paso: dividir las larguísimas palabras del original en palabras pequeñas.

2016-02-16

Termino el sistema de creación de gráficos.

( udg-set >udg >scan scan! )

          27 constant udgs   \ number of UDGs
           8 constant scans  \ scans per UDG
udgs scans * constant /udgs  \ size of the UDG set

create udg-set /udgs allot

: .udgs  ( -- )  cr udgs 0 do  i 'a' + emit  loop
                 cr udgs 0 do  i 128 + emit  loop  ;
  \ Print all game UDGs.
  \ XXX TMP -- for debugging

udg-set os-udg !
  \ Point system UDG to the game UDG set.
  \ Solo Forth will use this set for chars 128..255.

: >udg  ( c -- a )  'a' - scans * udg-set +  ;
  \ Convert UDG char _c_ to the address _a_ of its bitmap.

' >udg >body cell+ constant >'a'
:  ( -- )
  >'a' @ 97 <> abort" >udg has been corrupted"  ;
  \ XXX TMP -- for debugging

: >scan  ( n c -- a )  >udg +  ;
  \ Convert scan number _n_ of UDG char _c_ to its address _a_.

: scan!  ( c b n -- c )  rot >scan c!  ;
  \ Store scan _b_ into scan number _n_ of char _c_,
  \ and return _c_ back for further processing.

-->

( sprite! 2sprite! )

: sprite!  ( n0..n7 c -- )
  scans 0 do
    dup scans 1+ i - roll i scan!
  loop  drop  ;
  \ Store a 1x1 UDG sprite into char _c_.

: 2sprite!  ( n0..n7 c -- )
  scans 0 do
    dup scans 1+ i - pick flip i scan! 1+  \ first UDG
    dup scans 1+ i - roll      i scan! 1-  \ second UDG
  loop  drop  ;
  \ Store a 2x1 UDG sprite into char _c_.
  \ Scans _n0..n7_ are 16-bit: high part is char _c_,
  \ and low part is the next char.

-->

Con este médodo los gráficos pueden definirse durante la interpretación de la fuente y son legibles, como en estos ejemplos:

( graphics )

binary
  \ "ab",  30 points invader, sprite 2
0000000110000000
0000001111000000
0000011111100000
0000110110110000
0000111111110000
0000010110100000
0000100000010000
0000010000100000

char a 2sprite!

decimal  -->

( graphics )

binary
  \ "s", char 115, laser
00000000
00000001
00000001
00000001
00000001
00000001
00000000
00000000

char s sprite!

decimal  -->

También termino el nuevo método para la tabla de datos de los invasores, que antes de cada partida se restaurará desde una copia intocable:

( invaders-data )

  \ Invaders data is stored in a table.
  \ which has the following structure:
  \
  \ +0 = lifes (0..3)
  \ +1 = active? (0..1)
  \ +2 = y coordinate (row)
  \ +3 = x coordinate (column)
  \ +4 = main graphic (character)

  \ The `invader` variable points to the data of the current
  \ invader in the table.

create default-invaders-data
  \ Default invaders data table.
  \ This is used to restore the actual data table
  \ before a new game.

here

  \ lifes    active?   y      x     character
    3 c,     0 c,      5 c,   0 c,  'c' c,
    3 c,     0 c,      7 c,   0 c,  'g' c,
    3 c,     0 c,      9 c,   0 c,  'g' c,
    3 c,     0 c,     11 c,   0 c,  'k' c,
    3 c,     0 c,      5 c,  29 c,  'c' c,
    3 c,     0 c,      7 c,  29 c,  'g' c,
    3 c,     0 c,      9 c,  29 c,  'g' c,
    3 c,     0 c,     11 c,  29 c,  'k' c,
    3 c,     0 c,     13 c,  29 c,  'k' c,

here swap - constant /invaders-data
  \ Space occupied by the invaders data.

create invaders-data  /invaders-data allot
  \ Current invaders data.

Aún queda mucho código por factorizar, para hacerlo legible y fácil de mantener. También hay mucho código que puede ser optimizado eliminando estructuras de control, especialmente condicionales, como en este ejemplo:

: move-tank  ( -- )
  tank-x @ dup
  27 < if
    inkey 'x' = if  1 tank-x +!  then
  then
  3 > if
    inkey 'z' = if  -1 tank-x +!  then
  then
  tank-x @ 1- 20 at-xy ."  mn " ;

Que así es más rápido y además ocupa menos memoria una vez compilado:

: move-tank  ( -- )
  tank-x @ dup
  27 < if  inkey 'x' = abs tank-x +!  then
   3 > if  inkey 'z' =     tank-x +!  then
  tank-x @ 1- 20 at-xy ."  mn " ;

Y mejor todavía sin un solo condicional:

: move-tank  ( -- )
  tank-x @ dup 27 < inkey 'x' = and abs tank-x +!
                3 > inkey 'z' = and     tank-x +!
  tank-x @ 1- 20 at-xy ."  mn " ;

Y mejor todavía si leemos y actualizamos una sola vez la variable tank-x:

: move-tank  ( -- )
  tank-x @  inkey 'x' = abs +  inkey 'z' = +
            27 min 3 max
  dup tank-x !
  1- 20 at-xy ."  mn " ;

De forma similar, el código que gestiona el disparo era una sola palabra:

: shoot
  shoot-y @ 0=
  if    inkey 13 = if  tank-x @ shoot-x ! 19 shoot-y !  then
  else  shoot-y @ dup shoot-x @ at space 5 <
        if    0 shoot-y !
        else  shoot-y @ 2- shoot-y ! impact 0=
              if  shoot-y @ shoot-x @ at 115 emit  then
        then
  then ;

Y tras ser dividido en partes es más legible y fácil de mantener:

( shoot )

: .projectile  ( -- )  shoot-x @ shoot-y @ at-xy 115 emit  ;
  \ Show the projectile.

: fire?  ( -- f )  inkey 13 =  ;

: fire  ( -- )
  tank-x @ shoot-x ! [ tank-y 1- ] literal shoot-y !  ;
  \ The tank fires.

: -projectile  ( -- )  shoot-x @ shoot-y @ at-yx space  ;
  \ Delete the projectile.

: projectile-lost?  ( -- f )  shoot-y @ 5 <  ;
  \ Is the projectile lost?

: shooted  ( -- )
  -projectile projectile-lost?
  if    shoot-y off
  else  -2 shoot-y +! impact? ?exit  .projectile  then  ;
  \ Manage the projectile.

: shooted?  ( -- )  shoot-y @  ;
  \ Has the tank already shooted?

: shoot  ( -- )
  shooted? if  shooted exit  then
  fire? if  fire  then  ;
  \ Manage the shoot.

Un ejemplo similar con la gestión del ovni. Antes:

: ufo  ( -- )
  ufo-x @ dup 0<
  if    1+ ufo-x !
  else  dup 27 > if     3 swap at 3 spaces init-ufo
                 else   1+ dup ufo-x ! 3 swap at ."  op"
                 then
  then  ;

Después (será algo más lento, pero es mucho más fácil de leer y mantener):

 3 constant ufo-y       \ coordinate (row)
27 constant ufo-max-x   \ coordinate (column)

: ufo-invisible?  ( -- f )  ufo-x @ 0<  ;

: at-ufo  ( -- )  ufo-x @ ufo-y at-xy  ;
  \ Set the cursor position at the coordinates of the UFO.

: -ufo  ( -- )  at-ufo 3 spaces init-ufo  ;
  \ Delete and init the UFO.

: ufo-lost?  ( -- f )  ufo-x @ ufo-max-x >  ;
  \ Is the UFO lost?

: flying-ufo  ( -- )  1 ufo-x +! at-ufo ."  op"  ;
  \ Update the position of the UFO and show it.

: (ufo)  ( -- )  ufo-lost?  if  -ufo  else  flying-ufo  then  ;
  \ Manage the UFO.

: ufo  ( -- )
  ufo-invisible? if  1 ufo-x +!  else  (ufo)  then  ;
  \ Manage the UFO, if it's visible.

También esta palabra, ya factorizada, puede simplificarse:

: next-phase  ( -- )
  invaders @ 0= if
    phase @ dup 5 < if  1+  then  phase ! 100 pause init-round
  then ;

Quedando así:

: next-phase  ( -- )
  invaders @ ?exit
  phase @ 5 < abs +! 100 pause init-round  ;

El código que gestiona el impacto del proyectil con el ovni originalmente forma parte de la palabra que gestiona todo tipo de impactos:

      ufo-x @ 1+ ufo-y at-xy ." tu"
      18 12 do  i 15 beep  loop
      rnd 12 / 1+ 50 * dup ufo-x @ 1+ 3 at-xy
      . score @ + dup score ! .score 20 pause -ufo

Pero extraído y factorizado resulta más claro:

: ufo-bang  ( -- )  18 12 do  i 15 beep  loop  ;

: ufo-in-fire  ( -- )  ufo-x @ 1+ ufo-y at-xy ." tu"  ;

: ufo-explosion  ( -- )  ufo-in-fire ufo-bang  ;

: ufo-points  ( -- n )  rnd 12 / 1+ 50 *  ;
  \ Random points for impacting the UFO.

: ufo-bonus  ( -- )
  ufo-points dup ufo-x @ 1+ 3 at-xy .  score +! .score  ;
  \ Update the score with the UFO bonus.

: ufo-impacted  ( -- )  ufo-explosion ufo-bonus 20 pause -ufo  ;

Modifico el manejo de los datos de invasores, usando una celda por cada dato en lugar de solo un octeto. Esto simplifica algunos cálculos.

El resto del código que gestiona los impactos sigue necesitando una buena factorización:

: (impact)  ( -- )
  shoot-y @ ufo-y = if  ufo-impacted exit  then

  -1 invaders +!
  shoot-y @ 3 - 2 / shoot-x @ 15 >  if  5 +  then
  shoot-y @ dup 5 = if   drop 30
                    else 10 > if  10  else  20  then
                    then
  score @ + dup score !
  .score 1- 5 * 10000 + shoot-y !
  shoot-y @ 2+ c@ shoot-y @ 3 + c@ dup 15 < if  1+  then
  2dup at-yx ." qr"
  10 100 beep at-yx 2 spaces
  shoot-y @ dup c@ 1- swap c! shoot-y @ c@ 0> if
    0 shoot-y @ 1+ c! shoot-x @ 15 < if 0 else 29 then
    shoot-y @ 3 + c! shoot-y @ 2+ c@
    shoot-y @ 3 + c@ at-yx
    shoot-x @ 15 < if  space  then
    shoot-y @ 4 + c@ dup emit 1+ emit
  then  ;

Una primera simplificación sencilla es sustituir estructuras condicionales por cálculos:

: (impact)  ( -- )
  shoot-y @ ufo-y = if  ufo-impacted exit  then

  -1 invaders +!
  shoot-y @ 3 - 2/ shoot-x @ 15 > abs 5 * +
  shoot-y @ dup 5 = if   drop 30
                    else 10 > 10 * 20 +
                    then
  score @ + dup score !
  .score 1- 5 * 10000 + shoot-y !
  shoot-y @ 2+ c@ shoot-y @ 3 + c@ dup 15 < abs +
  2dup at-yx ." qr"
  10 100 beep at-yx 2 spaces
  shoot-y @ dup c@ 1- swap c! shoot-y @ c@ 0> if
    0 shoot-y @ 1+ c! shoot-x @ 15 < abs 29 *
    shoot-y @ 3 + c! shoot-y @ 2+ c@
    shoot-y @ 3 + c@ at-yx
    shoot-x @ 15 < if  space  then
    shoot-y @ 4 + c@ dup emit 1+ emit
  then  ;

La gestión del invasor también necesita factorización:

: move-invader  ( -- f )
  invader @ c@ 0> if
    invader @ 1+ c@ 0= if
      rnd invaders @ 5 < if 10 else 26 then
      > if 1 invader @ 1+ c! then 0
    else
      invader @ 10025 < if  1  else  -1  then
      dup dup invader @ 3 + c@ + invader @ 3 + c!
      invader @ 3 + c@ invader @ 2+ c@ at-xy
      0> if  space  then
      invader @ 4 + c@ invader @ 3 + c@ 2 mod 0> if  2-  then
      dup emit 1+ emit 0< if space then
      invader @ 3 + c@ dup 14 phase @ -  =
      if  drop 1  else  15 phase @ + = if  1  else  0  then
      then
    then
  else  0  then
  invader @ 10045 = if    10000
                    else  invader @ 5 +
                    then  invader !  ;  -->

El resultado, tras factorizar el código y mejorar el manejo de los datos de los tipos de invasores, es el siguiente:

( move-invader )

: at-invader  ( -- )  invader-x @ invader-y @ at-xy  ;
  \ Set the cursor position at the coordinates of the invader.

: invader-frame  ( -- c )
  invader-char @ invader-x @ 2 mod 0> 2* +  ;
  \ Frame of the invader.

: .invader-frame  ( -- )  invader-frame dup emit 1+ emit  ;
  \ Print the frame of the invader.

: .invader  ( n -- )
    dup at-invader 0> if  space  then
    .invader-frame 0< if  space  then  ;
  \ Print the current invader.
  \ _n_ is its x increment (1 or -1).

: move-active-invader  ( -- )
  1 invader-type @ 5 < 0= ?negate  ( 1 | -1 )
  dup invader-x +!  .invader
  invader-x @ dup 14 phase @ -  =
  if  drop 1  else  15 phase @ + =  then  ;

-->

( move-invader )

: move-inactive-invader  ( -- )
  rnd  26 invaders @ 5 < 16 * -  >
  if  invader-active on  then  ;

: next-invader  ( -- )
  invader-type @ [ invader-types 1- ] literal =
  if  invader-type off  else  1 invader-type +!  then  ;

: (move-invader)  ( -- f )
  invader-active @ if    move-active-invader
                   else  move-inactive-invader false  then  ;

: move-invader  ( -- f )
  invader-lifes @ if     (move-invader)
                  else   false
                  then   next-invader  ;

Las palabras de cambio de modo gráfico, que fueron añadidas recientemente:

: graphics-on  ( -- )  udg-set 'a' scans * -  os-chars !  ;
  \ Set the UDG set as main font, pointing the first UDG
  \ to char 'a'. This way the original code will work without
  \ change.
  \ XXX TMP -- compatibility layer for the original code

: graphics-off ( -- )  15360 os-chars !  ;
  \ Set the ROM charset.
  \ XXX TMP -- compatibility layer for the original code

Pueden ser simplificadas así:

udg-set 'a' scans * -  constant graphic
  \ Address of a presumed font whose char `a` is the first UDG
  \ of the game.  This way the original code will work without
  \ change.
  \ XXX TMP -- compatibility layer for the original code

15360 constant text
  \ Address of the ROM charset (char 0).

: mode  ( a -- )  os-chars !  ;
  \ Set the charset to address _a_ (the bitmap of char 0).

  \ Usage examples:
  \   graphic mode  \ print UDG instead of `a`..`}`
  \   text mode     \ ROM charset 0..127, plus UDG 128..255

El código de gestión de impacto que queda por factorizar usa shoot-y como variable temporal para un uso diferente: apuntar a un tipo de invasor que depende de la puntuación del usuario. Aún no lo entiendo bien. Además, el código aún usa el formato original de datos de invasores:

: (impact)  ( -- )
  shoot-y @ ufo-y = if  ufo-impacted exit  then

  invader-impacted

  score @ 1- 5 * 10000 + shoot-y !
  shoot-y @ 2+ c@
  shoot-y @ 3 + c@ dup 15 < abs +
  2dup at-yx ." qr"  10 100 beep at-yx 2 spaces

  shoot-y @ dup c@ 1- swap c! shoot-y @ c@ 0> if
    0 shoot-y @ 1+ c! shoot-x @ 15 < abs 29 *
    shoot-y @ 3 + c! shoot-y @ 2+ c@
    shoot-y @ 3 + c@ at-yx
    shoot-x @ 15 < if  space  then
    shoot-y @ 4 + c@ dup emit 1+ emit
  then  ;

Una primera simplificación es la siguiente, aunque hay cosas que aún no entiendo:

: (impact)  ( -- )
  shoot-y @ ufo-y = if  ufo-impacted exit  then
  invader-impacted

  invader-type @ >r

  score @ 1- invader-type !
    \ XXX TODO -- check the original

  invader-y @ invader-x @ dup 15 < abs +
  2dup at-yx ." qr"  10 100 beep at-yx 2 spaces

  -1 invader-lifes +!
  invader-lifes @ if
    invader-active off
    shoot-x @ 15 < abs 29 * invader-x !  at-invader
    shoot-x @ 15 < if  space  then
    invader-char @ .invader-chars
  then  r> invader-type !  ;
  \ Manage the impact.

2016-02-17

El código para imprimir los barriles y las paredes de ladrillo laterales, ya extraído de la palabra que hace el dibujo completo:

: containers1  ( -- )
  4 13 do
    containers-x @ dup i at-xy brick
    phase @ 0 do ." xy" loop  \ top part
    brick i 1+ at-xy brick
    phase @ 0 do ." z{" loop  \ bottom part
    brick
  -2 +loop  ;

Nuclear InvadersPara hacerlo más rápido, se puede preparar un par de cadenas que contengan el ladrillo de la izquierda y el máximo número de mitades de barriles (cinco, que es la última fase del juego). Basta con guardar la dirección del primer carácter de la cadena y calcular la longitud necesaria en cada caso. Así se eliminan los bucles interiores del bucle principal:

here 1+ s" vxyxyxyxyxy" s, constant containers-top
here 1+ s" vz{z{z{z{z{" s, constant containers-bottom

: containers1x  ( -- )
  4 13 do
    containers-x @ dup
    i    at-xy containers-top    phase @ 2* 1+ type brick
    i 1+ at-xy containers-bottom phase @ 2* 1+ type brick
  -2 +loop  ;

Se puede reducir los cálculos en el interior del bucle un poco más:

here 1+ s" vxyxyxyxyxy" s, constant containers-top
here 1+ s" vz{z{z{z{z{" s, constant containers-bottom

: containers1b  ( -- )
  4 13 do
    phase @ 2* 1+  containers-x @  2dup
    i    at-xy containers-top    swap type brick
    i 1+ at-xy containers-bottom swap type brick
  -2 +loop  ;

Y un poco más:

: containers1c  ( -- )
  phase @ 2* 1+  containers-x @
  4 13 do
    2dup 2dup
    i    at-xy containers-top    swap type brick
    i 1+ at-xy containers-bottom swap type brick
  -2 +loop  2drop  ;

La diferencia de velocidad de las tres últimas versiones no es significativa, debido a la manipulación adicional de la pila: 526, 525 y 523 cuadros de pantalla (20 ms por cuadro) por 100 ejecuciones de la palabra. Pero la primera versión, sin optimizar, con dos bucles interiores, tardaba 754 cuadros.

También el código que imprime las partes superior e inferior del contenedor de ladrillos puede optimizarse. Su versión actual tarda 4040 ms en ejecutarse 100 veces:

: brick  ( -- )  ." v"  ;

: bricks  ( n -- )  0 do  brick  loop  ;

variable containers-x
 4 constant containers-top-y
15 constant containers-bottom-y

: containers0  ( -- )
  containers-x @ containers-bottom-y at-xy
  phase @ 2* 2+ dup bricks
                    containers-x @ containers-top-y at-xy
                    bricks  ;

Utilizando una cadena el tiempo de 100 ejecuciones se reduce a 1920 ms:

here 1+ s" vvvvvvvvvvvv" s, constant bricks
  \ Compile a string of bricks and save the address
  \ of its first char.

: containers0  ( -- )
  bricks phase @ 2* 2+ 2dup
  containers-x @ containers-bottom-y  at-xy type
  containers-x @ containers-top-y     at-xy type  ;

Por el momento creo una sola palabra con todo el código, en lugar de dividirla en partes. Por otra parte, aún se podrá mejorar su aspecto sustituyendo los números por cálculos, cuando las constantes necesarias existan:

: containers  ( -- )
  15 phase @ - containers-x !
  bricks phase @ 2* 2+ 2dup
  containers-x @ containers-bottom-y  at-xy type
  containers-x @ containers-top-y     at-xy type
  phase @ 2* 1+  containers-x @
  4 13 do
    2dup 2dup
    i 1+ at-xy containers-bottom swap type brick
    i    at-xy containers-top    swap type brick
  -2 +loop  2drop  ;
  \ Draw the nuclear containers.

Nuclear InvadersHe escrito una variante que guarda la coordenada x en la pila de retorno, haciendo innecesaria la variable containers-x, pero el incremento de velocidad es despreciable: solo 60 ms menos en 1000 (mil) ejecuciones. Por tanto dejo la variable, que es más legible.

Escribo dos pequeñas palabras nuevas para poder crear gráficos con nombre, lo que resultará útil para aquellos gráficos a los que se hace referencia en el código:

: sprite  ( n0..n7 c "name" -- )  dup constant sprite!  ;
: 2sprite  ( n0..n7 c "name" -- )  dup constant 2sprite!  ;

Esta palabra aún necesita ser más legible:

: parade  ( -- )
  99 dup 103 dup 107 dup
  3 13 do
     1 i at-xy dup emit dup 1+ emit
    29 i at-xy dup emit     1+ emit
  -2 +loop  ;

Por ejemplo así, utilizando una palabra que ya existía y que imprime gráficos de 2x1:

: .2sprite  ( c -- )  dup emit 1+ emit  ;

: parade  ( -- )
  99 dup 103 dup 107 dup
  3 13 do
     1 i at-xy dup .2sprite
    29 i at-xy     .2sprite
  -2 +loop  ;

Tan solo falta crear los gráficos con un nombre.

Escribo alternativas a clear-arena, pero resultan más lentas que la original, escrita con spaces:

: clear-arena  ( -- )
  text font  0 2 at-xy 608 spaces  graphic font  ;
  \ Clear screen except the status bars.

  \ XXX BENCHMARK 1873 frames / 100 executions

608 constant /arena
create 'arena-blanks /arena allot
'arena-blanks /arena blank
'arena-blanks /arena 2constant arena-blanks

: clear-arena1  ( -- )
  text font  0 2 at-xy arena-blanks type graphic font  ;

  \ XXX BENCHMARK 2047 frames / 100 executions

create 'arena-blanks2 /arena allot
'arena-blanks2 /arena blank-char fill
'arena-blanks2 /arena 2constant arena-blanks2

: clear-arena2  ( -- )  0 2 at-xy arena-blanks2 type  ;

  \ XXX BENCHMARK 2025 frames / 100 executions

create 'arena-blanks3 here
22 c, 2 c, 0 c,  \ compile the `AT 2,0` control codes
here /arena allot /arena blank-char fill
here over - 2constant arena-blanks3

: clear-arena3  ( -- )  arena-blanks3 type  ;

  \ XXX BENCHMARK 2025 frames / 100 executions

2016-02-18

Hago unas pruebas de velocidad con código alternativo para imprimir la línea de separación de la barra de estado, aunque tarde o temprano usaré draw para dibujar una línea, en lugar de imprimir gráficos (como no quedaba más remedio en la versión de Jupiter ACE):

: status-bar-ruler  ( -- )
  1 [ status-bar-y 1- ] literal at-xy
  ." wwwwwwwwwwwwwwwwwwwwwwwwwwwwww"  ;
  \ XXX TODO use a line instead
  \ XXX benchmark: 1081 frames / 1000 executions

: status-bar-ruler2  ( -- )
  1 [ status-bar-y 1- ] literal at-xy 119 30 emits  ;
  \ XXX benchmark: 982 frames / 1000 executions

: status-bar-ruler3  ( -- )
  1 [ status-bar-y 1- ] literal at-xy ruler 30 emits  ;
  \ XXX benchmark: 982 frames / 1000 executions

Había un fallo importante, que causaba el bloqueo del sistema: Faltaba un dup en la conversión hecha del incrementador de fase:

: next-phase  ( -- )
  invaders @ ?exit
  phase dup @ 5 < abs +! 100 pause init-round  ;

La impresión de vidas necesitaba una simplificación, para hacer más fácil su uso en dos ocasiones diferentes: cuando se muestra la barra de estado por primera vez y cuando se actualiza tras decremetar las vidas disponibles. Así, ahora el número de iconos mostrado se corresponde con el número de vidas:

: life-icons  ( -- ca len )  ~~ s" mnmnmn" drop lifes @ 2*  ;
  \ String containing as much life icons as current lifes.

: .life-icons  ( -- )  ~~  life-icons type ." ||"  ;
  \ Print one icon for each remaining life.

: .lifes  ( -- )  ~~
  font@  rom-font 2 status-bar-y at-xy lifes ?
         graphic-font .life-icons
  font!  ;
  \ Print number of lifes and life icons.

Nuclear InvadersPrimera ejecución del juego sin bloqueos, aunque aún hay parte de la lógica del bucle principal, el control de vidas de los invasores y del jugador, que no está bien.

Factorizo el cálculo del sentido de avance de los invasores (los tipos 0..4 avanzan hacia la derecha, y los tipos 5...9 hacia la izquierda), y hago unas mediciones de velocidad (de palabras intermedias que descartan el resultado con drop):

: invader-direction  ( -- 1 | -1 )
  1 invader-type @ 5 < 0= ?negate  ;
  \ XXX BENCHMARKED: 261 frames / 10000 executions

: invader-direction2  ( -- 1 | -1 )
  -1 invader-type @ 5 < ?negate  ;
  \ XXX BENCHMARKED: 245 frames / 10000 executions

: invader-direction3  ( -- 1 | -1 )
  invader-type @ 4 > 1 or  ;
  \ XXX BENCHMARKED: 248 frames / 10000 executions

Suponía que la última versión sería la más rápida, pero, viendo el código en ensamblador de or, es evidente que tiene que ser más lento que ?negate. En todo caso, esta información se guardará en la tabla de datos de invasores en lugar de calcularla cada vez, y será mucho más rápido.

He extraído de move-active-invader el código que comprueba si el invasor se ha chocado con la central, y he movido su llamada a (move-invader), lo que parece más lógico, pues es en esta palabra donde debe devolverse un falso para los invasores inactivos.

: containers-reached?  ( -- f )
  invader-x @
  dup 14 phase @ - = if  drop true exit  then
      15 phase @ + =  ;
  \ XXX TODO only one calculation, depending on
  \ the direction

: move-active-invader  ( -- ) ~~
  invader-direction dup invader-x +! .invader ;

: (move-invader)  ( -- )  ~~
  invader-active @
  if    move-active-invader containers-reached?
  else  move-inactive-invader false
  then  ;

2016-02-19

En el listado original faltaban los datos del quinto tipo de invasor. Me extrañaba que hubiera 9 en lugar de 10. Y además de eso yo había puesto mal el número de celdas que ocupa cada registro en la tabla... Por todo ello el sistema se terminaba corrompiendo. Tras hacer estas dos correcciones los invasores por fin se mueven y el programa no se cuelga.

Hay un problema extraño con la lectura de teclas en move-tank: La segunda tecla leída en el código funciona lentamente, como si no fuera detectada siempre. Cambiando el el orden de las lecturas se confirma el problema. Quizá tenga que ver con cómo en Solo Forth inkey lee del sistema operativo la tecla disponible:

: move-tank  ( -- )
  tank-x @  inkey right-key = abs +
            inkey left-key  =     +
  tank-limit dup tank-x !
  1- tank-y at-xy ." |mn|" ;

El problema se arregla haciendo una solo lectura de teclado en lugar de dos tan seguidas. Parece que las dos lecturas tenían lugar más rápido de lo que las interrupciones del sistema actualizan el teclado.

: move-tank  ( -- ) ~~
  inkey tank-x @ over left-key  =     +
                 swap right-key = abs +
  tank-limit dup tank-x !
  1- tank-y at-xy ." |mn|" ;
  \ Move the tank depending on the key pressed.

Una de las simplificaciones pendientes:

: impact?  ( -- f )  \ XXX OLD
  missed? if  false  else  impact true  then  ;
  \ Did the projectil impacted?
  \ If so, do manage the impact.

: impact?  ( -- f )  \ XXX NEW
  missed? 0= dup if  impact  then  ;
  \ Did the projectil impacted?
  \ If so, do manage the impact.

El número total de invasores se guarda en la variable invaders, que se inicializa a 30:

: init-invaders  ( -- )
  init-invaders-data  0 invader-type !  30 invaders !  ;

Pero esa misma cantidad ya existe en la tabla de datos de invasores, pues es la suma de las unidades existentes de cada tipo. La ventaja de guardar el total en una variable es que durante el juego es más rápido consultar una variable que calcular la suma de las unidades de la tabla. Pero durante la inicialización del juego sí es lógico hacer el cálculo, para no duplicar información:

: total-invaders  ( -- n )
  0
  invader-types 0 do
    i invader-type ! invader-units @ +
  loop  ;
  \ Total number of invaders (sum of units of all invader
  \ types).

: init-invaders  ( -- )
  init-invaders-data  0 invader-type !
  total-invaders invaders !  ;

El movimiento de los invasores de izquierda a derecha empezó a fallar y no averigüé la causa del cambio. Reescribí el código de otra forma:

: .invader  ( -- )  invader-frame .2sprite  ;
  \ Print the current invader.

: flying-invader  ( -- )
  at-invader invader-direction@ dup 0>
  if    blank-char emit .invader invader-x +!
  else  invader-x +! .invader blank-char emit  then  ;

Hasta ahora la bandera que indica si los invasores han alcanzado los contenedores se devuelve en la pila a través de las varias palabras que gestionan la invasión. Para simplificar, la guardo en una variable nueva, catastrophe. Todo el código relacionado es el siguiente:

variable catastrophe   \ flag (game end condition)

: init-combat  ( -- )
  catastrophe off
  init-invaders init-ufo init-tank init-arena  ;

: containers-reached?  ( -- f )
  invader-x @
  dup 14 phase @ - = if  drop true exit  then
      15 phase @ + =  ;
  \ XXX TODO only one calculation, depending on
  \ the direction
  \ XXX TODO use x coordinates of the containers,
  \ which now are calculated when `phase` changes

: damages  ( -- )  containers-reached? catastrophe !  ;

: move-invader  ( -- )
  invader-active @
  if  flying-invader damages  else  activate-invader  then  ;

: invasion  ( -- )
  invader-units @ if  move-invader  then  next-invader  ;
  \ Move the current invader, if there are units left of it,
  \ and then choose the next one. _f_ is true when
  \ the invader has reached the containers.

: (combat)  ( -- )
  begin   2 ms
          break-key? if  rom-font quit  then  \ XXX TMP
          drive shoot ufo next-phase invasion
          catastrophe @
  until   tune  30 ms  dead  ;

Hago un cronometraje para elegir la versión más rápida de increase-level. La diferencia entre las dos versiones es muy pequeña, pero todo el tiempo de proceso que se pueda ahorrar servirá para añadir nuevas funcionalidades, como color y sonido. Resultados:

: increase-level1  ( -- )  level @    max-level < abs level +!  ;
  \ XXX BENCHMARKED --  616 frames / 30000 executions -- 1.00

: increase-level2  ( -- )  level @ 1+ max-level min   level +!  ;
  \ XXX BENCHMARKED --  599 frames / 30000 executions -- 0.97

Tras los últimos cambios, el control de colisión con los contenedores necesita una puesta al día. Ahora da positivo nada más atravesar la pared de ladrillo:

: containers-reached?  ( -- f )
  invader-x @
  dup 14 level @ - = if  drop true exit  then
      15 level @ + =  ;
  \ XXX TODO only one calculation, depending on
  \ the direction
  \ XXX TODO use x coordinates of the containers,
  \ which now are calculated when `level` changes

2016-02-20

Nuevo sistema para leer directamente las teclas, en lugar de usar inkey. Esto permite detectar la pulsación de una tecla aunque haya varias pulsadas.

El cálculo que hice del tamaño del edificio en función del nivel de juego no está bien, lo que provoca que los invasores choquen con los contenedores antes de llegar a ellos:

: set-building-size  ( -- )
  [ columns 2/ 1- ] literal level @ -
  dup building-left-x !
  dup 1+ containers-left-x !
      columns swap - dup containers-right-x !
                         1+ building-right-x !  ;
  \ Set the size of the building after the current level.

La versión corregida es la siguiente:

: set-building-size  ( -- )
  level @ 2* 2+  building-width !
  [ columns 2/ 1- ] literal \ half of the screen
  level @ \ half width of all containers
  2dup 1- - containers-left-x !
  2dup    - building-left-x !
  2dup    + containers-right-x !
       1+ + building-right-x !  ;
  \ Set the size of the building after the current level.

La nueva versión incluye el cálculo del ancho del edificio, que antes se hacía en building. No tiene ventaja no hacerlo por anticipado, como el resto de los valores.

2016-02-21

Nuclear InvadersEmpiezo a ocuparme del disparo, que aún no funciona. Para empezar, init-ocr necesitaba un 1+ para calcular correctamente el número de caracteres a examinar:

: init-ocr  ( -- )
  udg-set ocr-charset !  \ address of the first printable char
  first-sprite ocr-first !  \ its char code
  last-sprite @ first-sprite - 1+ ocr-chars !  ;  \ chars
  \ Set `ocr` to work with the sprites:
  \ Set the address of the first printable char to be
  \ examined, its char code and the number of examined chars.

Finalmente la mayor parte del código relacionado con el impacto ha sido reescrita, y ya funciona bien:

: impacted-invader  ( -- n )  ~~
  projectile-y @ [ building-top-y 1+ ] literal - 2/
  projectile-x @ [ columns 2/ ] literal > abs 5 * +  ;
  \ Invader type impacted calculated from the projectile
  \ coordinates: Invader types 0 and 5 are at the top, one row
  \ below the top of the building, types 1 and 6 are two lines
  \ below and so on. Types 0..4 are at the left of the
  \ screen; types 5..9 are at the right.

: -invader  ( -- )  at-invader ." ||"  ;
  \ Delete the current invader.

: invader-go-home  ( -- )  ~~
  invader-default-x@ invader-x !  at-invader
  invader-char@ .2sprite  ;

: current-invader-impacted  ( -- )  ~~
  invader-bonus invader-explosion
  -1 invaders +!  -1 invader-units +!
  invader-units @
  if  invader-active off  -invader  invader-go-home  then  ;

: invader-impacted  ( -- )  ~~
  invader-type @ >r  impacted-invader invader-type !
  current-invader-impacted  r> invader-type !  ;  -->
  \ A invader has been impacted by the projectile.
  \ Calculate its type, set it the current one and manage it.

2016-02-22

Corrijo un fallo extraño introducido recientemente, que provocaba el cuelgue del sistema durante la interpretación de la fuente: la constante con el número de GDU no estaba actualizada y el último gráfico corrompía el código posterior a la tabla de gráficos.

Primeros cambios para implementar dos jugadores.

Primeros cambios para cambiar los códigos de los GDU, abandonando las letras minúsculas usadas por la versión original en favor de los códigos propios de Solo Forth (128..255). Esto hará innecesario cambiar el juego de caracteres según haya que imprimir textos o gráficos, y además permitirá mezclar texto y gráficos en la misma cadena.

2016-02-23

El método empleado hasta ahora para definir los gráficos obligaba a elegir los GDU asociados, así como a incluir esos mismos GDU dentro de las cadenas que sirven para imprimir un gráfico de 2x1 en una sola operación:

: 1x1sprite!  ( b0..b7 c -- )
  /udg 0 do
    dup /udg 1+ i - roll i scan!
  loop  drop  ;
  \ Store a 1x1 UDG sprite into char _c_.

: 1x1sprite  ( n0..n7 c "name" -- )  dup constant 1x1sprite!  ;

: 2x1sprite!  ( n0..n7 c -- )
  /udg 0 do
    dup /udg 1+ i - pick flip i scan! 1+  \ first UDG
    dup /udg 1+ i - roll      i scan! 1-  \ second UDG
  loop  drop  ;
  \ Store a 2x1 UDG sprite into chars _c_ and _c_+1.
  \ Scans _n0..n7_ are 16-bit: high part is char _c_,
  \ and low part is _c_+1.

: 2x1sprite  ( n0..n7 c "name" -- )  dup constant 2x1sprite!  ;

: .2x1sprite  ( c -- )  dup emit 1+ emit  ;  -->

Nuclear InvadersHe implementado un nuevo método de definición de gráficos que asigna automáticamente los códigos GDU. De esta forma las definiciones de gráficos pueden reordenarse en la fuente sin necesidad de recalcular a mano sus códigos de GDU, lo cual hará muy fácil añadir fotogramas adicionales a algunos de los gráficos, así como agruparlos para reducir al mínimo el número de GDU que examina la palabra ocr para detectar colisiones. Otra ventaja es que una palabra nueva, sprite-string, permite crear una cadena correspondiente al último gráfico definido, sin necesidad de saber en qué GDU está almacenado. El nuevo código es el siguiente:

( latest-sprite-width latest-sprite-height )

$80 constant first-udg
$FF constant last-udg
    variable >udg  first-udg >udg !  \ next free UDG

variable latest-sprite-width
variable latest-sprite-height
variable latest-sprite-udg

: ?udg  ( c -- )  last-udg > abort" Too many UDGs"  ;
  \ Abort if UDG _n_ is too high.

: free-udg  ( n -- c )
  >udg @ dup latest-sprite-udg !
  tuck +  dup >udg !  1- ?udg  ;
  \ Free _n_ consecutive UDGs and return the first one _c_.

: latest-sprite-size!  ( width height -- )
  latest-sprite-height !  latest-sprite-width !  ;
  \ Update the size of the latest sprited defined.

-->

( sprite-string )

: ?sprite-height  ( -- )
  latest-sprite-height @ 1 >
  abort" Sprite height not supported for sprite strings"  ;

: sprite-string  ( "name" -- )
  ?sprite-height
  here latest-sprite-udg @  latest-sprite-width @ dup c,
  0 ?do  dup c, 1+  loop  drop  ;
  \ Create a definition "name" that will return a string
  \ containing all UDGs of the lastest sprite defined.

( 1x1sprite! 1x1sprite )

: (1x1sprite!)  ( b0..b7 c -- )
  1 ?free-udg  1 1 latest-sprite-size!
  /udg 0 do
    dup /udg 1+ i - roll i scan!
  loop  drop  ;
  \ Store a 1x1 UDG sprite into UDG _c_.

: 1x1sprite!  ( b0..b7 -- )
  1 free-udg (1x1sprite!)  ;
  \ Store a 1x1 UDG sprite into the next available UDG.

: 1x1sprite  ( n0..n7 "name" -- )
  1 free-udg dup constant (1x1sprite!)  ;

' emit alias .1x1sprite  ( c -- )

-->

( 2x1sprite! 2x1sprite )

: (2x1sprite!)  ( n0..n7 c -- )
  2 ?free-udg  2 1 latest-sprite-size!
  /udg 0 do
    dup /udg 1+ i - pick flip i scan! 1+  \ first UDG
    dup /udg 1+ i - roll      i scan! 1-  \ second UDG
  loop  drop  ;
  \ Store a 2x1 UDG sprite into chars _c_ and _c_+1.
  \ Scans _n0..n7_ are 16-bit: high part is char _c_,
  \ and low part is _c_+1.

: 2x1sprite!  ( n0..n7 -- )
  2 free-udg (2x1sprite!)  ;
  \ Store a 2x1 UDG sprite into the next two available UDGs.
  \ Scans _n0..n7_ are 16-bit: high part is char _c_, and low
  \ part is _c_+1.

: 2x1sprite  ( n0..n7 "name" -- )
  2 free-udg dup constant (2x1sprite!)  ;

: .2x1sprite  ( c -- )  dup emit 1+ emit  ;

La última variante provisional de la lista de controles, que usa tres gráficos como iconos, no queda bien, porque al cambiar los controles cambia la posición de los nombres de las teclas y de los iconos:

  \ XXX OLD
  \ : .control  ( n -- )  ."  = " .kk# 4 spaces  ;
  \ : .controls  ( -- )
  \   row dup s" [Space] to change controls:" rot center-type
  \   9 over 2+  at-xy ." Left " kk-left#  .control
  \   9 over 3 + at-xy ." Right" kk-right# .control
  \   9 swap 4 + at-xy ." Fire " kk-fire#  .control  ;
  \   \ Print controls at the current row.

: left-key$   ( -- ca len )  kk-left# kk#>string  ;
: right-key$  ( -- ca len )  kk-right# kk#>string  ;
: fire-key$   ( -- ca len )  kk-fire# kk#>string  ;

: controls$  ( -- ca len )
  left-arrow$ left-key$ s+
  s"   " s+ fire-key$ s+ s"   " s+
  right-key$ s+ right-arrow$ s+  ;
  \ String containing the description of the current controls.
  \ XXX TMP --
  \ XXX TODO -- rewrite

: .controls  ( -- )
  s" [Space] to change controls:" row dup >r center-type
  fire-button$ r@ 2+ center-type
  0 r@ 3 + at-xy columns spaces
  controls$ r> 3 + center-type  ;
  \ Print controls at the current row.
  \ XXX TMP --

2016-02-24

Añado color a los gráficos. Para ello escribo la palabra color, que a su vez crea palabras que modifican los atributos temporales:

( Colors) debug-point

0 constant black    1 constant blue   2 constant red
3 constant magenta  4 constant green  5 constant cyan
6 constant yellow   7 constant white

: papery   ( color -- paper-attribute )           8 *  ;
: brighty  ( attribute -- brighty-attribute )   64 or  ;
: flashy   ( attribute -- flashy-attribute )   128 or  ;

-->

( Colors) debug-point

: set-color  ( b -- )  23695 c!  ;
  \ Set the attribute _b_ as the current color, by modifying
  \ the system variable ATTR T (temporary attributes).

: color  ( b "name" -- )
  create c,  does> ( -- )  ( pfa ) c@ set-color  ;
  \ Create a word "name" that will set attribute _b_ as the
  \ current color.

              white color text-color
              black color arena-color
 white papery red + color brick-color
       blue brighty color tank-color
               blue color life-color
              green color invader-color
     yellow brighty color container-color
            magenta color ufo-color

Creo un efecto de rotura en los muros, cuando un invasor choca con ellos y penetra en el edificio:

( invasion )

variable broken-wall-x
  \ Column of the wall broken by the current alien.

: flying-to-the-right?  ( -- f )  invader-x-inc@ 0>  ;
  \ Is the current invader flying to the right?

red papery c,  here  red c,  constant broken-brick-colors
  \ Address of the broken brick color used for the right
  \ side of the building; the address before contains
  \ the color used for the left side of the building.
  \ This is faster than pointing to a list of two colors,
  \ because a flag (0|-1) can be used as index.

: broken-wall-color  ( -- )
  broken-brick-colors flying-to-the-right? + c@ set-color  ;
  \ Set the color of the broken wall.  This depends on the side
  \ of the building, because the same two UDGs are used for
  \ both sides, with inverted colors.

: left-broken-wall  ( -- )
  broken-wall-x @ dup
  invader-y @ 1- at-xy bottom-broken-brick .1x1sprite
  invader-y @ 1+ at-xy top-broken-brick  .1x1sprite  ;

: right-broken-wall  ( -- )
  broken-wall-x @ dup
  invader-y @ 1- at-xy top-broken-brick .1x1sprite
  invader-y @ 1+ at-xy bottom-broken-brick .1x1sprite  ;

-->

( invasion )

: broken-wall  ( -- )
  broken-wall-color flying-to-the-right?
  if  left-broken-wall  else  right-broken-wall  then  ;
  \ Show the broken wall of the building, hit by the current invader.

: broken-wall?  ( -- f )
  invader-x @ flying-to-the-right?
  if    1+ building-left-x
  else  building-right-x
  then  @ dup broken-wall-x ! =  ;
  \ Has the current invader broken the wall of the building?

-->

( invasion )

: broken-container?  ( -- f )
  invader-x @ flying-to-the-right?
  if    1+ containers-left-x
  else  containers-right-x  then  @ =  ;
  \ Has the current invader broken a container?

: damages  ( -- )
  broken-wall? if  broken-wall exit  then
  broken-container? catastrophe !  ;
  \ Manage the possible damages caused by the current invader.

Nuclear InvadersAumento de dos a cuatro el número de fotogramas de los invasores y del ovni.

Muevo la posición inicial de los invasores a los bordes de la pantalla. En el juego original no se usaban la primera y la última columnas. Para ello, aparte de actualizar las constantes correspondientes, solo ha hecho falta imprimir de forma condicional uno de los espacios de flying-invader:

: ?space   ( -- )  column if  space  then  ;
  \ Print a space, if current column is not zero.  This is used
  \ to print a space after the current invader, in order to
  \ delete its old position, but not when the invader is at the
  \ right margin of the screen.

: flying-invader  ( -- )
  invader-x-inc@ dup 0>  \ flying to the right?
  if    at-invader space .invader invader-x +!
  else  invader-x +! at-invader .invader ?space  then  ;

Al ampliar el rango de movimiento del tanque, me fijo en que el algoritmo que había no es eficaz, pues redibuja el gráfico incluso cuando el jugador no lo ha movido:

: drive  ( -- )
  tank-x @ kk-left  pressed? +
           kk-right pressed? abs +
  tank-limit dup tank-x ! tank-y
  at-xy tank-color ?space tank$ type ?space  ;

Distinguir las pulsaciones de teclas, en lugar de sumar sus efectos como se hace ahora, tendría la desventaja de que una tecla tendría prioridad sobre la otra. Ahora, por el contrario, pulsar las dos teclas de movimiento hace que los incrementos se compensen y el tanque no se mueva. Para conservar este efecto y al mismo tiempo evitar que el tanque sea redibujado cuando no se mueve, reescribo la palabra.

2016-02-25

Nuclear InvadersMejoro el efecto final, mostrando los contenedores rotos con un gráfico específico, igual que ya hice con la rotura de la pared de ladrillo:

( invasion )

: left-broken-container  ( -- )
  invader-x @ 2+ invader-y @ at-xy
  top-right-broken-container .1x1sprite
  invader-x @ 1+ invader-y @ 1+ at-xy
  bottom-left-broken-container .1x1sprite  ;
  \ Broke the container on its left side.

: right-broken-container  ( -- )
  invader-x @ 1- invader-y @ at-xy
  top-left-broken-container .1x1sprite
  invader-x @ invader-y @ 1+ at-xy
  bottom-right-broken-container .1x1sprite  ;
  \ Broke the container on its right side.

-->

( invasion )

: broken-container  ( -- )
  container-color
  flying-to-the-right?  if    left-broken-container
                        else  right-broken-container  then  ;
  \ Broke the container.

: broken-container?  ( -- f )
  invader-x @ flying-to-the-right?
  if    1+ containers-left-x
  else     containers-right-x  then  @ =  ;
  \ Has the current invader broken a container?

2016-03-22

Hago la compilación del juego independiente de Solo Forth. Hasta ahora la fuente del juego era una parte más de la librería de Solo Forth. Ahora el juego tiene su propio Makefile, que crea una imagen de disquete MGT a partir de un enlace a la librería de Solo Forth y de las fuentes del juego.

2016-03-24

Actualizo los enlaces a la librería de Solo Forth, que ha sido dividida en muchos ficheros. Ahora solo se incluyen en el disquete los módulos necesarios.

Corrijo row>pixel.

2016-03-25

Corrijo y mejoro la impresión de la barra de estado y de las vidas.

Corrijo el número de controles disponibles: el bucle de selección mostraba uno más, inexistente en la tabla.

2016-03-30

Cambio el método de impresión de la línea que separa la zona de juego de la barra de estado. En lugar del lento adraw usado hasta ahora, uso un fill en la memoria de vídeo:

: ruler  ( -- )
  [ 0 tank-y row>pixel 8 - pixel-addr nip ] literal
  columns $FF fill  ;
  \ Draw the ruler of the status bar.

2016-04-20

Actualizo el núcleo y la librería de Solo Forth a la versión 0.5.0-pre.28+20160420.

2016-05-08

Actualizo el núcleo y la librería de Solo Forth a la versión 0.7.0-pre.78.

2016-05-13

Actualizo el núcleo y la librería de Solo Forth a la versión 0.9.0-pre.15. Esta versión de Solo Forth añade load-section, que permite compilar un grupo de bloques como una unidad. Esto permite cambiar el formato de la fuente principal de Nuclear Invaders de FSB a texto corriente, sin límites de bloques. Inicio la conversión. Hace falta actualizar el fichero Makefile y el bloque número 1, que sirve de cargador.

2016-08-01

Muevo a la librería de Solo Forth las constantes y modificadores de color. Actualizo a la versión 0.10.0-pre.23+20160801 de Solo Forth.

2016-10-04

Actualizo a la versión 0.10.0-pre.71+20160921 de Solo Forth. Añado instrucciones para compilar el juego.

2016-10-11

Nuclear InvadersAumento el ancho del tanque, para facilitar la gestión de disparos. Para facilitar la definición visual de un gráfico de 3x1 GDU en la fuente escribo la palabra udg-row[ para la librería de Solo Forth. Resumen de los cambios:

true constant [big-tank] immediate  \ XXX TMP --

  [big-tank] [if]  \ XXX NEW

  \ XXX TODO --

#3 constant udg/tank  #3 free-udg udg-row[

000000000001100000000000
000000000001100000000000
000000000001100000000000
001111111111111111111100
011111111111111111111110
111111111111111111111111
111111111111111111111111
111111111111111111111111
]udg-row  udg/tank 1 latest-sprite-size!

  [else]  \ XXX OLD

  2 constant udg/tank

  0000000100000000
  0000001110000000
  0000001110000000
  0111111111111100
  1111111111111110
  1111111111111110
  1111111111111110
  1111111111111110  2x1sprite!  [then]

sprite-string tank$  ( -- ca len )

  [big-tank] [if]  \ XXX NEW

00011000
00011000
00011000
00011000
00011000
00011000
00011000
00000000

  [else]  \ XXX OLD

00000000
00000001
00000001
00000001
00000001
00000001
00000000
00000000

  [then]

1x1sprite projectile

: fire  ( -- )
  tank-x @  [big-tank] [if]  1+  [then]  projectile-x !
  [ tank-y 1- ] literal projectile-y !  fire-sound  ;
  \ The tank fires.

Aspecto actual de la pantalla inicial (aún provisional):

Nuclear Invaders

2016-10-12

Termino la primera prueba para lograr cuatro disparos simultáneos. El método es demasiado lento. Pantallazo y resumen de cambios:

Nuclear Invaders

false constant [single-projectile] immediate
  \ XXX UNDER DEVELOPMENT

[single-projectile] [if]

  \ XXX OLD -- one single projectile on the screen

variable projectile-x  \ column
variable projectile-y  \ row (0 if no shoot)

: init-projectiles  ( -- )  projectile-y off  ;

[else]

  \ XXX NEW -- 4 projectiles on the screen
  \ XXX FIXME --

%11 constant max-projectile#
  \ Bitmask for the projectile counter (0..3).

max-projectile# 1+ constant #projectiles
  \ Maximum number of projectiles.

variable projectile#  projectile# off
  \ Number of the current projectile (0..3).

#projectiles cells constant /projectiles
  \ Bytes used by the projectiles' tables.

create 'projectile-x /projectiles allot
create 'projectile-y /projectiles allot
  \ Tables for the coordinates of all projectiles.

: >projectile  ( -- n )  projectile# @ cells  ;
  \ Current projectile offset _n_ in the projectiles' tables.

: projectile-x  ( -- a )  >projectile 'projectile-x +  ;
: projectile-y  ( -- a )  >projectile 'projectile-y +  ;
  \ Fake variables for the coordinates of the current
  \ projectile.

: init-projectiles  ( -- )
  'projectile-y /projectiles erase  ;

[then]

[single-projectile] [if]

: shoot  ( -- )
  shooted? if  shooted exit  then
  fire? if  fire  then  ;
  \ Manage the shoot.

[else]

: next-projectile  ( -- )
  projectile# @ 1+ max-projectile# and projectile# !  ;
  \ Point to the next current projectile.

: shoot  ( -- )
  projectile# @ 30 23 at-xy .  \ XXX INFORMER
  shooted? if  shooted next-projectile  then
  fire? if  fire next-projectile  then  ;
  \ Manage the shoot.

[then]

2016-10-13

Pruebo un método alternativo para manejar los proyectiles simultáneos, llevando la cuenta. Sigue siendo demasiado lento y no soluciona otros problemas pendientes.

2016-10-14

Intento otro enfoque: guardo en una pila extra los identificadores de los proyectiles libres, los que no están volando. Funciona bien, pero es necesario limitar la cadencia de disparo.

Retoco los gráficos del tanque y del proyectil para hacer éste doble.

2016-10-15

Primeros cambios para que los proyectiles sean un punto en lugar de un GDU.

Muestro la versión en la pantalla de título, que aún es provisional.

Nuclear Invaders

2016-10-16

Termino el código alternativo (con compilación condicional) para mostrar los proyectiles como puntos.

Dejo el tanque de 3x1 como definitivo y borro el código antiguo que usaba un gráfico de 2x1. El nuevo tamaño permite una coordenada horizontal del proyectil más lógica y jugable.

Acelero el código de manejo de proyectiles, usando variables de octeto.

Arreglo el número de iconos de las vidas restantes, para que incluya el tanque actual.

Creo diez fotogramas para los proyectiles de UDG, que se muestran aleatoriamente durante el vuelo de cada proyectil.

Nuclear Invaders

Nuclear Invaders

2016-10-21

Añado puntuación por pasar de nivel, dependiende de éste y de los proyectiles usados.

Elimino el código alternativo para usar un único proyectil.

2016-10-22

Mejoro la puntuación por pasar de nivel, añadiendo un valor dependiente del número de vidas que quedan:

: level-bonus  ( -- n )
  level @ 100 *  lifes @ 10 * +  used-projectiles @ -  0 max  ;
  \ Return bonus _n_ after finishing a level.

Mejoro la representación gráfica de los agujeros en la pared izquierda del edificio. Hasta ahora se usaban los mismos gráficos que en la pared derecha, con los colores de tinta y papel invertidos. Esto ahorraba dos UDG pero el efecto de radiación descubría el truco, como se ve en el siguiente pantallazo:

Nuclear Invaders

Tras usar gráficos diferentes para los agujeros de cada lado del edificio, el fallo queda corregido:

Nuclear Invaders

Hago los primeros cambios para permitir más de un jugador.

2016-11-17

Hago las primeras pruebas para hacer que la velocidad de los invasores sea siempre la misma, independientemente de su número.

A partir de esta fecha, esta página no será actualizada. El desarrollo podrá seguirse en el repositorio en GitHub.