Espamática
ZX SpectrumEnsamblador Z80Retro

0x04 Ensamblador ZX Spectrum OXO – opciones y fin de partida

En este capítulo de Ensamblador ZX Spectrum OXO vamos a implementar las distintas opciones de la partida y el fin de la misma; según lo dejamos en el capítulo anterior la partida continúa indefinidamente. Las opciones las seleccionaremos a través de un menú de inicio.

Tabla de contenidos

En Var.asm vamos a añadir las siguientes constantes para los colores, y así evitar poner números y que cambiar los colores en todo el juego sea más sencillo.

ASM
INK0:	        equ $00
INK1:	        equ $01
INK2:	        equ $02
INK3:	        equ $03
INK4:	        equ $04
INK5:	        equ $05
INK6:	        equ $06
INK7:	        equ $07

Como ejercicio, revisad las declaraciones de títulos y textos y donde veías $10, cambiad el número que le sigue por la etiqueta INK que le corresponde.

De igual manera, buscad en el resto de archivos las llamadas a INK, y en lugar de cargar en A el número de la tinta, cargad la etiqueta INK correspondiente.

Parte de los textos del menú de inicio ya los tenemos definidos en Var.asm: la etiqueta TitleEspamatica y desde TitleOptionStart hasta TitleOptionTime.

Vamos a borrar todas estas líneas pues la vamos a implementar de nuevo, añadiendo colores y posiciones, quedando así:

ASM
Title3EnRaya:       db $10, INK2, $13, $01, $16, $02, $0a
                    defm "Tres en raya"
TitleOptionStart:   db $10, INK1, $13, $01, $16, $08, $08, "0. "
                    db $10, INK5
                    defm "Empezar"
TitleOptionPlayer:  db $10, INK7, $13, $01, $16, $0a, $08, "1. "
                    db $10, INK6
                    defm "Jugadores"
TitleOptionPoint:   db $10, INK7, $16, $0c, $08, "2. ", $10, INK6
                    defm "Puntos"
TitleOptionTime:    db $10, INK7, $16, $0e, $08, "3. ", $10, INK6
                    defm "Tiempo"
TitleEspamatica:    db $16, $14, $08
                    db $10, INK2, "E", $10, INK6, "s"
                    db $10, INK4, "p", $10, INK5, "a"
                    db $10, INK2, "m", $10, INK6, "a"
                    db $10, INK4, "t", $10, INK5, "i"
                    db $10, INK2, "c", $10, INK6, "a"
                    db $10, INK7, " 2019", $ff

Ya tenemos la definición del menú de inicio casi lista para poder pintarlo, sólo nos falta una rutina para pintar los valores de la opciones. La implementamos en Screen.asm.

ASM
; -------------------------------------------------------------------
; Pinta los valores de las opciones.
;
; Altera el valor de los registros AF, BC y HL.
; -------------------------------------------------------------------
PrintOptions:
ld      a, INK4         ; A = tinta verde
call    INK             ; Cambia tinta
ld      b, INI_TOP-$0a  ; B = coord Y
ld      c, INI_LEFT-$15 ; C = coord X
call    AT              ; Posiciona cursor
ld      hl, MaxPlayers  ; HL = valor jugadores
call    PrintBCD        ; Lo pinta
ld      b, INI_TOP-$0c  ; B = coord Y
call    AT              ; Posiciona cursor
ld      hl, MaxPoints   ; HL = valor puntos
call    PrintBCD        ; Lo pinta
ld      b, INI_TOP-$0e  ; B = coord Y
call    AT              ; Posiciona cursor
ld      hl, MaxSeconds  ; HL = valor tiempo
jp      PrintBCD        ; Lo pinta y sale

PrintOptions pinta los valores de las opciones en verde. Posiciona el cursor y va pintando cada uno de los valores.

Vamos a ver que tal queda el menú de inicio. Localizamos Main en Main.asm, y debajo de CALL OPENCHAN añadimos estas líneas:

ASM
Menu:
di                              ; Desactiva interrupciones
im      $01                     ; Interrupciones = modo 1
ei                              ; Activa interrupciones

Hemos añadido una etiqueta para la parte del menú, desactivado la interrupciones para cambiar al modo uno y volverlas a activar. Luego usaremos las interrupciones en modo dos, y es por eso que estás líneas son necesarias aquí.

Más abajo localizamos CALL CLS y a continuación, añadimos las líneas siguientes:

