0x09 Ensamblador ZX Spectrum Marciano – Comienza la partida

En este capítulo de Ensamblador ZX Spectrum Marciano, vamos a implementar el inicio y el fin de la partida.

Al igual que en capítulos anteriores, creamos la carpeta Paso09 y copiamos desde la carpeta Paso08 los archivos Cargador.tap, Const.asm, Ctrl.asm, Game.asm, Graph.asm, Int.asm, Main.asm, make o make.bat, Print.asm y Var.asm.

Antes de empezar con el objetivo de este capítulo, vamos a revisar la rutina PrintString para ver dos variaciones.

Rutina PrintString

Las variaciones que vamos a ver de la rutina PrintString las vamos a implementar en un nuevo archivo que vamos a llamar TestPrint.asm, luego decidiremos que rutina va a ser la definitiva.

Abrimos el archivo TestPrint.asm y añadimos el siguiente código.

org     $5dad

TestPrint:
ld      hl, string
ld      b, stringEOF - string
call    PrintString

ld      hl, stringNull
call    PrintStringNull

ld      hl, stringFF
call    PrintStringFF

ret

; -----------------------------------------------------------------------------
; Pinta cadenas.
;
; Entrada:  HL = primera posición de memoria de la cadena
;           B  = longitud de la cadena.
;
; Altera el valor de los registros AF y HL
; -----------------------------------------------------------------------------
PrintString:
ld      a, (hl)         ; Carga en A el carácter a pintar
rst     $10             ; Pinta el carácter
inc     hl              ; Apunta HL al siguiente carácter
djnz    PrintString     ; Hasta que B valga 0

ret

; -----------------------------------------------------------------------------
; Pinta cadenas.
;
; Entrada:  HL = primera posición de memoria de la cadena
;
; Altera el valor de los registros AF y HL
; -----------------------------------------------------------------------------
PrintStringNull:
ld      a, (hl)         ; Carga en A el carácter a pintar
or      a               ; Comprueba si es 0
ret     z               ; De ser así, sale
rst     $10             ; Pinta el carácter
inc     hl              ; Apunta HL al siguiente carácter
jr      PrintStringNull ; Bucle hasta que termine de pintar la cadena

; -----------------------------------------------------------------------------
; Pinta cadenas.
;
; Entrada:  HL = primera posición de memoria de la cadena
;
; Altera el valor de los registros AF y HL
; -----------------------------------------------------------------------------
PrintStringFF:
ld      a, (hl)         ; Carga en A el carácter a pintar
cp      $ff             ; Comprueba si es $FF
ret     z               ; De ser así sale
rst     $10             ; Pinta el carácter
inc     hl              ; Apunta HL al siguiente carácter
jr      PrintStringFF   ; Bucle hasta que termine de pintar la cadena

string:
db      $10, $05, $11, $03, $16, $05, $0a, "Hola Ensamblador"
stringEOF:
db      $00

stringNull:
db      $10, $07, $11, $01, $16, $07, $0a, "Hola Ensamblador", $00

stringFF:
db      $10, $02, $11, $07, $16, $09, $0a, "Hola Ensamblador", $ff

end     TestPrint

En este código podemos ver tres rutinas PrintString:

  • PrintString: la rutina tal cual la tenemos ahora.
  • PrintStringNull: rutina que pinta cadenas y usa como fin de cadena el carácter nulo ($00).
  • PrintStringFF: rutina que pinta cadenas y usa como fin de cadena el carácter $FF.

La primera de estás rutinas ya la conocemos, por lo que vamos a explicar la rutina PrintStringNull, ya que PrintStringFF solo se diferencia en una línea a ésta.

PrintStringNull:
ld a, (hl)
or a
ret z
rst $10
inc hl
jr PrintStringNull

PrintStringNull y PrintStringFF, reciben en HL la primera posición de la cadena (al igual que PrintString), pero no necesitan conocer la longitud de la misma.

Cargamos en A el carácter al que apunta HL, LD A, (HL), comprobamos si es cero, OR A, y salimos si es así, RET Z. La línea que cambia en PrintStringFF con respecto a PrintStrinNull es OR A, que la cambiamos por CP $FF, ya que $FF es el carácter que se usa en este caso como fin de cadena. Debemos recordar que el resultado de OR A solo es cero si A vale cero.

