Espamática
ZX SpectrumEnsamblador Z80Retro

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.

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:

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

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.

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

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

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

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

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

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

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

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

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

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

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

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

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