ASM
ld      hl, Title3EnRaya        ; HL = dirección tres en raya	
call    PrintString             ; Pinta el menú
menu_op:    
call    PrintOptions            ; Pinta las opciones
jr      menu_op                 ; Bucle menú

Pintamos la pantalla de inicio y los valores de las opciones. Nos quedamos en un bucle infinito, ya que ahí vamos a implementar la lógica del menú.

Con la definición del menú y una pocas líneas lo pintamos, pero falta hacerlo funcionar.

Compilamos, cargamos en el emulador y vemos como queda.

Antes de implementar la lógica del menú, en Game.asm vamos a implementar una rutina que utilice la ROM para leer el teclado y que nos devuelva el código ASCII de la última tecla pulsada. Esta rutina la vamos a usar para las opciones del menú y para la solicitud del nombre de los jugadores.

ASM
; -------------------------------------------------------------------
; Espera a que se pulse una tecla y devuelve su código Ascii.
;
; Salida: A -> Código Ascii de la tecla.
;
; Altera el valor de los registros AF y HL.
; -------------------------------------------------------------------
WaitKeyAlpha:
ld      hl, FLAGS_KEY           ; HL = dirección flag teclado
set     $03, (hl)               ; Entrada modo L

; Bucle hasta que se obtenga una tecla.
WaitKeyAlpha_loop:
bit     $05, (hl)               ; ¿Tecla pulsada?
jr      z, WaitKeyAlpha_loop    ; No pulsada, bucle
res     $05, (hl)               ; Bit a 0 para futuras inspecciones

; Obtiene el Ascii de la tecla pulsada
; Ascii válidos 12 ($0C), 13 ($0D) y de 32 ($20) a 127 ($7F)
; Si la tecla pulsada es Space, carga ' ' en A
WaitKeyAlpha_loadKey:
ld      hl, LAST_KEY            ; HL = dirección última tecla pulsada
ld      a, (hl)                 ; A = última tecla pulsada
cp      $80                     ; ¿Ascii > 127?
jr      nc, WaitKeyAlpha        ; Sí, tecla no válida, bucle
cp      KEYDEL                  ; ¿Pulsado Delete?
ret     z                       ; Sí, sale
cp      KEYENT                  ; ¿Pulsado Enter?
ret     z                       ; Sí, sale
cp      KEYSPC                  ; ¿Pulsado Espacio?
jr      c, WaitKeyAlpha         ; Ascii < espacio, inválida, bucle
ret                             ; Sale

WaitKeyAlpha espera hasta que se haya pulsado una tecla que sea válida para nosotros, esto es: delete, enter o un código ASCII entre treinta y dos y ciento veintisiete. Una vez que se ha pulsado una tecla válida, devuelve el código ASCII de la misma en A.

Implementamos en Main.asm la rutina del menú, justo debajo de CALL PrintOptions, siendo el aspecto final este:

ASM
menu_op:
call    PrintOptions        ; Pinta las opciones
call    WaitKeyAlpha        ; Espera pulsación tecla
cp      KEY0                ; ¿Pulsado 0?
jr      z, Init             ; Sí, inicia partida
menu_Players:
cp      KEY1                ; ¿Pulsado 1?
jr      nz, menu_Points     ; No, salta
ld      a, (MaxPlayers)     ; A = jugadores
xor     $03                 ; Alterna entre 1 y 2
ld      (MaxPlayers), a     ; Actualiza en memoria
jr      menu_op             ; Bucle
menu_Points:
cp      KEY2                ; ¿Pulsado 2?
jr      nz, menu_Time       ; No, salta
ld      a, (MaxPoints)      ; A = puntos
inc     a                   ; A += 1
cp      $06                 ; ¿A = 6?
jr      nz, menu_PointsDo   ; No, salta
ld      a, $01              ; A = 1
menu_PointsDo:  
ld      (MaxPoints), a      ; Actualiza en memoria
add     a, '0'              ; A = ascii de valor
ld      (info_points), a    ; Actualiza en memoria
cp      '1'                 ; ¿Puntos 1?
jr      z, menu_Points1     ; Sí, salta
ld      a, 's'              ; Plural
jr      menu_PointsEnd      ; Salta
menu_Points1:
ld      a, ' '              ; Singular
menu_PointsEnd:
ld      (info_gt1), a       ; Actualiza en memoria
jr      menu_op             ; Bucle
menu_Time:
cp      KEY3                ; ¿Pulsado 3?
jr      nz, menu_op         ; No, bucle
ld      a, (MaxSeconds)     ; A = segundos
add     a, $05              ; A += 5
daa                         ; Ajuste decimal
cp      $35                 ; ¿ A = 35 BCD?
jr      nz, menu_TimeDo     ; No, salta
ld      a, $05              ; A = 5
menu_TimeDo:
ld      (MaxSeconds), a     ; Actualiza en memoria
jr      menu_op             ; Bucle