Si el carácter cargado en A no es el de fin de cadena, pintamos el carácter, RST $10, apuntamos HL al siguiente carácter, INC HL, y seguimos en bucle hasta que pinte toda la cadena, JR PrintStringNull.

El uso de una u otra rutina tiene sus pros y sus contras. La primera comparativa la vamos a realizar sobre bytes y ciclos de reloj.


BytesCiclos
PrintString647/42
PrintStringNull751/45
PrintStringFF854/48
Ensamblador ZX Spectrum, PrintString

Si vemos esta tabla, la rutina más optima es la primera ya que ocupa menos bytes y es más rápida. La realidad es que sí es más rápida, pero no ocupa menos bytes, ya que cada vez que la llamemos hay que añadir dos bytes de cargar en B la longitud de la cadena. Si usamos mucho esta rutina, rápidamente vemos que el ahorro de bytes no es tal.

La opción lógica es la segunda rutina, que es más rápida y ocupa menos bytes que la tercera, pero como vamos a ver más adelante, tiene sus desventajas.

TestPrint es un programa individual, para compilarlo tenemos que invocar PASMO desde la línea de comandos:

pasmo ––name TestPrint ––tapbas TestPrint.asm TestPrint.tap

Antes de continuar, vamos a compilar el programa TestPrint, lo cargamos en el emulador y vemos los resultados.

Ensamblador ZX Spectrum, comienza la partida
Ensamblador ZX Spectrum, comienza la partida

Como podemos ver, todo ha ido bien. Tenemos tres cadenas, y hemos pintado cada una con una rutina distinta.

Vamos a ver ahora los inconvenientes de la segunda rutina, para lo cual vamos a modificar la segunda cadena, que ahora es:

stringNull:
db $10, $07, $11, $01, $16, $07, $0a, «Hola Ensamblador», $00

Y vamos a modificar el segundo byte, $07, por $00.

stringNull:
db $10, $00, $11, $01, $16, $07, $0a, «Hola Ensamblador», $00

Compilamos, cargamos en el emulador y vemos los resultados.

Ensamblador ZX Spectrum, comienza la partida
Ensamblador ZX Spectrum, comienza la partida

Aquí hay algo que no funciona, pero ¿qué? Revisemos la definición de las cadenas.

string:
db $10, $05, $11, $03, $16, $05, $0a, «Hola Ensamblador»
stringEOF:
db $00

stringNull:
db $10, $00, $11, $01, $16, $07, $0a, «Hola Ensamblador», $00

stringFF:
db $10, $02, $11, $07, $16, $09, $0a, «Hola Ensamblador», $ff

La segunda cadena, stringNull, la terminamos con $00 porque la rutina pinta hasta que se encuentra este valor. Todas las cadenas empiezan con $10, que es el código de INK, por lo que el siguiente byte debe ser un código de color, del $00 al $07.

Cuando se pasa la cadena stringNull a la rutina PrintStringNull, lee el primer carácter, $10 (INK), lee el siguiente carácter, $00, y sale.

Lo siguiente que hace el programa es cargar en HL la cadena stringFF y llamar a la rutina PrintStringFF. Esta rutina lee el primer carácter, $10 (INK), y lo pinta, pero como lo anterior también ha sido un INK, lo que espera ahora es un código de color, y lo que le pasamos es $10 (16), un color que no es válido, de ahí el mensaje K Invalid Colour, 40:1.

Vamos a volver a modificar el segundo byte de la cadena stringNull, poniéndolo a $05, y vamos a modificar el segundo byte de la cadena stringFF, que ahora vale $02 y lo ponemos a $00.

Compilamos, cargamos en el emulador y vemos los resultados.

Ensamblador ZX Spectrum, comienza la partida
Ensamblador ZX Spectrum, comienza la partida

Como podemos ver, vuelve a funcionar y pinta la tercera cadena en color negro, $00, por lo que en esta ocasión nos decantamos por la rutina PrintStringFF.

Copiamos el código de la rutina, abrimos el archivo Print.asm, localizamos la rutina PrintString y sustituimos el código de dicha rutina por el código que acabamos de copiar. El aspecto final de la rutina deber ser el siguiente:

; -----------------------------------------------------------------------------
; Pinta cadenas terminadas en $FF.
;
; Entrada:  HL = primera posición de memoria de la cadena
;
; Altera el valor de los registros AF y HL
; -----------------------------------------------------------------------------
PrintString:
ld      a, (hl)         ; Carga en A el carácter a pintar
cp      $ff             ; Comprueba si es $FF
ret     z               ; De ser así sale
rst     $10             ; Pinta el carácter
inc     hl              ; Apunta HL al siguiente carácter
jr      PrintString     ; Bucle hasta que termine de pintar la cadena

