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
- Menú
- Inicio de partida
- Tiempo de turno
- Fin de partida
- Ensamblador ZX Spectrum, conclusión
- Enlaces de interés
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.
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.
Menú
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í:
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.
; -------------------------------------------------------------------
; 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:
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:
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.
; -------------------------------------------------------------------
; 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:
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:
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.
; 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.
GetPlayersName:
ld hl, Name_p1
ld de, Name_p1+$01
ld bc, LENNAME*2-$01
ld (hl), " "
ldir
Limpiamos los nombres de los jugadores.
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.
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.
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.
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.
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.
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:
; -------------------------------------------------------------------
; 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.
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):
; -------------------------------------------------------------------
; 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:
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í:
ld a, (seconds) ; A = segundos
Localizamos menu_TimeDo, borramos LD, (MaxSecond), A y en su lugar añadimos las líneas siguientes:
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:
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:
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.
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.
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.
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:
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.
; ------------------------------------------------------------------------
; 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.
; -------------------------------------------------------------------
; 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.
CountDownISR:
push af
push bc
push de
push hl
push ix ; Preserva registros
Preservamos el valor de los registros.
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.
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.
; 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.
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.
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:
10 CLEAR 24200
20 LOAD ""CODE 24200
30 LOAD ""CODE 32348
40 RANDOMIZE USR 24200
El script para compilar en Windows queda así:
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í:
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.
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:
Start:
Localizamos la etiqueta loop_reset y justo debajo de ella vamos a implementar la verificación de si alguien gana la partida.
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.
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.
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í.
Enlaces de interés
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.
También puedes visitar el resto de tutoriales:
Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.