Tras llamar a la rutina que comprueba si se ha pulsado alguna tecla válida, evalúa si es alguna entre la tecla cero y la tres, y según cual sea actúa de una manera u otra. Observad que sólo hace el ajuste decimal al incrementar los segundos; es la única opción cuyo valor pasa de nueve.

Compilamos, cargamos en el emulador y vemos los resultados. Todo parece ir bien hasta que pulsamos el cero, iniciamos la partida y quedan restos del menú.

Para solucionar esto, debajo de la etiqueta Init añadimos la línea siguiente:

ASM
call    CLS                     ; Limpia pantalla

Inicio de partida

Al iniciar la partida, lo primero que vamos a hacer es solicitar los nombres de los jugadores. Antes de nada, añadimos otra constante en ROM.asm; en esta dirección están las coordenadas del cursor.

ASM
; Posición del cursor en pantalla 2. 
; Si se carga en BC -> B = Y, C = X.
CURSOR:     equ $5c88

Implementamos la rutina encargada de solicitar los nombres de los jugadores en Game.asm. Aunque seguimos sin explicar instrucción a instrucción, debido al tamaño de la rutina la vamos a ir implementando por bloques.

ASM
GetPlayersName:
ld      hl, Name_p1      
ld      de, Name_p1+$01  
ld      bc, LENNAME*2-$01
ld      (hl), " "        
ldir

Limpiamos los nombres de los jugadores.

ASM
ld      e, $01
getPlayersName_loop:
ld      a, INK4
call    INK
ld      b, INI_TOP - $0f
ld      c, INI_LEFT - $01
call    CLL
call    AT

ld      hl, TitlePlayerNumber
ld      (hl), "1"
ld      a, $01
cp      e
jr      z, getPlayersName_cont
ld      (hl), "2"
getPlayersName_cont:
ld      hl, TitlePlayerName
call    PrintString

ld      hl, Name_p1
ld      a, $01
cp      e
jr      z, getPlayersName_cont2
ld      hl, Name_p2

Hacemos un bucle de dos iteraciones máximo, una por jugador: cambiamos la tinta, borramos la línea en la que se solicitan los nombres, posicionamos el cursor, preparamos el título dependiendo del jugador y apuntamos HL al nombre del jugador.

ASM
getPlayersName_cont2:
ld      d, $00
ld      a, INK3                   
call    INK                       
call    getPlayersName_getName 

ld      a, (MaxPlayers)           
cp      $02			 
jr      nz, getPlayersName_onlyOne
inc     e                         
cp      e                         
jr      z, getPlayersName_loop    
ret

Usamos D para controlar la longitud del nombre, cambiamos la tinta y solicitamos el nombre del jugador. Obtenemos los jugadores, vemos si son dos, y si no es así ponemos el nombre por defecto al dos. Si son dos jugadores, comprobamos si el nombre introducido es del jugador dos, y si no es así bucle para solicitarlo.

ASM
getPlayersName_onlyOne:
ld      hl, Name_p2Default
ld      de, Name_p2       
ld      bc, LENNAME       
ldir                      
ret

Si es un solo jugador, asigna al segundo el nombre por defecto.

ASM
getPlayersName_getName:
push    hl                       
call    WaitKeyAlpha             
pop     hl                       

cp      KEYDEL                   
jr      z, getPlayersName_delete 
cp      KEYENT                   
jr      z, getPlayersName_enter		
push    de                       
ld      e, a                     
ld      a, LENNAME               
cp      d                        
ld      a, e 
pop     de 
jr      z, getPlayersName_getName
                                 
ld      (hl), a 
inc     hl 
rst     $10
inc     d 
jr      getPlayersName_getName

Esperamos a la pulsación de una tecla válida, comprobamos si es delete y de serlo saltamos a su manejo. Comprobamos si es enter y de serlo saltamos a su manejo.

Si no es delete, ni es enter, vemos si hemos llegado a la longitud máxima, y si no es así añadimos el carácter al nombre y lo pintamos.

ASM
getPlayersName_delete:
ld      a, $00                   
cp      d                        
jr      z, getPlayersName_getName