Ahora tenemos que modificar la definición de las cadenas y las llamadas a PrintString.

Seguimos en el archivo Print.asm, localizamos la etiqueta PrintFramey borramos la segunda y la quinta línea.

ld      hl, frameTopGraph       ; Carga en HL la dirección de la parte superior
; ld      b, frameBottomGraph - frameTopGraph ; Carga en B la longitud
call    PrintString             ; Pinta la cadena

ld      hl, frameBottomGraph    ; Carga en HL la dirección de la parte inferior
; ld      b, frameEnd - frameBottomGraph  ; Carga en B la longitud
call    PrintString             ; Pinta la cadena

Localizamos la etiqueta PrintInfoGame y borramos la cuarta línea.

ld      a, $01          ; Carga 1 en A
call    OPENCHAN        ; Activa el canal 1, línea de comando
ld      hl, infoGame    ; Carga la dirección de la cadena de títulos en HL
;ld      b, infoGame_end - infoGame  ; Carga la longitud en B

Abrimos el archivo Var.asm, localizamos la etiqueta infoGame y después de enemigos añadimos $FF. Borramos la etiqueta infoGame_end pues ya no tiene ninguna utilidad.

infoGame:
db  $10, $03, $16, $00, $00
db 'Vidas   Puntos   Nivel  Enemigos', $ff

Localizamos la etiqueta frameTopGraph y añadimos al final de la definición de los bytes $FF. En la siguiente etiqueta, frameBottomGraph, hacemos lo mismo y borramos la etiqueta frameEnd, que ya no tiene ninguna utilidad.

frameTopGraph:
db $16, $00, $00, $10, $01
db $96, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $98, $ff
frameBottomGraph:
db $16, $14, $00
db $9b, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9d, $ff

Compilamos (ahora ya lo volvemos a hacer ejecutando make o make.bat), cargamos en el emulador y comprobamos que todo sigue funcionando. También vemos que el programa ocupa ahora tres bytes menos; con cada línea que hemos quitado en la que cargábamos la longitud de la cadena en B, hemos reducido dos bytes (seis en total), pero al poner $ff al final de las cadenas hemos vuelto a añadir tres bytes.

Inicio y fin de la partida

Vamos a implementar una pantalla de inicio a modo de menú y dos finales, uno para cuando nos matan sin haber conseguido finalizar el juego, y otro para cuando logramos finalizar el juego.

Inicio de la partida

En la pantalla de inicio, vamos a mostrar un texto a modo de presentación, las teclas de control y la selección de los distintos tipos de control.

Abrimos el archivo Var.asm y al inicio del mismo incluimos la definición de la pantalla de inicio.

title:
db  $10, $02, $16, $00, $08, "BATALLA ESPACIAL", $0d, $0d, $0d, $ff

firstScreen:
db  $10, $06, "Las naves alienigenas atacan la", $0d
db  "Tierra, el futuro depende de ti.", $0d, $0d
db  "Destruye todos los enemigos que", $0d
db  "puedas, y protege el planeta.", $0d, $0d, $0d
db  $10, $03, "Z - Izquierda", $16, $0a, $15,"X - Derecha"
db  $16, $0c, $0b, "V - Disparo", $0d, $0d
db  $10, $04, "1 - Teclado       3 - Sinclair 1", $0d, $0d
db  "2 - Kempston      4 - Sinclair 2", $0d, $0d, $0d
db  $10, $05, "Apunta, dispara, esquiva a las", $0d 
db  "naves enemigas, vence y libera", $0d
db  "al planeta de la amenza." 
db  $ff

Lo primero que hacemos es poner la tinta en rojo, $10, $02, luego posicionamos el cursor en la línea 0, columna 8, $16, $00, $08, pintamos el nombre del juego, BATALLA ESPACIAL, y añadimos tres retornos de carro, $0d, $0d, $0d. Seguimos definiendo el resto de líneas hasta que acabamos con el delimitador de cadena, $FF, que es valor que espera la rutina PrintString para saber hasta donde tiene que pintar.

Abrimos ahora el archivo Print.asm y al final del mismo vamos a implementar la rutina que pinta la pantalla de inicio y que, más adelante, guardará la elección de controles que hayamos hecho.

