0x02 Ensamblador ZX Spectrum OXO – marcadores y partida a dos
El siguiente paso de Ensamblador ZX Spectrum OXO es implementar los marcadores y la partida a dos jugadores, teniendo así gran parte del programa desarrollado.
Tabla de contenidos
Información
En primer lugar añadiremos el nombre del programa, jugadores, marcadores y la ficha que mueven.
Agregamos en Var.asm los datos necesarios.
LENNAME: equ $0c ; Longitud nombre jugadores
; Información
Info: db $10, $07, $16, $00, $05, "Tres en raya a "
info_points: db "5 punto"
info_gt1: db "s"
player1_title: db $10, $05, $16, $02, $00
player1_name: defs LENNAME, " "
player1_figure: db $16, $04, $00, $90, $91, $0d, $91, $90
player_tie: db $10, $02, $16, $02, $0d, "Tablas"
player2_title: db $10, $06, $16, $02, $14
player2_name: defs LENNAME, " "
player2_figure: db $16, $04, $1b, $92, $93, $16, $05, $1b
db $94, $95, $ff
; Títulos
TitleTurn: db $16, $15, $05, $13, $01
db "Turno para "
TitleTurn_name: defs LENNAME, " "
db $ff
TitleError: db $10, $02, $16, $15, $09, $12, $01, $13, $01
defm "Casilla ocupada", $ff
TitleEspamatica: defm "Espamatica 2019", $ff
TitleGameOver: db $10, $07, $16, $15, $03, $12, $01, $13, $01
defm "Partida finalizada. Otra?", $ff
TitleLostMovement: db $10, $02, $12, $01, $13, $01, $16, $15, $05
defm "Pierde turno "
TitleLostMov_name: defs LENNAME, " "
db $ff
TitleOptionStart: defm "0. Empezar", $ff
TitleOptionPlayer: defm "1. Jugadores", $ff
TitleOptionPoint: defm "2. Puntos", $ff
TitleOptionTime: defm "3. Tiempo", $ff
TitlePlayerName: defm "Nombre jugador "
TitlePlayerNumber: db " : ", $ff
TitlePointFor: db $10, $07, $16, $15, $05, $12, $01, $13, $01
defm "Punto para "
TitlePointName: defs LENNAME, " "
db $ff
TitleTie: db $10, $07, $16, $15, $0d, $12, $01, $13, $01
defm "Tablas", $ff
; -------------------------------------------------------------------
; Desarrollo de la partida.
; -------------------------------------------------------------------
; Casillas del tablero. Un byte por casilla, del 1 al 9.
; Bit 0, casilla ocupada por jugador 1.
; Bit 4, casilla ocupada por jugador 2.
Grid: db $00, $00, $00, $00, $00, $00, $00, $00, $00
; Posiciones de las fichas en el tablero, 2 bytes por ficha X, Y
GridPos: db POS1_LEFT, POS1_TOP ; 1
db POS2_LEFT, POS1_TOP ; 2
db POS3_LEFT, POS1_TOP ; 3
db POS1_LEFT, POS2_TOP ; 4
db POS2_LEFT, POS2_TOP ; 5
db POS3_LEFT, POS2_TOP ; 6
db POS1_LEFT, POS3_TOP ; 7
db POS2_LEFT, POS3_TOP ; 8
db POS3_LEFT, POS3_TOP ; 9
Points_p1: db $00 ; Puntos jugador 1
Points_p2: db $00 ; Puntos jugador 2
Points_tie: db $00 ; Puntos tablas
PlayerMoves: db $00 ; Jugador que mueve
MaxPlayers: db $01 ; Máximo jugadores
MaxPoints: db $05 ; Máximo puntos
MaxSeconds: db $10 ; Máximo seguntos
Name_p1: db "Amstrad CPC " ; Nombre jugador 1
Name_p2: db "ZX Spectrum " ; Nombre jugador 2
Name_p2Default: db "ZX Spectrum " ; Nombre por defecto
En la etiqueta Info definimos los literales que se van a mostrar en la parte superior de la pantalla. Aunque lo veáis raro, la división que se hace en varias etiquetas se debe a que hay valores que cambian por la selección que hagan los jugadores:
- Puntos necesarios para ganar la partida.
- Si son más de un punto, se pone en plural.
- Nombre de los jugadores.
También se definen las etiquetas que vamos a usar para llevar la cuenta de la puntuación de cada jugador y sus nombres.
Vamos a Screen.asm e implementamos una rutina para pintar la información y otra para pintar la puntuación.
; -------------------------------------------------------------------
; Pinta la información de la partida.
;
; Altera el valor de HL.
; -------------------------------------------------------------------
PrintInfo:
ld hl, Info ; HL = información
jp PrintString ; La pinta
PrintInfo pinta la información a mostrar en la parte superior de la pantalla. No asigna colores de tinta, ni posiciones, ya que todo eso está definido en la etiqueta Info.
Ya lo tenemos todo listo. Vamos a Main.asm y tras la llamada a PrintBoard, añadimos el código que pinta la información y la puntuación.
ld hl, Name_p1
ld de, player1_name
ld bc, LENNAME
ldir
ld hl, Name_p2
ld de, player2_name
ld bc, LENNAME
ldir
call PrintInfo
xor a
ld hl, Points_p1
ld de, Points_p1+$01
ld bc, $03
ld (hl), a
ldir
call PrintPoints
Los nombres de los jugadores están en las etiquetas Name_p1 y Name_p2, por lo que tenemos que pasarlos al lugar correspondiente de la información. Inicializamos la puntuación y la pintamos.
Compilamos, cargamos en el emulador y vemos los resultados.
Partida a dos
Vamos a implementar la partida a dos jugadores: controles, movimientos de ficha, comprobaciones de que sean correctos y de consecución de tres en raya.
En Var.asm incluimos unas serie de constantes con colores de tinta, líneas en las que se producen las tres en raya, y códigos de teclas, que vamos a usar junto a la rutina de la ROM para verificar los controles.
INKWARNING: equ $C2 ; Tinta advertencias
INKPLAYER1: equ $05 ; Tinta jugador 1
INKPLAYER2: equ $06 ; Tinta jugador 2
INKTIE: equ $07 ; Tinta tablas
KEYDEL: equ $0c ; Tecla Delete
KEYENT: equ $0d ; Tecla Enter
KEYSPC: equ $20 ; Tecla Space
KEY0: equ $30 ; Tecla 0
KEY1: equ $31 ; Tecla 1
KEY2: equ $32 ; Tecla 2
KEY3: equ $33 ; Tecla 3
KEY4: equ $34 ; Tecla 4
KEY5: equ $35 ; Tecla 5
KEY6: equ $36 ; Tecla 6
KEY7: equ $37 ; Tecla 7
KEY8: equ $38 ; Tecla 8
KEY9: equ $39 ; Tecla 9
WINNERLINE123: equ $01 ; Linea ganadora 123
WINNERLINE456: equ $02 ; Linea ganadora 456
WINNERLINE789: equ $03 ; Linea ganadora 789
WINNERLINE147: equ $04 ; Linea ganadora 147
WINNERLINE258: equ $05 ; Linea ganadora 258
WINNERLINE369: equ $06 ; Linea ganadora 369
WINNERLINE159: equ $07 ; Linea ganadora 159
WINNERLINE357: equ $08 ; Linea ganadora 357
La lógica la vamos a implementar en el archivo Game.asm. Lo creamos y añadimos el include en Main.asm.
; -------------------------------------------------------------------
; Espera a que se pulse alguna tecla del tablero.
; Durante el juego se activan las interrupciones para realizar la
; cuenta atrás.
; Con las interrupciones activadas no actualiza FLAGS_KEY/LAST_KEY.
;
; Salida: C -> Tecla pulsada.
;
; Altera el valor de los registros AF y BC.
; -------------------------------------------------------------------
WaitKeyBoard:
ld a, $f7 ; A = semifila 1-5
in a, ($fe) ; Lee semifila
cpl ; Invierte bits, teclas pulsadas a 1
and $1F ; A = bits 1 a 4
jr z, waitKey_cont ; Niguna tecla pulsada, salta
; Evalúa la tecla pulsada del 1 al 5
ld c, KEY1 ; C = tecla 1
ld b, $05 ; B = teclas a comprobar
waitKey_1_5:
rra ; ¿Tecla pulsada?
ret c ; Pulsada, salta
inc c ; C = codigo siguiente tecla
djnz waitKey_1_5 ; Bucle para 5 teclas
waitKey_cont:
ld a, $ef ; A = semifila 0-6
in a, ($fe) ; Lee semifila
cpl ; Invierte bits, teclas pulsadas a 1
and $1F ; A = bits 1 a 4
jr z, waitKey_end ; Niguna tecla pulsada, salta
; Evalúa la tecla pulsada 9 al 6
rra ; Se salta la tecla cero
ld c, KEY9 ; C = tecla 9
ld b, $04 ; B = teclas a comprobar
waitKey_9_6:
rra ; ¿Tecla pulsada?
ret c ; Pulsada, salta
dec c ; C = codigo siguiente tecla
djnz waitKey_9_6 ; Bucle para 4 teclas
; No se ha pulsado ninguna tecla
waitKey_end:
ld c, KEY0
ret
Con WaitKeyBoard vamos a comprobar si se ha pulsado alguna de las teclas del tablero, del uno al nueve. En este caso no vamos a usar la rutina de la ROM ya que, más adelante, tendremos las interrupciones en modo dos activas, y no se actualizarán ni FLAGS_KEY ni LAST_KEY, y la ROM no podrá decirnos la última tecla pulsada.
La comprobación de las teclas es muy parecida a la que hicimos en Batalla espacial, con las siguientes variantes:
- CPL: invertimos los bit y evaluamos si se pulsó alguna tecla con AND $1F; saltamos si no se pulsó ninguna.
- Lo que devolvemos, en este caso en C, es el código ASCII de la tecla pulsada; de la tecla cero si no se ha pulsado ninguna.
También se puede ver que antes de comprobar las teclas del 9 al 6 hacemos una rotación para ignorar la tecla 0.
Continuamos ahora con la lógica del movimiento de la ficha.
; -------------------------------------------------------------------
; Comprueba y realiza el movimiento si es correcto.
;
; Entrada: C -> Tecla pulsada
; Salida: Z -> Movimiento correcto
; NZ -> Movimiento incorrecto
;
; Altera el valor de los registros AF y HL.
; -------------------------------------------------------------------
ToMove:
push bc ; Perserva BC
ld hl, Grid ; HL = dirección grid
ld a, c ; A = C (codigo tecla)
sub $30 ; A = valor númerico tecla
dec a ; A-=1, para que no sume uno más
ld b, $00 ; B = 0
ld c, a ; C = A, BC = desplazamiento
add hl, bc ; HL = dirección casilla de tecla
pop bc ; Recupera BC
ld a, (hl) ; A = valor casilla
or a ; ¿Está libre?
ret nz ; Ocupada, sale
ld a, (PlayerMoves) ; A = jugador que mueve
or a ; ¿Jugador 1?
jr nz, toMove_p2 ; Jugador 2, salta
set $00, (hl) ; Activa casilla jugador 1
jr toMove_end ; Salta
toMove_p2:
set $04, (hl) ; Activa casilla jugador 2
toMove_end:
xor a ; Pone flag Z
ret ; Sale
ToMove recibe en C el código de la tecla pulsada. Calculamos el valor restando el código ASCII de cero, restamos uno para no sumar uno de más en el desplazamiento, y apuntamos a la celda correspondiente a la tecla pulsada. Comprobamos si la celda no está ocupada, si no lo está activamos los bits uno o cuatro de la celda según el jugador que mueve. Si la celda está ocupada sale con NZ, si no lo está sale con Z (XOR A).
Necesitamos una rutina que pinte la ficha en el lugar correcto; la implementamos en Screen.asm.
; -------------------------------------------------------------------
; Pinta la ficha.
;
; Entrada: C -> Tecla pulsada.
;
; Altera el valor de los registros AF, BC y HL.
; -------------------------------------------------------------------
PrintOXO:
; Cálculo posición ficha
ld a, c ; A = tecla
sub $30 ; A = valor tecla
dec a ; A-=1, para no sumar de más en desplazamiento
add a, a ; A+=A, desplazamiento, dos bytes por posición
ld b, $00 ; B = 0
ld c, a ; C = A
ld hl, GridPos ; HL = dirección posiciones grid
add hl, bc ; HL+=BC, dirección coord X celda
ld c, (hl) ; C = coord X celda
inc hl ; HL = dirección coord Y
ld b, (hl) ; B = coord Y celda
call AT ; Posiciona cursor
; Cálculo ficha
ld a, (PlayerMoves) ; A = jugador que mueve
or a ; Comprueba jugador
jr nz, printOXO_Y ; No cero, salta
printOXO_X:
ld a, INKPLAYER1 ; A = tinta jugador 1
call INK ; Cambia tinta
ld a, $90 ; A = 1er sprite
rst $10 ; Lo pinta
ld a, $91 ; A = 2º sprite
rst $10 ; Lo pinta
dec b ; B = línea inferior
call AT ; Posiciona cursor
ld a, $91 ; A = 2º sprite
rst $10 ; Lo pinta
ld a, $90 ; A = 1er sprite
rst $10 ; Lo pinta
ret ; Sale
printOXO_Y:
ld a, INKPLAYER2 ; A = tinta jugador 2
call INK ; Cambia tinta
ld a, $92 ; A = 1er sprite
rst $10 ; Lo pinta
ld a, $93 ; A = 2º sprite
rst $10 ; Lo pinta
dec b ; B = línea inferior
call AT ; Posiciona cursor
ld a, $94 ; A = 3er sprite
rst $10 ; Lo pinta
ld a, $95 ; A = 4º sprite
rst $10 ; Lo pinta
ret ; Sale
En la primera parte de PrintOXO se calcula el desplazamiento para obtener las coordenadas donde pintar la ficha, se obtienen y se posiciona el cursor. Tras esto se obtiene que jugador mueve.
La forma de pintar una u otra ficha son casi idénticas: cambia la tinta, pinta la parte de arriba, posiciona el cursor en la línea inferior, y pinta la parte de abajo antes de salir.
Ya podemos empezar a probar como se ve en el programa todo lo implementado. Vamos a Main.asm y debajo de la etiqueta Loop, antes de JR Loop, añadimos las llamadas a las rutinas implementadas.
call WaitKeyBoard
ld a, c
cp KEY0
jr z, loop_cont
call ToMove
jr nz, loop_cont
call PrintOXO
ld a, (PlayerMoves)
xor $01
ld (PlayerMoves), a
loop_cont:
Esperamos a que el jugador pulse una tecla, si la ha pulsado se comprueba si el movimiento es correcto, y si es así pintamos la ficha y cambiamos de jugador.
Compilamos, cargamos en el emulador y vemos los resultados.
Vemos como las fichas se pintan y una vez que se ocupa todo el tablero sólo podemos reiniciar y volver a cargar, por lo que tenemos que implementar lo siguiente:
- Si el tablero está lleno, reiniciarlo.
- Mostrar mensajes de información.
- Comprobar si hay tres en raya o tablas.
- Actualizar el marcador.
Reinicio del tablero
En Var.asm, antes de PlayersMoves, añadimos una etiqueta para llevar la cuenta del número de movimientos. El tablero estará lleno en el momento en el que el valor llegue a nueve.
La ponemos antes porque PlayersMoves sólo la vamos a reiniciar cuando si inicia una partida. Si la reiniciamos con cada punto, el jugador uno siempre movería primero.
MoveCounter: db $00 ; Contador de movimientos
También en Var.asm añadimos una constante al inicio, en la que especificamos la longitud de datos a inicializar en cada partida.
LENDATA: equ $04 ; Longitud de los datos a inicializar
Según sea necesario añadir más datos que debamos inicializar, cambiaremos el valor de esta constante.
En Main.asm localizamos la etiqueta Loop y cuatro líneas antes la línea LD BC, $03. Sustituimos está línea por la siguiente:
ld bc, LENDATA
Compilamos, cargamos en el emulador y comprobamos que todo sigue funcionando.
Como hemos comentado, no siempre vamos a inicializar todos los datos, por lo que es buena idea implementar una rutina a la que se le pueda pasar la longitud de los datos a inicializar, y así poder llamarla desde distintos sitios, con distintos valores.
Los valores que vamos a inicializar son:
- Points_p1
- Points_p2
- Points_tie
- MoveCounter
- PlayerMoves (solo al iniciar la partida)
Hay más valores que debemos inicializar, pero están más arriba, por lo que, o los inicializamos en dos partes, o cambiamos la etiqueta de sitio. La etiqueta es Grid (movimientos de cada jugador), y debemos de poner todos los valores a cero al inicio de cada punto. Vamos a cambiar la etiqueta de lugar, justo por encima de Points_p1, quedando así:
; -------------------------------------------------------------------
; Desarrollo de la partida.
; -------------------------------------------------------------------
; Posiciones de las fichas en el tablero, 2 bytes por ficha Y, X
GridPos: db POS1_LEFT, POS1_TOP ; 1
db POS2_LEFT, POS1_TOP ; 2
db POS3_LEFT, POS1_TOP ; 3
db POS1_LEFT, POS2_TOP ; 4
db POS2_LEFT, POS2_TOP ; 5
db POS3_LEFT, POS2_TOP ; 6
db POS1_LEFT, POS3_TOP ; 7
db POS2_LEFT, POS3_TOP ; 8
db POS3_LEFT, POS3_TOP ; 9
; Casillas del tablero. Un byte por casilla, del 1 al 9.
; Bit 0 a 1, casilla ocupada por jugador 1.
; Bit 4 a 1, casilla ocupada por jugador 2.
Grid: db $00, $00, $00, $00, $00, $00, $00, $00, $00
Points_p1: db $00 ; Puntos jugador 1
Points_p2: db $00 ; Puntos jugador 2
Points_tie: db $00 ; Puntos tablas
MoveCounter: db $00 ; Contador de movimientos
PlayerMoves: db $00 ; Jugador que mueve
El valor de LENDATA hay que cambiarlo por $0D.
Implementamos la rutina de inicialización en Game.asm.
; -------------------------------------------------------------------
; Inicializa los valores de la partida/punto.
;
; Entrada: BC -> Longitud de los valores a inicializar.
;
; Altera el valor de los registros BC, DE y HL.
; -------------------------------------------------------------------
ResetValues:
ld hl, Grid ; HL = dirección de Grid
ld de, Grid+$01 ; DE = dirección de Grid+1
ld (hl), $00 ; Pone a cero la primera posición
ldir ; Pone a cero el resto (BC)
ret
ResetValues recibe en BC el número de bytes a limpiar (debe ser uno menos de la longitud total, por eso LENDATA es igual a $0D en lugar de $0E), apunta HL y DE a la primera y segunda posición de Grid, limpia la primera y luego el resto.
Si os fijáis bien, esta rutina os debe de sonar mucho, id y mirad la rutina CLS de Screen.asm. ¡Son casi iguales!
Podríamos implementar una rutina con las líneas LD (HL), $00, LDIR y RET, teniendo en cuenta que antes de llamarla habría que cargar los valores necesarios en HL y BC, y eliminar CLS y ResetValues. Lo dejo a vuestra elección, yo lo voy a dejar como está ahora.
Volvemos a Main.asm, localizamos Loop y unas líneas más arriba XOR A. Desde XOR A a CALL PrintPoint, sólo vamos a dejar las líneas LD BC, LENDATA y CALL PrintPoints, el resto las borramos.
Entre LD BC, LENDATA y CALL PrintPoints, vamos a añadir CALL ResetValues, quedando así:
call PrintInfo
ld bc, LENDATA
call ResetValues
call PrintPoints
Loop:
Tras cargar la longitud de los datos a limpiar en BC, llamamos a la inicialización de los valores y pintamos los puntos.
Compilamos, cargamos en el emulador y vemos que todo sigue funcionando.
Para poder saber si el tablero está lleno, tenemos que actualizar MoveCounter con cada movimiento y comprobar si ha llegado a nueve.
Localizamos loop_cont y añadimos las líneas siguientes justo por encima, después de LD (PlayerMoves), A:
ld hl, MoveCounter
inc (hl)
ld a, $09
cp (hl)
jr nz, loop_cont
ld bc, LENDATA-$01
call ResetValues
call PrintBoard
Incrementamos el contador de movimientos, comprobamos si ha llegado a nueve, en cuyo caso cargamos en BC la longitud de los datos a limpiar menos uno (para no limpiar el jugador que mueve), inicializamos los datos y pintamos el tablero vacío.
Compilamos y cargamos en el emulador. Si vamos pulsando las teclas del uno al nueve, hasta que se llene el tablero, el jugador al que le toca mover no se ha limpiado y mueve al que le tocaba en el siguiente turno.
Mensajes de información
Durante el transcurso de la partida se deben mostrar diversos mensajes a los jugadores: si el movimiento es erróneo, el jugador que mueve, quién consigue el punto, si hay tablas y si la partida a finalizado.
Los mensajes ya los definimos al inicio del capítulo, y en cada uno de ellos la tinta, brillo, parpadeo y localización, por lo que lo único que tendríamos que hacer es llamar a PrintString con la dirección del mensaje en HL.
Hay dos motivos para no poder hacerlo así:
- Los mensajes Turno para, Pierde turno y Punto para no están completos, se completan con el nombre del jugador en posesión del turno.
- Antes de escribir un mensaje, hay que borrar la línea.
Vamos a implementar dos rutinas, una en Game.asm y la otra en Screen.asm; estas rutinas nos harán de puente cuando llamemos a PrintString.
Empezamos por la rutina que implementamos en Screen.asm.
; -------------------------------------------------------------------
; Pinta los mensajes
;
; Entrada: HL -> dirección del mensaje
; -------------------------------------------------------------------
PrintMsg:
ld b, INI_TOP-$15 ; B = coord Y linea a borrar (invertida)
call CLL ; Borra la línea
jp PrintString ; Pinta el mensaje
PrintMsg recibe en HL la dirección del mensaje, borra la línea de mensajes y salta a pintar el mensaje; sale por allí. Borrar la línea antes de pintar un nuevo mensaje es la única utilidad de esta rutina.
Implementamos la rutina de Game.asm, que va a completar los tres mensajes que pintan el nombre del jugador.
; -------------------------------------------------------------------
; Completa los mensajes en los que se muestra el nombre del jugador.
;
; Entrada: HL -> dirección del mensaje
; DE -> dirección donde debe ir el nombre
;
; Altera el valor de los registros AF, BC y DE.
; -------------------------------------------------------------------
DoMsg:
push hl ; Perserva HL
ld hl, player1_name ; HL = nombre jugador 1
ld a, (PlayerMoves) ; A = jugador
or a ; ¿Jugador 1?
ld a, INKPLAYER1 ; A = tinta jugador 1
jr z, doMsg_cont ; Jugador 1, salta
ld hl, player2_name ; HL = nombre jugador 2
ld a, INKPLAYER2 ; A = tinta jugador 2
doMsg_cont:
call INK ; Cambia tinta
ld bc, LENNAME ; BC = longitud nombre
ldir ; Pasa nombre jugador a mensaje
pop hl ; Recupera HL
jp PrintMsg ; Pinta mensaje
DoMsg recibe en HL la dirección de mensaje a pintar, y en DE la dirección dónde se debe poner el nombre del jugador. Comprueba que jugador tiene el turno, pone uno u otro nombre y la tinta del jugador. Pinta el mensaje y sale por allí.
Vamos a Main.asm y pintamos todos los mensajes que podemos pintar, por ahora, empezando por quién tiene el turno.
Localizamos Loop, y justo debajo añadimos las siguientes líneas:
ld b, $19
loop_wait:
halt
djnz loop_wait
ld hl, TitleTurn
ld de, TitleTurn_name
call DoMsg
loop_key:
Cada vez que pasamos por el bucle pintamos el jugador al que le toca mover. Antes realizamos una pausa de como medio segundo para que de tiempo a leer los mensajes.
Ahora localizamos la línea CP KEY0 y, justo debajo, en la línea JR Z, loop_cont, cambiamos loop_cont por loop_key.
Compilamos, cargamos en el emulador y vemos que ya pintamos a que jugador le toca mover.
Vamos a pintar también el mensaje de error de casilla ocupada. Localizamos la línea CALL ToMove, borramos la línea siguiente, JR NZ, loop_cont, y añadimos las siguientes:
jr z, loop_print
ld hl, TitleError
call PrintMsg
jr Loop
loop_print:
Si retorna de la rutina ToMove con el flag Z activo, siginifca que el movimiento es correcto y saltamos a loop_print, etiqueta que hemos añadido antes de CALL PrintOXO. Si no es el caso, el movimiento no es correcto y pintamos el mensaje de error y volvemos al inicio del bucle.
Compilad, cargad en el emulador e intentad mover a una casilla ocupada. Se debe pintar el mensaje de error, aunque se ve durante un corto espacio de tiempo; no os preocupéis, más adelante quitaremos la pausa al inicio de Loop y utilizaremos los efectos de sonido para poder temporizar.
Comprobación de tablas
La comprobación de tablas es sencilla; si se ha llegado a nueve movimientos y no hay tres en raya, hay tablas.
Localizamos la etiqueta loop_cont y cuatro líneas por encima, entre JR NZ, loop_cont y LD BC, LENDATA-$01, añadimos lo siguiente:
ld hl, Points_tie
inc (hl)
ld hl, TitleTie
call PrintMsg
Con estas líneas, si se llega a nueve movimientos sin haber tres en raya, se suma uno al marcador de tablas.
Id a la etiqueta loop_key y encima añadid la siguiente línea:
call PrintPoints
Ahora, en cada iteración del bucle pintamos las puntuaciones. El CALL PrintPoints que hay justo encima de Loop lo podemos quitar.
Compilad, cargad en el emulador y veréis como no todo funciona como debería, no se actualiza el marcador de tablas.
En realidad todo está funcionando como debe, el problema está en que ResetValues está reiniciando las puntuaciones, lo cual es fácil de solucionar, aunque lo haremos más adelante.
También observamos que al repintar el tablero el parpadeo está activo, debido a que el mensaje Punto para lo deja así.
Para solucionarlo podríamos implementar una rutina para quitar el parpadeo, pero optamos por solucionarlo en la definición del tablero. Vamos a Var.asm y justo debajo de la etiqueta Board_1 añadimos la siguiente línea para desactivar el parpadeo y brillo al pintar el tablero.
db $12, $00, $13, $00
Comprobación de tres en raya
La comprobación de tres en raya no tendríamos que realizarla hasta que se hayan realizado al menos tres movimientos. En principio, se deberían dar al menos cinco movimientos antes de poder haber tres en raya, pero dado que vamos a implementar la posibilidad de que el jugador pierda el turno si tarda más de lo establecido en realizarlo, es posible que con tres movimientos haya tres en raya. No obstante, vamos a realizar la operación cada vez que se mueva una ficha, y así siempre tardará lo mismo cada iteración del bucle (más o menos).
En Game.asm añadimos la rutina que comprueba si hay tres en raya.
; -------------------------------------------------------------------
; Comprueba si hay tres en raya.
;
; Retorno: A -> línea de tres en raya
; Z si hay tres en raya, NZ en el caso contrario.
;
; Altera el valor de los registros AF, B e IX.
; -------------------------------------------------------------------
ChekcWinner:
ld ix, Grid-$01 ; IX = dirección grid - 1
ld b, $03 ; B = suma celdas jugador 1
ld a, (PlayerMoves) ; A = jugador
or a ; ¿Jugador 1?
jr z, ChekcWinner_check ; Jugador 1, salta
ld b, $30 ; B = suma celdas jugador 2
ChekcWinner_check:
ld a, (ix+1) ; A = celda 1
add a, (ix+2) ; A+= celda 2
add a, (ix+3) ; A+= celda 3
cp b ; ¿Tres en raya?
ld a, WINNERLINE123 ; A = indicador línea 123
ret z ; Tres en raya, sale
ld a, (ix+4) ; A = celda 4
add a, (ix+5) ; A+= celda 5
add a, (ix+6) ; A+= celda 6
cp b ; ¿Tres en raya?
ld a, WINNERLINE456 ; A = indicador línea 456
ret z ; Tres en raya, sale
ld a, (ix+7) ; A = celda 7
add a, (ix+8) ; A+= celda 8
add a, (ix+9) ; A+= celda 9
cp b ; ¿Tres en raya?
ld a, WINNERLINE789 ; A = indicador línea 789
ret z ; Tres en raya, sale
ld a, (ix+1) ; A = celda 1
add a, (ix+4) ; A+= celda 4
add a, (ix+7) ; A+= celda 7
cp b ; ¿Tres en raya?
ld a, WINNERLINE147 ; A = indicador línea 147
ret z ; Tres en raya, sale
ld a, (ix+2) ; A = celda 2
add a, (ix+5) ; A+= celda 5
add a, (ix+8) ; A+= celda 8
cp b ; ¿Tres en raya?
ld a, WINNERLINE258 ; A = indicador línea 258
ret z ; Tres en raya, sale
ld a, (ix+3) ; A = celda 3
add a, (ix+6) ; A+= celda 6
add a, (ix+9) ; A+= celda 9
cp b ; ¿Tres en raya?
ld a, WINNERLINE369 ; A = indicador línea 369
ret z ; Tres en raya, sale
ld a, (ix+1) ; A = celda 1
add a, (ix+5) ; A+= celda 5
add a, (ix+9) ; A+= celda 9
cp b ; ¿Tres en raya?
ld a, WINNERLINE159 ; A = indicador línea 159
ret z ; Tres en raya, sale
ld a, (ix+3) ; A = celda 3
add a, (ix+5) ; A+= celda 5
add a, (ix+7) ; A+= celda 7
cp b ; ¿Tres en raya?
ld a, WINNERLINE357 ; A = indicador línea 357
ret ; Ultima condición, siempre sale
En CheckWinner apuntamos IX a la dirección anterior a Grid, en B cargamos el valor que deben sumar las celdas en el caso de que gane uno u otro jugador: tres para el jugador uno y treinta para el dos.
Luego, vamos comprobando las posibles combinaciones de tres en raya que hay y salimos si se ha producido alguna, con la combinación ganadora en A y el flag Z activado. Para comprobar si ha habido tres en raya, sumamos los valores de las celdas en A y lo compramos con B.
En la última comprobación, si no se han logrado las tres en raya, se sale con el flag Z desactivado.
En Main.asm vamos a añadir las líneas para la comprobación de tres en raya, y para actuar según se haya conseguido o no. Añadimos las líneas siguientes justo debajo de CALL PrintOXO.
loop_checkWinner:
call ChekcWinner
jr nz, loop_tie
ld hl, TitlePointFor
ld de, TitlePointName
call DoMsg
ld hl, Points_p1
ld a, (PlayerMoves)
or a
jr z, loop_win
inc hl
loop_win:
inc (hl)
jr loop_reset
loop_tie:
Llamamos a la comprobación de tres en raya y saltamos si no las hay. Si hay tres en raya, pintamos el mensaje de Punto para, obtenemos que jugador lo ha conseguido e incrementamos su marcador.
Justo debajo de loop_tie están estas líneas:
ld a, (PlayerMoves)
xor $01
ld (PlayerMoves), a
Las quitamos y las ponemos debajo de la etiqueta loop_cont, de lo contrario el siguiente punto lo empieza el mismo jugador que ganó el anterior.
Por último, antes de LD BC, LENDATA-$01 añadimos la etiqueta loop_reset.
Compilamos, cargamos en el emulador y todo parece funcionar, excepto los marcadores que siguen sin actualizarse.
Vamos a marcar visualmente dónde se han producido las tres en raya, dibujando una diagonal que cruce las tres fichas. Implementamos la rutina en Screen.asm.
; -------------------------------------------------------------------
; Imprime la línea ganadora.
;
; Entrada: A -> Línea ganadora
;
; Altera el valor de los registros AF, BC, DE y HL.
; -------------------------------------------------------------------
PrintWinnerLine:
ld hl, COORDX ; HL = coord X
ld bc, $6c6c ; BC = desplazamiento
ld de, $01ff ; DE = orientación
cp WINNERLINE159 ; ¿Gana línea 159?
jr z, printWinnerLine_159 ; Sí, salta
ld e, $01 ; DE = orientación
cp WINNERLINE357 ; ¿Gana línea 357?
jr z, printWinnerLine_357 ; Sí, salta
ld c, $00 ; Desplazamiento
cp WINNERLINE147 ; ¿Gana línea 147?
jr z, printWinnerLine_147 ; Sí, salta
cp WINNERLINE258 ; ¿Gana línea 258?
jr z, printWinnerLine_258 ; Sí, salta
cp WINNERLINE369 ; ¿Gana línea 369?
jr z, printWinnerLine_369 ; Sí, salta
ld bc, $006c ; Desplazamiento
ld (hl), $48 ; Coord X
inc hl ; Apunta HL a coord Y
cp WINNERLINE123 ; ¿Gana línea 123?
jr z, printWinnerLine_123 ; Sí, salta
cp WINNERLINE456 ; ¿Gana línea 456?
jr z, printWinnerLine_456 ; Sí, salta
cp WINNERLINE789 ; ¿Gana línea 789?
jr z, printWinnerLine_789 ; Si es así, salta a pintarla.
printWinnerLine_159:
ld (hl), $b7 ; Coord X
jr printWinnerLine_Y
printWinnerLine_357:
ld (hl), $48 ; Coord X
jr printWinnerLine_Y
printWinnerLine_147:
ld (hl), $58 ; Coord X
jr printWinnerLine_Y
printWinnerLine_258:
ld (hl), $80 ; Coord X
jr printWinnerLine_Y
printWinnerLine_369:
ld (hl), $a8 ; Coord X
printWinnerLine_Y:
inc hl ; Apunta HL a coord Y
ld (hl), $10 ; Coord Y
jr printWinnerLine_end ; Pinta la línea
printWinnerLine_123:
ld (hl), $70 ; Coord Y
jr printWinnerLine_end ; Pinta la línea
printWinnerLine_456:
ld (hl), $47 ; Coord Y
jr printWinnerLine_end ; Pinta la línea
printWinnerLine_789:
ld (hl), $20 ; Coord Y
printWinnerLine_end:
jp DRAW ; Pinta la línea
PrintWinnerLine recibe en A la línea ganadora. En primer lugar evaluamos cual es esa línea para saltar a una parte u otra de la rutina. Dado que las distintas líneas a pintar comparten datos comunes, se van cambiando solo los datos que difieren y se llama a DRAW para pintar la línea y salir por allí (para más información ver comentarios de DRAW en ROM.asm).
En Main.asm, localizamos la etiqueta loop_checkWinner y debajo de JR NZ, loop_tie añadimos la línea siguiente:
call PrintWinnerLine
Compilamos, cargamos en el emulador y vemos los resultados.
Como podemos comprobar se pinta una línea marcando las tres en raya, aunque es tan rápido que apenas podemos verlo. En el próximo capítulo usaremos el sonido para temporizar.
No obstante, si queréis comprobar que se pinta la línea, podéis poner debajo de CALL PrintWinnerLine, estas dos líneas:
tmp:
jr tmp
No olvidéis quitarlas luego.
Actualización del marcador
Vamos a arreglar el error que arrastramos desde hace tiempo y que provoca que el marcador no se esté actualizando.
En Var.asm localizamos la etiqueta Grid y vemos que por debajo están las etiquetas Points_p1, Points_p2 y Point_tie. Movemos estas tres etiquetas a justo debajo de MoveCounter.
; -------------------------------------------------------------------
; Desarrollo de la partida.
; -------------------------------------------------------------------
; Posiciones de las fichas en el tablero, 2 bytes por ficha Y, X
GridPos: db POS1_LEFT, POS1_TOP ; 1
db POS2_LEFT, POS1_TOP ; 2
db POS3_LEFT, POS1_TOP ; 3
db POS1_LEFT, POS2_TOP ; 4
db POS2_LEFT, POS2_TOP ; 5
db POS3_LEFT, POS2_TOP ; 6
db POS1_LEFT, POS3_TOP ; 7
db POS2_LEFT, POS3_TOP ; 8
db POS3_LEFT, POS3_TOP ; 9
; Casillas del tablero. Un byte por casilla, del 1 al 9.
; Bit 0 a 1, casilla ocupada por jugador 1.
; Bit 4 a 1, casilla ocupada por jugador 2.
Grid: db $00, $00, $00, $00, $00, $00, $00, $00, $00
MoveCounter:db $00 ; Contador de movimientos
Points_p1: db $00 ; Puntos jugador 1
Points_p2: db $00 ; Puntos jugador 2
Points_tie: db $00 ; Puntos tablas
PlayerMoves:db $00 ; Jugador que mueve
En Main.asm, localizamos la etiqueta loop_reset, y en la línea de más abajo, LD BC, LENDATA-$01, sustituimos $01 por $04.
Compilamos, cargamos en el emulador y, ahora sí, se actualizan los marcadores.
Ensamblador ZX Spectrum, conclusión
En este capítulo hemos pintado la información de la partida y empezado la implementación de la partida a dos, controlando cuando hay tablas o tres en raya y sumando los puntos.
En el próximo capítulo de Ensamblador ZX Spectrum OXO implementaremos el sonido.
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.