dec     d                        
dec     hl                       
ld      a, ' '                   
ld      (hl), a                  
ld      bc, (CURSOR)             
inc     c                        
call    AT                       
rst     $10                      
call    AT                       
jr      getPlayersName_getName

Si la tecla pulsada es delete y la longitud del nombre no es cero, borramos el carácter anterior del nombre y de la pantalla.

ASM
getPlayersName_enter:
ld      a, 0                     
cp      d                        
jr      z, getPlayersName_getName
ret

Si la tecla pulsada es enter y la longitud del nombre no es cero, se finaliza la solicitud del nombre.

El aspecto final de la rutina es el siguiente:

ASM
; -------------------------------------------------------------------
; Solicita el nombre de los jugadores.
;
; Altera el valor de los registos AF, BC, DE y HL.
; -------------------------------------------------------------------
GetPlayersName:
ld      hl, Name_p1                 ; HL = dirección nombre jugador 1
ld      de, Name_p1+$01             ; DE = HL+1
ld      bc, LENNAME*2-$01           ; BC = longitud nombres - 1
ld      (hl), " "                   ; Limpia primera posición
ldir                                ; Limpia el resto

ld      e, $01			      ; E = 1
getPlayersName_loop:
ld      a, INK4                     ; A = tinta 4
call    INK                         ; Cambia tinta
ld      b, INI_TOP - $0f            ; B = coord Y
ld      c, INI_LEFT - $01           ; X = coord X
call    CLL                         ; Borra la línea
call    AT                          ; Posiciona cursor

ld      hl, TitlePlayerNumber       ; HL = número de jugador
ld      (hl), "1"                   ; Jugador 1
ld      a, $01                      ; A = 1
cp      e                           ; ¿Jugador 1?
jr      z, getPlayersName_cont      ; Sí, salta
ld      (hl), "2"                   ; Jugador 2    
getPlayersName_cont:
ld      hl, TitlePlayerName         ; HL = titulo nombre jugador
call    PrintString                 ; Lo pinta

ld      hl, Name_p1                 ; HL = dirección nombre jugador 1
ld      a, $01                      ; A = 1
cp      e                           ; ¿Jugador 1?
jr      z, getPlayersName_cont2     ; Sí, salta
ld      hl, Name_p2                 ; HL = dirección nombre jugador 2
getPlayersName_cont2:
ld      d, $00                      ; D = contador longitud nombre
ld      a, INK3                     ; A = tinta 3
call    INK                         ; Cambia tinta
call    getPlayersName_getName      ; Pide nombre jugador

ld      a, (MaxPlayers)             ; A = jugadores
cp      $02                         ; ¿Dos jugadores?
jr      nz, getPlayersName_onlyOne  ; Un jugador, nombre por defecto
inc     e                           ; E+=1
cp      e                           ; Compara con jugadores
jr      z, getPlayersName_loop      ; Iguales, salta
ret

; Un solo jugador
; Copia el nombre por defecto del jugador 2
getPlayersName_onlyOne:
ld      hl, Name_p2Default          ; HL = nombre por defecto jugador 2
ld      de, Name_p2                 ; DE = nombre jugador 2
ld      bc, LENNAME                 ; Longitud nombre
ldir                                ; Copia nombre por defecto
ret                                 ; Sale

; Pide el nombre del jugador
getPlayersName_getName:
push    hl                          ; Preserva HL
call    WaitKeyAlpha                ; Espera tecla válida
pop     hl                          ; Recupera HL

cp      KEYDEL                      ; ¿Delete?
jr      z, getPlayersName_delete    ; Sí, salta
cp      KEYENT                      ; ¿Enter?
jr      z, getPlayersName_enter     ; Sí, salta	
push    de                          ; Preserva DE
ld      e, a                        ; E = código Ascii
ld      a, LENNAME                  ; A = longitud máxima nombre
cp      d                           ; ¿D = longitud máxima?
ld      a, e                        ; A = código Ascii
pop     de                          ; Recupera DE
jr      z, getPlayersName_getName   ; D = longitud máxima
                                    ; otro carácter
                                    ; Enter o Delete

ld      (hl), a                     ; Añade caracter a nombre
inc     hl                          ; HL = siguiente posición
rst     $10                         ; Imprime carácter
inc     d                           ; D+=1
jr      getPlayersName_getName      ; Solicitar otro carácter

getPlayersName_delete:
ld      a, $00                      ; A = 0
cp      d                           ; ¿Longitud 0?
jr      z, getPlayersName_getName   ; Sí, otro carácter