PrintFirstScreen:
call    CLS
ld      hl, title
call    PrintString
ld      hl, firstScreen
call    PrintString

Limpiamos la pantalla, CALL CLS, cargamos en HL la dirección de memoria en la que empieza la definición del título, LD HL, title, lo pintamos, CALL PrintString, cargamos en HL la dirección de memoria en la que empieza la definición de la pantalla, LD HL, firtScreen, y llamamos a la rutina que pinta las cadenas, CALL PrintString.

Dado que más adelante vamos a permitir elegir entre cuatro tipo de controles, lo vamos a ir preparando.

printFirstScreen_op:
ld      a, $f7
in      a, ($fe)
bit     $00, a
jr      nz, printFirstScreen_op
call    FadeScreen

ret

Cargamos en A la semifila 1-5, LD A, $F7, leemos el teclado, IN A, ($FE), comprobamos si se ha pulsado el uno (Teclado), BIT $00, A, y de no ser así sigue en bucle hasta que se pulse el uno, JR NZ, printFirstScreen_op. Realizamos el efecto de fundido de la pantalla, CALL FadeScreen, y salimos, RET.

El aspecto final de la rutina es el siguiente:

; -----------------------------------------------------------------------------
; Pantalla de presentación y selección de controles.
;
; Altera el valor de los registros AF y HL.
; -----------------------------------------------------------------------------
PrintFirstScreen:
call    CLS                 ; Limpia la pantalla
ld      hl, title           ; Carga en HL la definición del título
call    PrintString         ; Pinta el título
ld      hl, firstScreen     ; Carga en HL la definición de la pantalla
call    PrintString         ; Pinta la pantalla

printFirstScreen_op:
ld      a, $f7              ; Carga en A la semifila 1-5
in      a, ($fe)            ; Lee el teclado
bit     $00, a              ; Comprueba si se ha pulsado el 1
jr      nz, printFirstScreen_op  ; Si no se ha pulsado, sigue hasta que se pulse
call    FadeScreen          ; Fundido de pantalla

ret

Es hora de comprobar si lo que acabamos de implementar funciona. Abrimos el archivo Main.asm, localizamos la etiqueta Main y dentro de la misma la llamada a pintar el marco, CALL PrintFirstScreen. Justo por encima de esta llamada vamos a incluir la llamada a la rutina que pinta la pantalla de inicio y que más adelante servirá para seleccionar el tipo de controles.

call    PrintFirstScreen

Compilamos, cargamos en el emulador y vemos los resultados.

Ensamblador ZX Spectrum, comienza la partida
Ensamblador ZX Spectrum, comienza la partida

Como podemos comprobar, ahora sale la pantalla de inicio, y no salimos de ella hasta que pulsamos el uno. Todavía quedan cosas por hacer, pero de momento lo dejamos así y seguimos con el fin de partida.

Fin de la partida

El fin de partida se puede dar de dos modos distintos: se nos acaban las cinco vidas de las que vamos a disponer y perdemos, o superamos el nivel treinta y ganamos.

En base a lo expuesto en el párrafo anterior, vamos a definir dos pantallas de fin distintas. Volvemos al archivo Var.asm y tras la definición de firstScreen, añadimos las definición de las dos pantallas de fin.

gameOverScreen:
db  $10, $06, "Has perdido todas tus naves, no", $0d
db  "has podido salvar la Tierra.", $0d, $0d
db  "El planeta ha sido invadido por", $0d
db  "los aliengenas.", $0d, $0d
db  "Puedes volver a intentarlo, de", $0d
db  "ti depende salvar la Tierra.", $ff

winScreen:
db  $10, $06, "Enhorabuena, has destruido a los"
db  "alienigenas, salvaste la Tierra.", $0d, $0d
db  "Los habitantes del planeta te", $0d
db  "estaran eternamente agradecidos.", $ff

pressEnter:
db  $10, $04, $16, $10, $03, "Pulsa Enter para continuar", $ff

Igual que hicimos con la pantalla de inicio, vamos a implementar las rutinas que impriman las pantallas de fin, y que esperen la pulsación de la tecla Enter para continuar. Vamos al archivo Print.asm, y nos situamos al final del mismo.

PrintEndScreen:
push    af
call    FadeScreen
ld      hl, title
call    PrintString
pop     af
or      a
jr      nz, printEndScreen_Win

