Espamática
ZX SpectrumEnsamblador Z80Retro

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.

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

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

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

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

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.

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

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.

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

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

ASM
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:

ASM
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í:

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

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í:

ASM
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:

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

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.

ASM
; -------------------------------------------------------------------
; 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:

ASM
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:

ASM
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:

ASM
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:

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

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

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

ASM
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:

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

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:

ASM
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:

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

ASM
; -------------------------------------------------------------------
; 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í.

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