dec     d                           ; D-=1
dec     hl                          ; HL-=1, carácter anterior
ld      a, ' '                      ; A = espacio
ld      (hl), a                     ; Limpia carácter anterior
ld      bc, (CURSOR)                ; BC = posición cursor
inc     c                           ; BC = columna anterior para AT
call    AT                          ; Posiciona cursor
rst     $10                         ; Borra el carácter pantalla	
call    AT                          ; Posiciona cursor
jr      getPlayersName_getName      ; Otro carácter

getPlayersName_enter:
ld      a, 0                        ; A = 0
cp      d                           ; ¿Longitud 0?
jr      z, getPlayersName_getName   ; Sí, otro carácter
ret                                 ; Fin nombre

Vamos a ver si todo funciona. En Main.asm localizamos Init y, debajo de CALL CLS, añadimos la llamada a la solicitud de los nombres y otra llamada a CLS.

ASM
call    GetPlayersName          ; Solicita nombres jugadores
call    CLS                     ; Limpia pantalla

Compilamos, cargamos en el emulador y vemos los resultados. Ya podemos proporcionar los nombres de los jugadores y aparecen en la información de la partida. Si la partida es a un solo jugador, jugamos contra ZX Spectrum.

Tiempo de turno

Unas de las opciones son los segundos de los que dispone cada jugador para realizar el movimiento, si pasados esos segundos no se ha realizado el movimiento, pierde el turno y pasa al otro jugador.

Vamos a hacer uso de la interrupciones para controlar el tiempo. Creamos el archivo Int.asm.

Al inicio de Main.asm, justo debajo de ORG $5E88, añadimos las líneas siguientes (ya lo hicimos en Batalla espacial):

ASM
; -------------------------------------------------------------------
; Indicadores
;       bit 0   ->  Reiniciar cuenta atrás
;       bit 1   ->  Pierde turno
;       bit 2   ->  Pintar cuenta atrás
;       bit 3   ->  Sonido de advertencia, acaba cuenta atrás
; -------------------------------------------------------------------
flags:      db $00
; Valor cuenta atrás
countdown:  db $00
; Segundos por turno
seconds:    db $00

Seconds la vamos a usar para que las interrupciones sepan el número de segundos por turno que han seleccionado los jugadores. Con seconds ya no es necesaria la variable MaxSeconds, la quitamos.

Localizamos Main, y tras la línea CALL OPENCHAN, inicializamos seconds:

ASM
ld      a, $10                  ; A = $10 BCD
ld      (seconds), a            ; Actualiza segundos
ld      (countdown), a          ; Actualiza cuenta atrás

Localizamos menu_Time y tras JR NZ, menu_op modificamos la línea LD A, (MaxSeconds), quedando así:

ASM
ld      a, (seconds)            ; A = segundos

Localizamos menu_TimeDo, borramos LD, (MaxSecond), A y en su lugar añadimos las líneas siguientes:

ASM
ld      (seconds), a            ; Actualiza segundos
ld      (countdown), a          ; Actualiza cuenta atrás

En Screen.asm, al final de la rutina PrintOptions, reemplazamos LD HL, MaxSeconds por:

ASM
ld	hl, seconds	                ; HL = valor tiempo

Por último, en Var.asm, borramos la definición de MaxSeconds.

La rutina de las interrupciones cambiará el valor de flags y el de countdown; debemos tenerlo en cuenta en el bucle principal.

Para que la rutina de interrupciones se ejecute cincuenta veces por segundo (en PAL, sesenta en NTSC) hay que activar el modo dos de las mismas. Localizamos Loop en Mains.asm y encima añadimos:

ASM
di                              ; Desactiva interrupciones
ld      a, $28                  ; A = $28
ld      i, a                    ; I = A (interrupciones en $7e5c)
im      $02                     ; Interrupciones = modo 2
ei                              ; Actualiza interrupciones

Desactivamos las interrupciones, cargamos $28 en el registro de interrupciones, las ponemos en modo dos y las activamos.

Buscamos loop_key, y justo debajo implementamos la lógica del manejo de flags.

ASM
ld      a, (flags)              ; A = Flags
bit     $02, a                  ; ¿Bit 2 activo?
res     $02, a                  ; Desactiva bit 2
jr      z, loop_warning         ; No activo, salta
push    af                      ; Preserva AF
call    PrintCountDown          ; Pinta cuenta atrás
pop     af                      ; Recupera AF

Cargamos flags en A, evaluamos si el bit dos está activo y lo desactivamos. Si no está activo saltamos, si lo está pintamos la cuenta atrás.