Preservamos el valor de A, PUSH AF, hacemos el fundido de pantalla, CALL FadeScreen, apuntamos HL al inicio de la cadena del título, LD HL, title, y la pintamos, CALL PrintString. Recuperamos el valor de AF, POP AF, evaluamos si A vale cero, OR A, y saltamos si no es así, JR NZ, printEndScreen_Win.

printEndScreen_GameOver:
ld      hl, gameOverScreen
call    PrintString
jr      printEndScreen_WaitKey

Si el valor de A es cero, apuntamos HL al inicio de la definición de la pantalla de fin de partida si hemos perdido, LD HL, gameOverScreen, la pintamos, CALL PrintString, y saltamos para esperar la pulsación de la tecla Enter, JR printEndScreen_WaitKey.

printEndScreen_Win:
ld      hl, winScreen
call    PrintString

Si el valor de A es distinto de cero, apuntamos HL al inicio de la definición de la pantalla de fin de partida si hemos ganado, LD HL, winScreen, y la pintamos, CALL PrintString.

Preparamos el resto para esperar a que el jugador presione la tecla Enter.

printEndScreen_WaitKey:
ld      hl, pressEnter
call    PrintString
call    PrintInfoGame
call    PrintInfoValue

Apuntamos HL al inicio de la cadena que pide que se pulse la tecla Enter, LD HL, pressEnter, la pintamos, CALL PrintString, pintamos los títulos de la información de la partida, CALL PrintInfoGame, y pintamos la información de la partida para mostrar al jugador el nivel al que ha llegado y los puntos que ha obtenido, CALL PrintInfoValue.

printEndScreen_WaitKeyLoop:
ld      a, $bf       
in      a, ($fe)
rra
jr      c, printEndScreen_WaitKeyLoop
call    FadeScreen

ret

Cargamos en A la semifila Enter-H, LD A, $BF, leemos el teclado, IN A, ($FE), rotamos el registro A hacia la derecha, RRA, y seguimos en bucle hasta que el flag de acarreo no esté activo, JR C, printEndScreen_WaitKeyLoop. Una vez que el Enter se ha pulsado, hacemos el fundido de pantalla, CALL FadeScreen, y salimos, RET.

La forma en la que evaluamos si se ha pulsado el uno es la siguiente: cuando leemos del teclado la semifila Enter-H, el bit cero indica si el Enter se ha pulsado o no, a valor uno si no se ha pulsado y a cero si sí se ha pulsado. Al rotar el registro A hacia la derecha, el valor del bit cero se pone en el acarreo, de tal forma que si se activa, es que no se ha pulsado el Enter y si se desactiva, sí se ha pulsado.

El aspecto final de la rutina es el siguiente:

; -----------------------------------------------------------------------------
; Pantalla de fin de partida.
;
; Entrada:  A -> Tipo de fin, 0 = Game Over, !0 = Win.
;
; Altera el valor de los registros AF y HL.
; -----------------------------------------------------------------------------
PrintEndScreen:
push    af                      ; Preserva el valor de AF
call    FadeScreen              ; Fundido de pantalla
ld      hl, title               ; Apunta HL al título
call    PrintString             ; Pinta el título
pop     af                      ; Recupera el valor de AF
or      a                       ; Evalúa si A vale 0
jr      nz, printEndScreen_Win  ; Si no vale 0, salta

printEndScreen_GameOver:
ld      hl, gameOverScreen      ; Apunta HL a la pantalla de Game Over
call    PrintString             ; La pinta
jr      printEndScreen_WaitKey  ; Salta a esperar pulsación de Enter

printEndScreen_Win:
ld      hl, winScreen           ; Apunta HL a la pantalla de Win
call    PrintString             ; La pinta

printEndScreen_WaitKey:
ld      hl, pressEnter          ; Apunta HL a la cadena 'Pulse Enter'
call    PrintString             ; La pinta
call    PrintInfoGame           ; Pinta los títulos de información de la partida
call    PrintInfoValue          ; Pinta los datos de la partida

printEndScreen_WaitKeyLoop:
ld      a, $bf                  ; Carga a semifila Enter-H en A
in      a, ($fe)                ; Lee el teclado
rra                             ; Rota A a la derecha para ver estado del Enter
jr      c, printEndScreen_WaitKeyLoop   ; Si hay acarreo no se ha pulsado, bucle
call    FadeScreen              ; Fundido de pantalla

