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.
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.