ASM
loop_warning:
bit     $03, a                  ; ¿Bit 3 activo?             
res     $03, a                  ; Desactiva bit 3
jr      z, loop_lostMov         ; No activo, salta
ld      bc, SoundCountDown      ; BC = dirección sonido 
call    PlayMusic               ; Emite sonido

Evaluamos si el bit tres está activo y lo desactivamos. Si no está activo saltamos, si lo está emitimos el sonido de advertencia.

ASM
loop_lostMov:
bit     $01, a                  ; ¿Bit 1 activo?
res     $01, a                  ; Desactiva bit 1
ld      (flags), a              ; Actualiza en memoria
halt                            ; Sincroniza con interrupciones
jr      z, loop_keyCont         ; No activo, salta
ld      hl, TitleLostMovement   ; HL = dirección mov perdido
ld      de, TitleLostMov_name   ; DE = dirección nombre
call    DoMsg                   ; Pinta mensaje
ld      bc, SoundLostMovement   ; BC = dirección sonido
call    PlayMusic               ; Emite sonido
jr      loop_cont               ; Salta
loop_keyCont:

Evaluamos si el bit uno está activo, lo desactivamos, actualizamos flags y sincronizamos con las interrupciones. Si no está activo saltamos, si lo está pintamos el mensaje de movimiento perdido y emitimos el sonido. La etiqueta loop_keyCont queda encima de CALL WaitKeyBoard.

Por último, localizamos loop_cont, borramos JR Loop y añadimos en su lugar las líneas siguientes:

ASM
ld      a, $01                  ; A = reiniciar cuenta atrás
ld      (flags), a              ; Actualiza flags
jp      Loop                    ; Bucle principal

Actualizamos flags para que la rutina de interrupciones reincie la cuenta atrás. También hemos sustituido JR por JP debido a las líneas que hemos añadido, hacen que JR esté fuera de rango.

En Screen.asm vamos implementar la rutina que pinta la cuenta atrás.

ASM
; ------------------------------------------------------------------------
; Pinta la cuenta atrás.
;
; Altera el valor de los registros AF, BC y HL.
; ------------------------------------------------------------------------
PrintCountDown:
ld      a, INK3         ; A = tinta 3
call    INK             ; Cambia tinta
ld      b, INI_TOP-$0c  ; B = coord Y
ld      c, INI_LEFT     ; C = coord X
call    AT              ; Posiciona cursor
ld      hl, countdown   ; HL = dirección cuenta atrás
call    PrintBCD        ; Pinta cuenta atrás izquierda
ld      c, INI_LEFT-$1e ; C = coord X
call    AT              ; Posiciona cursor
call    PrintBCD        ; Pinta cuenta atrás derecha
ret

La cuenta atrás la pintamos en dos posiciones, a la izquierda y la derecha del tablero. Ponemos la tinta en magenta, ponemos el cursor a la izquierda, pintamos la cuenta atrás, ponemos el cursor a la derecha y pintamos la cuenta atrás.

La rutina de manejo de las interrupciones la implementamos en Int.asm (no incluyáis este archivo en Main.asm). También vamos a ver esta implementación por bloques.

ASM
; -------------------------------------------------------------------
; Int.asm
;
; Manejo de interrupciones en modo 2
; -------------------------------------------------------------------
org     $7e5c

; -------------------------------------------------------------------
; Indicadores
;       bit 0   ->  Reiniciar cuenta atrás
;       bit 1   ->  Pierde turno
;       bit 2   ->  Pintar cuenta atrás
;       bit 3   ->  Sonido de advertencia, acaba cuenta atrás
; -------------------------------------------------------------------
FLAGS:      equ $5e88

COUNTDOWN:  equ FLAGS+$01       ; Valor cuenta atrás
SECONDS:    equ FLAGS+$02       ; Segundos por turno

La rutina de interrupciones carga en la dirección $7E5C. Después añadimos las constantes con las direcciones de memoria que usaremos  para el intercambio de información entre Main.asm e Int.asm.

ASM
CountDownISR:
push    af                  
push    bc                  
push    de                  
push    hl                  
push    ix                      ; Preserva registros

Preservamos el valor de los registros.

ASM
countDown_flags:
ld      a, (FLAGS)              ; A = flags
and     $01                     ; ¿Reiniciar cuenta atrás?
jr      z, countDown_cont       ; No, salta
ld      a, (SECONDS)            ; A = SECONDS
ld      (COUNTDOWN), a          ; Actualiza en memoria
ld      a, $04                  ; A = pintar cuenta atrás
ld      (FLAGS), a              ; Actualiza en memoria
jr      countDownISR_end        ; Fin