ret

Ahora tenemos que probar si nuestras pantallas de fin de partida se muestran bien, vamos al archivo Main.asm, localizamos la línea CALL PrintFirstScreen que hemos añadido antes, y justo por encima de ella vamos a añadir las siguientes líneas:

xor     a
call    PrintEndScreen
ld      a, $01
call    PrintEndScreen

Ponemos A a cero, XOR A, pintamos la pantalla de fin de partida, CALL PrintEndScreen, ponemos A a uno, LD A, $01, y pintamos la pantalla de fin de partida.

Compilamos, cargamos en el emulador y vemos el resultado.

Ensamblador ZX Spectrum, comienza la partida
Ensamblador ZX Spectrum, comienza la partida

La primera llamada que hacemos a la rutina que pinta la pantalla de fin, la hacemos con A valiendo cero, de ahí que pinte la pantalla correspondiente a cuando hemos perdido todas nuestras vidas.

Si presionamos la tecla Enter, ponemos A a uno y volvemos a llamar a la rutina, esta vez se pinta la pantalla correspondiente a cuando hemos superado los treinta niveles.

Ensamblador ZX Spectrum, comienza la partida
Ensamblador ZX Spectrum, comienza la partida

Ahora si pulsamos Enter deberíamos ver la pantalla de inicio.

Ahora tenemos que encajar todo esto para que cada cosa esté en su lugar. Lo primero que vamos ha hacer es eliminar las últimas cuatro líneas que hemos utilizado para probar la rutina PrintEndScreen y las vamos a sustituir por las siguientes para inicializar los datos de la partida:

Main_start:
xor     a
ld      hl, enemiesCounter  
ld      (hl), $20
inc     hl
ld      (hl), a ; $1d
inc     hl
ld      (hl), $01 ; $29
inc     hl
ld      (hl), $05
inc     hl
ld      (hl), a
inc     hl
ld      (hl), a

Ponemos A a cero, XOR A. Apuntamos HL al contador de enemigos, LD HL, enemiesCounter, y lo ponemos a veinte en BCD, LD (HL), $20. Apuntamos HL al contador de niveles, INC HL, y lo ponemos a cero, LD (HL), A.

Apuntamos HL al contador de niveles BCD, INC HL, y lo ponemos a cero, LD (HL), A. Apuntamos HL al contador de vidas, INC HL, y lo ponemos a cinco, LD (HL), $05. Apuntamos HL al primer byte del marcador de puntos en BCD, INC HL, y lo ponemos a cero, LD (HL), A.

Apuntamos HL al segundo byte, INC HL, y lo ponemos a cero, LD (HL), A. Por último, llamamos al cambio de nivel para que reinicie los enemigos y cargue el nivel uno.

En las líneas que cargamos el nivel hemos comentado los valores $1D y $29. Más adelante pondremos estos valores para probar el fin del juego superando tan solo el último nivel.

Buscamos las líneas en las que cargamos el vector de interrupciones, desde DI hasta EI, las cortamos y las pegamos justo encima de la etiqueta MainStart.

Localizamos la etiqueta Main_loop y justo al final, encima de JR Main_loop añadimos la comprobación de si seguimos teniendo vidas.

ld      a, (livesCounter)
or      a
jr      z, GameOver

Cargamos en A en número de vidas, LD A, (livesCounter), comprobamos si son cero, OR A, y saltamos si es así, JR Z, GameOver.

Localizamos la etiqueta Main_restart, y justo debajo de ella añadimos la comprobación de si hemos superado el último nivel.

ld      a, (levelCounter)
cp      $1e
jr      z, Win

Cargamos en A el contador de nivel, LD A, (levelCounter), evaluamos si estamos en el último, CP $1E, y saltamos si es así, JR Z, Win.

Localizamos la línea CALL ChangeLevel, que está casi al final de Main_restart, la cortamos y la pegamos debajo de CALL FadeScreen, siendo de esta manera la quinta línea de Main_restart.

Vamos al final de Main_restart, y justo debajo vamos a implementar el fin de partida.

GameOver:
xor     a
call    PrintEndScreen
jp      Main_start

Ponemos A a cero, XOR A, pintamos la pantalla de fin, CALL PrintEndScreen, y volvemos al inicio, JP MainStart.

Win:
ld      a, $01
call    PrintEndScreen
jp      Main_start

