0x01 Ensamblador ZX Spectrum OXO – tablero de juego
Tres en raya nació de la lectura del libro “Club de programación de juegos de ZX Spectrum”, de Gary Plowman. Al terminar de teclear el listado Basic que viene en el libro, nos propone como ejercicio añadir el modo de un jugador.
Una vez que llegué a la conclusión de que el Basic no es lo mío, decidí desarrollar el juego desde cero en ensamblador, con las opciones siguientes:
- Modo de uno y dos jugadores.
- Posibilidad de especificar los nombres de los jugadores.
- Puntos a conseguir para terminar la partida.
- Tiempo máximo por movimiento.
En Batalla espacial vimos el uso de la rutina de la ROM para el manejo de teclado, pero es en Tres en raya donde se utiliza de manera más intensa.
También vamos a hacer uso de las interrupciones, para controlar el tiempo máximo por movimiento e informar al usuario, con una cuenta atrás, de los segundos que le quedan para mover; perderá el turno si los consume sin haber realizado el movimiento.
Llegados a este punto, tras haber leído los tutoriales de Pong y Batalla espacial, ya se debería tener cierto nivel, por lo que no se explicarán las instrucciones una a una; se explicará lo que hace la rutina en su conjunto, que junto con los comentarios debería ser suficiente para su comprensión.
Tabla de contenidos
Tablero de juego
Lo primero que vamos a hacer es diseñar y pintar el tablero de juego, que se compone de una cuadrícula con nueve celdas.
Creamos la carpeta TresEnRaya, y dentro los archivos:
- Main.asm
- Rom.asm
- Screen.asm
- Sprite.asm
- Var.asm
Esta vez no voy a ir creando carpetas por cada paso, aunque si lo preferís, podéis seguir haciéndolo así.
Sprites
Definimos los gráficos en Sprite.asm: la X del jugador uno, la O del jugador dos, la cruceta del tablero y las líneas vertical y horizontal. Al igual que en Batalla espacial, vamos usar los UDG.
El aspecto de los gráficos en Sprite.asm es el siguiente:
; -------------------------------------------------------------------
; Fichero: Sprite.asm
;
; Definición de gráficos.
; -------------------------------------------------------------------
; Sprite jugador 1
Sprite_P1:
db $c0, $e0, $70, $38, $1c, $0e, $07, $03 ; $90
db $03, $07, $0e, $1c, $38, $70, $e0, $c0 ; $91
; Sprite jugador 2
Sprite_P2:
db $03, $0f, $1c, $30, $60, $60, $c0, $c0 ; $92 Arriba/Izquierda
db $c0, $f0, $38, $0c, $06, $06, $03, $03 ; $93 Arriba/Derecha
db $c0, $c0, $60, $60, $30, $1c, $0f, $03 ; $94 Abajo/Izquierda
db $03, $03, $06, $06, $0c, $38, $f0, $c0 ; $95 Abajo/Derecha
; Sprite cruceta
Sprite_CROSS:
db $18, $18, $18, $ff, $ff, $18, $18, $18 ; $96
; Sprite línea vertical
Sprite_SLASH:
db $18, $18, $18, $18, $18, $18, $18, $18 ; $97
; Sprite línea horizontal
Sprite_MINUS:
db $00, $00, $00, $ff, $ff, $00, $00, $00 ; $98
Con las prácticas que realizasteis en Batalla espacial, deberíais ser capaces de dibujar los sprites en un papel para ver cómo quedan.
El siguiente paso es pintar el tablero. Son necesarias constantes para referenciar la ROM, definir posiciones, datos a pintar y rutinas para pintarlos.
Rutinas y variables de la ROM
Las constantes de la ROM las definimos en ROM.asm.
; -------------------------------------------------------------------
; Fichero: Rom.asm
;
; Rutinas de la ROM y variables de sistema.
; -------------------------------------------------------------------
; Atributos permanentes de la pantalla 2, principal.
ATTR_S: equ $5c8d
; Atributos actuales de la pantalla 2.
ATTR_T: equ $5c8f
; Atributo del borde y pantalla 1. Usada por BEEPER.
BORDCR: equ $5c48
; Dirección de las coordenadas de inicio X e Y (PLOT)
COORDX: equ $5c7d
COORDY: equ $5c7e
;--------------------------------------------------------------------
; Dirección donde están los flags de estado del teclado cuando
; no están activas las interrupciones.
;
; Bit 3 = 1 entrada en modo L, 0 entrada en modo K.
; Bit 5 = 1 se ha pulsado una tecla, 0 no se ha pulsado.
; Bit 6 = 1 carácter numérico, 0 alfanumérico.
;--------------------------------------------------------------------
FLAGS_KEY: equ $5c3b
;--------------------------------------------------------------------
; Dirección dónde está la última tecla pulsada
; cuando no están activas las interruciones.
;--------------------------------------------------------------------
LAST_KEY: equ $5c08
; Dirección de los gráficos definidos por el usuario.
UDG: equ $5c7b
; Dirección de inicio del área de gráficos de la VideoRAM.
VIDEORAM: equ $4000
; Longitud del área de gráficos de la VideoRAM.
VIDEORAM_L: equ $1800
; Dirección de inicio del área de atributos de la VideoRAM.
VIDEOATT: equ $5800
; Longitud del área de atributos de la VideoRAM.
VIDEOATT_L: equ $0300
;--------------------------------------------------------------------
; Rutina beeper de la ROM.
;
; Entrada: HL -> Nota.
; DE -> Duracion.
;
; Altera el valor de los registros AF, BC, DE, HL e IX.
;--------------------------------------------------------------------
BEEPER: equ $03b5
;--------------------------------------------------------------------
; Dibuja una línea desde las coordenadas COORDS.
;
; Entrada: B -> Desplazamiento vertical de la línea.
; C -> Desplazamiento horizontal de la línea.
; D -> Orientación vertical de la línea:
; $01 = Arriba, $FF = Abajo.
; E -> Orientación horizontal de la línea:
; $01 = Izquierda, $FF = Derecha.
; Altera el valor de los registros AF, BC y HL.
;--------------------------------------------------------------------
DRAW: equ $24ba
;--------------------------------------------------------------------
; Rutina AT de la ROM. Posiciona el cursor.
;
; Entrada: B -> Coordenada Y.
; C -> Coordenada X.
;
; La esquina superior izquierda de la pantalla es 24, 33.
; (0-21) (0-31)
;--------------------------------------------------------------------
LOCATE: equ $0dd9
;--------------------------------------------------------------------
; Rutina de la ROM que abre el canal de la pantalla.
;
; Entrada: A -> Canal (1 = Pantalla 2, 2 = Pantalla 1).
;--------------------------------------------------------------------
OPENCHAN: equ $1601
Como veréis, hemos añadido variables y rutinas no vistas hasta ahora. No os preocupéis, las iremos viendo según avanzamos.
Variables
En Var.asm añadimos los datos necesarios para pintar el tablero.
; -------------------------------------------------------------------
; Fichero: Var.asm
;
; Declaraciones de variables y constantes.
; -------------------------------------------------------------------
; Tinta del tablero
INKBOARD: equ $04
; Posición Y = 0 para LOCATE.
INI_TOP: equ $18
; Posición X = 0 para LOCATE.
INI_LEFT: equ $21
; Posiciones para pintar los elementos de OXO.
POS1_TOP: equ INI_TOP - $07
POS2_TOP: equ POS1_TOP - $05
POS3_TOP: equ POS2_TOP - $05
POS1_LEFT: equ INI_LEFT - $0a
POS2_LEFT: equ POS1_LEFT - $05
POS3_LEFT: equ POS2_LEFT - $05
; -------------------------------------------------------------------
; Gráficos.
; -------------------------------------------------------------------
; Líneas verticales del tablero.
Board_1:
db $20, $20, $20, $20, $97, $20, $20, $20, $20, $97, $20, $20, $20
db $20, $ff
; Líneas horizontales del tablero.
Board_2:
db $98, $98, $98, $98, $96, $98, $98, $98, $98, $96, $98, $98, $98
db $98, $ff
; -------------------------------------------------------------------
; Partes de la pantalla.
; -------------------------------------------------------------------
; Números de guía para que el jugador sepa que tecla pulsar
; para poner las fichas (O X O)
Board_Helper:
; Tinta magenta
db $10, $03
; AT,Y,X,número
db $16, $08, $0a, "1", $16, $08, $10, "2", $16, $08, $15, "3"
db $16, $0d, $0a, "4", $16, $0d, $10, "5", $16, $0d, $15, "6"
db $16, $12, $0a, "7", $16, $12, $10, "8", $16, $12, $15, "9"
db $ff
Pantalla
En Screen.asm vamos a implementar las rutinas necesarias para pintar los elementos en la pantalla.
; -------------------------------------------------------------------
; Posiciona el cursor. La esquina superior está en 24,33.
;
; Entrada: B -> Y.
; C -> X.
; -------------------------------------------------------------------
AT:
push af
push bc
push de
push hl ; Preserva registros
call LOCATE ; Posiciona cursor.
pop hl
pop de
pop bc
pop af ; Recupera registros.
ret
La rutina AT recibe en los registro B y C las coordenadas Y y X simultáneamente (son coordenadas invertidas). Llama a la rutina de la ROM que posiciona el cursor.
Vamos a administrar los distintos atributos de color de manera diferenciada, con una rutina para cada atributo.
; -------------------------------------------------------------------
; Cambia el color de borde.
;
; Entrada: A -> Color para el borde.
;
; Altera el valor de los registros AF.
; -------------------------------------------------------------------
BORDER:
push bc ; Preserva BC
and $07 ; A = color
out ($fe), a ; Cambia color borde
rlca
rlca
rlca ; Rota tres bits a izquierda para poner
; color en bits de paper/border
ld b, a ; B = A
ld a, (BORDCR) ; A = variable sistema BORDCR.
and $c7 ; Quita los bits de paper/border
or b ; Agrega el color de paper/border
ld (BORDCR), a ; BORDCR = A, para que BEEPER no cambie
pop bc ; Recupera BC
ret
La rutina BORDER recibe en A el color a asignar al borde. Para asegurar que es un color correcto, nos quedamos con los bits 0, 1 y 2. Cambiamos el color de borde y luego ponemos el valor en la variable de sistema BORDCR para que BEEPER no lo cambie.
; -------------------------------------------------------------------
; Asigna el color de la tinta.
;
; Entrada: A -> Color de la tinta.
; FBPPPIII
;
; Altera el valor de los registros AF.
; -------------------------------------------------------------------
INK:
push bc ; Preserva BC
and $07 ; A = INK
ld b, a ; B = A
ld a, (ATTR_T) ; A = atributo actual
and $f8 ; A = FLASH, BRIGHT y PAPER
or b ; Añade INK
ld (ATTR_T), a ; Actualiza atributo actual
ld (ATTR_S), a ; Actualiza atributo permanente
pop bc ; Recupera BC
ret
La rutina INK recibe en A el color de la tinta. Nos aseguramos de quedarnos solo con el color de la tinta, obtenemos los atributos actuales, dejamos el parpadeo, brillo y fondo, y le añadimos la tinta. Actualizamos las variables de sistema, que son consultadas por RST $10 y al cambiar el canal, para aplicar los atributos de color.
; -------------------------------------------------------------------
; Asigna el color de fondo.
;
; Entrada: A -> Color de fondo.
;
; Altera el valor de los registros AF.
; -------------------------------------------------------------------
PAPER:
push bc ; Preserva BC
and $07 ; A = color
rlca
rlca
rlca ; Rota tres bits a izquierda para poner
; color en bits de paper/border
ld b, a ; B = A
ld a, (ATTR_T) ; A = atributo actual
and $c7 ; Quita fondo
or b ; Añade fondo
ld (ATTR_T), a ; Actualiza atributo actual
ld (ATTR_S), a ; Actualiza atributo permanente
pop bc ; Recupera BC
ret
La rutina PAPER recibe el color en A. Nos quedamos con el color, rotamos tres bits a la izquierda para ponerlo en los bits del fondo, y lo guardamos en B. Obtenemos los atributos actuales, quitamos el fondo y le agregamos el que venía a A. Actualizamos las variables de sistema.
; -------------------------------------------------------------------
; Pinta cadenas terminadas en $FF.
;
; Entrada: HL -> Dirección de la cadena.
;
; Altera el valor de los registros AF y HL.
; -------------------------------------------------------------------
PrintString:
ld a, (hl) ; A = carácter a pintar
cp $ff ; ¿Es $FF?
ret z ; Sí, sale
rst $10 ; Pinta carácter
inc hl ; HL = siguiente carácter
jr PrintString ; Bucle hasta fin de cadena
La rutina PrintString ya la vimos en Batalla espacial, por lo que no necesita explicación.
Vamos a implementar rutinas para limpiar la pantalla completa, una línea y los atributos.
; -------------------------------------------------------------------
; Cambia los atributos de la VideoRAM con el atributo especificado
;
; Entrada: A -> Atributo especificado (FBPPPIII)
; Bits 0-2 Color de tinta (0-7).
; Bits 3-5 Color de papel (0-7).
; Bit 6 Brillo (0/1).
; Bit 7 Parpadeo (0/1).
;
; Altera el valor de los registros BC, DE y HL.
; -------------------------------------------------------------------
CLA:
ld hl, VIDEOATT ; HL = inicio atributos
ld (hl), a ; Cambia atributo
ld de, VIDEOATT+1 ; DE = 2ª dirección atributos VideoRAM
ld bc, VIDEOATT_L-1; BC = longitud atributos - 1
ldir ; Cambia los atributos
ld (ATTR_T), a ; Actualiza variable de sistema
ld (ATTR_S), a ; atributo actual, permanente
ld (BORDCR), a ; y borde
ret
La rutina CLA recibe en A los atributos a asignar a la pantalla, en el formato que ya hemos visto anteriormente. Los cambia y actualiza las variables de sistema de atributos actuales, permanentes y borde.
; -------------------------------------------------------------------
; Borra la línea de patalla especificada.
;
; Entrada: B -> Línea de pantalla a borrar.
; Coordenas invertidas.
;
; Altera el valor de los registros AF.
; -------------------------------------------------------------------
CLL:
ld a, (ATTR_T) ; A = atributos actuales
and $3f ; A = PAPER + INK
ld (ATTR_T), a ; Actualiza en memoria
push bc ; Preserva BC
ld c, INI_LEFT ; C = 1ª columna
call AT ; Posiciona cursor.
ld b, $20 ; B = 32 columnas*línea.
CLL_loop:
ld a, $20 ; A = espacio
rst $10 ; Lo imprime
djnz CLL_loop ; Bucle mientras B > 0
pop bc ; Recupera BC
ret
La rutina CLL recibe en A la línea a borrar (con las coordenadas invertidas) y la borra. Antes quita el brillo y el parpadeo de los atributos actuales.
; -------------------------------------------------------------------
; Borra los gráficos de la VideoRAM.
;
; Altera el valor de los registros BC, DE y HL.
; -------------------------------------------------------------------
CLS:
ld hl, VIDEORAM ; HL = dirección VideoRAM
ld de, VIDEORAM+1 ; DE = 2ª dirección VideoRAM
ld bc, VIDEORAM_L-1 ; BC = logitud VideoRAM - 1
ld (hl), $00 ; Limpia 1ª posición
ldir ; Limpia el resto
ret
La rutina CLS borra el área gráfica de la pantalla. No usamos la rutina de la ROM porque tenemos elementos de varios colores, pero son muy estáticos y no cambian, sin embargo la rutina CLS de la ROM si que cambia los atributos, así que implementamos una a medida.
Al igual que hicimos en Batalla espacial, vamos a pintar diversos datos numéricos en formato BDC, pero esta vez, debido a que el rango de los números va estar entre cero y diez, controlamos si la decena es cero para pintar un espacio en su lugar.
; -------------------------------------------------------------------
; Pinta el valor de números BCD.
;
; Sólo pinta números del 0 al 99.
;
; Entrada: HL -> Puntero al número a pintar.
;
; Altera el valor del registro AF.
; -------------------------------------------------------------------
PrintBCD:
ld a, (hl) ; A = número a pintar
and $f0 ; A = decenas
rrca
rrca
rrca
rrca ; Decenas a bits del 0 al 3
or a ; ¿Decena = 0?
jr nz, PrintBCD_ascii ; No, salta
ld a, ' ' ; Decena = 0
jr PrintBCD_continue ; Pinta espacio
PrintBCD_ascii:
add a, '0' ; A = A + código Ascii del 0
PrintBCD_continue:
rst $10 ; Pinta 1er dígito
ld a, (hl) ; A = número a pintar
and $0f ; Se queda con las unidades
add a, '0' ; A = A + código Ascii del 0
rst $10 ; Imprime 2ª dígito
ret
PrintBCD recibe el HL el puntero al número a pintar, lo carga en A, se queda con las decenas y si son cero, pinta un espacio. El resto de la rutina es igual a lo visto en Batalla espacial.
Tenemos casi todo listo para pintar el tablero de juego, sólo nos falta la rutina que lo hace.
; -------------------------------------------------------------------
; Pinta el tablero.
;
; Altera el valor de los registros AF, BC, D y HL.
; -------------------------------------------------------------------
PrintBoard:
ld a, INKBOARD ; A = tinta
call INK ; Cambia tinta
; Coordenadas iniciales del tablero
ld b, INI_TOP - $06 ; B = coord Y
ld c, INI_LEFT - $09 ; C = coord X
ld d, $0e ; D = nº líneas tablero
printBoard_1:
call AT ; Posiciona cursor
ld hl, Board_1 ; HL = línea vertical
call PrintString ; Pinta caracteres línea vertical
dec b ; B-=1, siguiente línea
dec d ; D-=1
jr nz, printBoard_1 ; Bucle mientras D > 0
printBoard_2:
; Coodenadas de la primera línea horizontal
ld b, INI_TOP - $0a ; B = coord Y
ld c, INI_LEFT - $09 ; C = coord X
call AT ; Posiciona cursor
ld hl, Board_2 ; HL = línea horizontal
call PrintString ; Pinta línea horizontal
; Coodenadas de la segunda línea horizontal, la coordenada X no cambia
ld b, INI_TOP - $0f ; B = coord Y
call AT ; Posiciona cursor
ld hl, Board_2
call PrintString ; Pinta línea horizontal
printBoard_3:
; Pinta los números de guía para que el usuario sepa que tecla pulsar
; para poner las fichas (O X O)
Ld hl, Board_Helper ; HL = helper
jp PrintString ; Pinta helper y sale
PrintBoard pinta el tablero de juego: cambia la tinta y prepara todo para pintar la línea vertical, que es pintada en printBoard_1. La línea horizontal la pinta printBoard_2, y la guía para que sepa el jugador que teclas pulsar la pinta printBoard_3.
Al pintar las líneas verticales y las horizontales, donde se cruzan la vertical se borra. Fijaos bien en la definición de Board2 y veréis como no se pinta el mismo carácter en toda ella.
Main
Para ver si todo funciona, implementamos la primera versión del archivo Main.asm.
org $5e88
Main:
ld hl, Sprite_P1
ld (UDG), hl
ld a, $02
call OPENCHAN
xor a
call BORDER
call CLA
call CLS
Init:
call PrintBoard
Loop:
jr Loop
include "Rom.asm"
include "Screen.asm"
include "Sprite.asm"
include "Var.asm"
end Main
Ponemos la dirección de inicio, teniendo en cuenta que haremos un cargador personificado. Indicamos dónde están los UDG, abrimos el canal dos, cambiamos borde, atributos, borramos la pantalla y pintamos el tablero. Nos quedamos en un bucle infinito que será el principal.
Compilamos, cargamos en el emulador y vemos los resultados.
Ensamblador ZX Spectrum, conclusión
En este capítulo hemos diseñado y pintado el tablero sobre el que se desarrollarán las partidas de tres en raya.
En el próximo capítulo de Ensamblador ZX Spectrum OXO implementaremos los marcadores y la partida a dos jugadores.
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.