Comprobamos si desde Main.asm se indica que se debe reiniciar la cuenta atrás (cambio de turno) y salta si no es así. Si hay cambio de turno, reinicia la cuenta atrás al valor especificado en el menú, indica en flags que se debe pintar la cuenta atrás y salta para salir de la rutina.

ASM
countDown_cont:
ld      hl, countDownTicks      ; HL = contador ticks
inc     (hl)                    ; Contador ticks+=1
ld      a, $32                  ; A = 50
cp      (hl)                    ; ¿Contador ticks = 50?
jr      nz, countDownISR_end    ; No, salta.
xor     a                       ; A = 0, Z = 1, Carry = 0
ld      (hl), a                 ; Contador ticks = 0

En countDownTicks vamos sumando uno en cada interrupción, y cuando llegamos a cincuenta es señal de que ha pasado un segundo, lo que comprobamos en este bloque. Si no se ha llegado a cincuenta salta para salir de la rutina, si ha llegado ponemos el contador de ticks a cero y seguimos con la rutina.

ASM
; Ha llegado a 50, ha pasado un segundo
ld      a, (COUNTDOWN)          ; A = valor cuenta atrás
dec     a                       ; A-=1
daa                             ; Ajuste decimal
ld      (COUNTDOWN), a          ; Actualiza en memoria
ld      b, $04                  ; B = pintar cuenta atrás / 4 seg	
cp      b                       ; ¿Menos de 4 segundos?
jr      nc, countDownISR_reset  ; A >= 4, salta
set     $03, b                  ; Sonido de advertencia
or      a                       ; ¿A = 0?
jr      nz, countDownISR_reset  ; No, salta
set     $01, b                  ; Pierde turno

En el caso de que haya pasado un segundo, calculamos que tipo de información hay que pasar a Main.asm. Restamos un segundo de la cuenta atrás, vemos si está por debajo de cuatro (también comunica a Main.asm que hay que pintar la cuenta atrás), y saltamos si no lo está. Si lo está, activamos el bit para el sonido de la advertencia, evaluamos si la cuenta atrás ha llegado a cero, y saltamos si no lo ha hecho. Activa el bit de perdida de turno si ha llegado a cero.

ASM
countDownISR_reset:
ld      a, b                    ; A = B
ld      (FLAGS), a              ; Actualiza en memoria

Actualizamos el valor de flags con la información que hay que pasar a Main.asm.

ASM
countDownISR_end:
pop	ix                      
pop	hl                      
pop	de                      
pop	bc                      
pop	af                          ; Recupera registros
	

ei                              ; Reactiva interrupciones
reti                            ; Sale

countDownTicks: db $00          ; Ticks (50*seg)

Recuperamos el valor de los registros, activamos interrupciones y salimos. Por último, declaramos el contador de ticks.

Recordad que es necesario compilar ahora dos .tap por separado y hacer nuestro propio cargador.

El cargador Basic es el siguiente:

VB
10 CLEAR 24200
20 LOAD ""CODE 24200
30 LOAD ""CODE 32348
40 RANDOMIZE USR 24200

El script para compilar en Windows queda así:

ShellScript
echo off
cls
echo Compilando oxo
pasmo --name TresEnRaya --tap Main.asm oxo.tap oxo.log
echo Compilando int
pasmo --name Int --tap Int.asm int.tap int.log
echo Generando Tres en raya
copy /y /b cargador.tap+oxo.tap+int.tap TresEnRaya.tap
echo Proceso finalizado

La versión en Linux queda así:

ShellScript
clear
echo Compilando oxo
pasmo --name TresEnRaya --tap Main.asm oxo.tap oxo.log
echo Compilando int
pasmo --name Int --tap Int.asm int.tap int.log
echo Generando Tres en raya
cat cargador.tap oxo.tap int.tap > TresEnRaya.tap
echo Proceso finalizado

Compilamos, cargamos en el emulador y comprobamos que se pinta la cuenta atrás, suena una alarma al bajar de cuatro segundos y pierde el turno al llegar a cero. También veremos el mensaje de perdida de turno.

Fin de partida

Para finalizar el capítulo vamos a implementar el fin de partida, que se da cuando uno de los jugadores llega a los puntos definidos en el menú, o se llega al máximo tablas.

Al acabar la partida se pregunta si queremos otra; añadimos las teclas de la respuesta como constantes en Var.asm. También añadimos el número máximo de tablas.

ASM
KEYN:           equ $4e ; Tecla N
KEYn:           equ $6e ; Tecla n
KEYS:           equ $53 ; Tecla S
KEYs:           equ $73 ; Tecla s
MAXTIES:        equ $05 ; Numero máximo tablas

Dependiendo de si se opta o no por jugar otra partida, se saltará a un lugar u otro de Main.asm. Localizamos la etiqueta Init y después de CALL GetPlayersName añadimos la siguiente etiqueta:

ASM
Start:

Localizamos la etiqueta loop_reset y justo debajo de ella vamos a implementar la verificación de si alguien gana la partida.

ASM
call    PrintPoints             ; Pinta los puntos
ld      a, (MaxPoints)          ; A = máximo puntos
ld      b, a                    ; B = A
ld      a, (Points_p1)          ; A = puntos jugador 1
cp      b                       ; ¿Jugador 1 gana?
jr      z, EndPlay              ; Sí, salta 
ld      a, (Points_p2)          ; A = puntos jugador 2
cp      b                       ; ¿Jugador 2 gana?
jr      z, EndPlay              ; Sí, salta
ld      b, MAXTIES              ; B = máximo tablas
ld      a, (Points_tie)         ; A = puntos tablas
cp      b                       ; ¿A = B?
jr      z, EndPlay              ; Sí, salta

Pintamos los puntos y comparamos los puntos de los jugadores con el máximo de puntos definidos. Si alguno de los jugadores tiene los puntos necesarios, gana la partida.

Al final de Main.asm, antes de los include, añadimos la rutina del fin partida.

ASM
EndPlay:
di                              ; Desactiva interrupciones
im      $01                     ; Interrupciones en modo 1
ei                              ; Activa interrupciones
ld      hl, TitleGameOver       ; HL = título game over
call    PrintMsg                ; Pinta el mensaje
endPlay_waitKey:
call    WaitKeyAlpha            ; Espera tecla
cp      KEYN                    ; ¿Pulsada N?
jp      z, Menu                 ; Sí, menú
cp      KEYn                    ; ¿Pulsada n?
jp      z, Menu                 ; Sí, menú
cp      KEYS                    ; ¿Pulsada S?
jp      z, Start                ; Sí, inicio
cp      KEYs                    ; ¿Pulsada s?
jp      z, Start                ; Sí, inicio
jr      endPlay_waitKey         ; No pulsadas, bucle

Desactivamos interrupciones, pasamos a modo uno, activamos interrupciones, esperamos a que se responda a la pregunta, y según la respuesta saltamos a un sitio u otro.

  • Menu: volvemos al menú principal. Podemos volver a seleccionar las distintas opciones e introducir nuevos nombres de jugadores.
  • Start: es la etiqueta que hemos añadido. Limpia la pantalla, la pinta, reinicia los datos de la partida y pasa las interrupciones a modo dos.

Si no se ha pulsado ninguna de las teclas esperadas, bucle hasta que se pulse alguna.

Si vais a la etiqueta Menu veréis que las tres primeras líneas son para pasar a modo uno de interrupciones, cosa que también hacemos en EndPlay. Borrad las tres líneas que están debajo de la etiqueta Menu.

Compilamos, cargamos en el emulador y vemos los resultados. Todo parece funcionar correctamente, pero al finalizar la partida la puntuación se muestra parpadeando. Esto lo arrastramos desde el principio, aunque no nos hayamos percatado hasta ahora.

Para cambiar el comportamiento, localizamos PrintPoints dentro de Screen.asm, y justo debajo añadimos las líneas necesarias para desactivar el parpadeo.

ASM
ld      a, (ATTR_T)     ; A = atributos temporales
and     $7f             ; Quita parpadeo 
ld      (ATTR_T), a     ; Actualiza en memoria

Ensamblador ZX Spectrum, conclusión

En este capítulo hemos añadido un menú de inicio con distintas opciones, hemos implementado el inicio de la partida y el fin de la partida, y hemos utilizado las interrupciones para limitar el tiempo que tiene cada jugador para realizar el movimiento. Llegados aquí, podemos jugar las primeras partidas completas a dos jugadores.

¿Queréis jugar contra el Spectrum y ganarle? Lo implementamos en el siguiente capítulo.

Todo el código que hemos generado lo podéis descargar desde aquí.

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



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

Descubre más desde Espamática

Suscríbete ahora para seguir leyendo y obtener acceso al archivo completo.

Seguir leyendo