Ponemos A a uno, LD A, $01, pintamos la pantalla de fin, CALL PrintEndScreen, y volvemos al inicio, JP MainStart.

Como podemos ver, en esta ocasión hemos usado JP en lugar de JR, debido a que si en Win ponemos JR Main_start nos da un error de salto fuera de rango.

Vamos a realizar un nuevo cambio, en esta ocasión vamos a hacer que al cambiar de nivel la nave se pinte en la posición inicial. Vamos al archivo Game.asm, localizamos la etiqueta changeLevel_end y antes del RET añadimos lo siguiente:

ld      hl, shipPos             ; Apunta HL a la posición de la nave
ld      (hl), SHIP_INI          ; Carga la posición inicial

Apuntamos HL a la posición de la nave, LD HL, shipPos, y cargamos la posición inicial, LD (HL), SHIP_INI. Dado que el Z80 es Little Endian, HL apunta a la coordenada X de la nave y al cargar SHIP_INI en (HL), carga el segundo byte definido en SHIP_INI en la coordenada X de la nave, LD (HL), $11.

Y llegamos a la hora de la verdad, compilamos cargamos en el emulador y si toda va bien, al perder las cinco vidas acaba la partida, Game Over.

Volvemos a Main.asm y las líneas:

ld (hl), a ; $1d
inc hl
ld (hl), a ; $29

Las dejamos como:

ld      (hl), $1d
inc     hl
ld      (hl), $29

Compilamos, cargamos y al iniciar la partida lo hacemos en el nivel treinta. Lo superamos y fin de partida, Win.

Dado que hemos modificado varias cosas en Main.asm, el aspecto que debe tener ahora es el siguiente:

org     $5dad

; -----------------------------------------------------------------------------
; Indicadores
;
; Bit 0 -> se debe mover la nave            0 = No, 1 = Sí
; Bit 1 -> el disparo está activo           0 = No, 1 = Sí
; Bit 2 -> se deben mover los enemigos      0 = No, 1 = Sí
; -----------------------------------------------------------------------------
flags:
db $00

Main:
ld      a, $02
call    OPENCHAN

ld      hl, udgsCommon
ld      (UDG), hl

ld      hl, ATTR_P
ld      (hl), $07
call    CLS

xor     a
out     ($fe), a
ld      a, (BORDCR)
and     $c0
or      $05
ld      (BORDCR), a

di
ld      a, $28
ld      i, a
im      2
ei

Main_start:
xor     a
ld      hl, enemiesCounter  
ld      (hl), $20
inc     hl
ld      (hl), a ; $1d
inc     hl
ld      (hl), a ; $29
inc     hl
ld      (hl), $05
inc     hl
ld      (hl), a
inc     hl
ld      (hl), a

call    ChangeLevel
call    PrintFirstScreen
call    PrintFrame
call    PrintInfoGame
call    PrintShip
call    PrintInfoValue

call    LoadUdgsEnemies
call    PrintEnemies

Main_loop:
call    CheckCtrl
call    MoveFire

push    de
call    CheckCrashFire
pop     de

ld      a, (enemiesCounter)
cp      $00
jr      z, Main_restart

call    MoveShip
call    MoveEnemies
call    CheckCrashShip

ld      a, (livesCounter)
or      a
jr      z, GameOver

jr      Main_loop

Main_restart:
ld      a, (levelCounter)
cp      $1e
jr      z, Win

call    FadeScreen
call    ChangeLevel
call    PrintFrame
call    PrintInfoGame
call    PrintShip
call    PrintInfoValue
jr      Main_loop

GameOver:
xor     a
call    PrintEndScreen
jp      Main_start

Win:
ld      a, $01
call    PrintEndScreen
jp      Main_start

include "Const.asm"
include "Var.asm"
include "Graph.asm"
include "Print.asm"
include "Ctrl.asm"
include "Game.asm"

end     Main

Ensamblador ZX Spectrum, conclusión

Llegados a este punto, ya podemos echar nuestras primeras partidas, aunque todavía nos quedan cosas por hacer.

En el próximo capítulo implementaremos el control con joystick y daremos las posibilidad de obtener vidas extras.

Puedes descargar el código generado desde aquí.

Enlaces de interés

Ensamblador para ZX Spectrum Batalla espacial por Juan Antonio Rubio García.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartirIgual 4.0 Internacional License.

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí las desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información.plugin cookies

ACEPTAR
Aviso de cookies
A %d blogueros les gusta esto: