0x06 Ensamblador ZX Spectrum OXO – ajustes finales
Hemos llegado al capítulo final de Ensamblador para ZX Spectrum OXO. Vamos ha realizar unos ajustes que, si bien no son del todo necesarios, creo que pueden ser de interés.
Añadiremos una opción en el menú para que puedan seleccionar los jugadores el número máximo de tablas que puede haber.
Siguiendo con las tablas, actualmente para finalizar el punto es necesario realizar todos los movimientos posibles (ocupar la totalidad de las celdas), aunque en ocasiones ya sabemos que no es posible ganar y son tablas. Implementaremos la detección de tablas para que detecte si ya no es posible ganar y finalizar el punto.
Propondré unas modificaciones que nos harán ahorrar unos cuantos bytes y ciclos de reloj. Modificaremos los movimientos del Spectrum para que ya no sea tan sencillo ganarle. Por último, vamos a añadir la pantalla de carga.
Tabla de contenidos
- Menú tablas
- Detección de tablas
- Ahorramos bytes y ciclos de reloj
- Movimiento del Spectrum
- Dificultad
- Pantalla de carga
- Ensamblador ZX Spectrum, conclusión
- Enlaces de interés
Menú tablas
Actualmente, el número máximo de tablas lo tenemos declarado en la constante MAXTIES en Var.asm; la borramos.
Localizamos MaxPoints y debajo añadimos la nueva variable para las tablas:
MaxTies: db $05 ; Máximo tablas
Localizamos TitleEspamatica y justo encima añadimos la opción de menú para las tablas:
TitleOptionTies: db $10, INK7, $16, $10, $08, "4. ", $10, INK6
defm "Tablas"
Para que quede más simétrico, localizamos Title3EnRaya y en la parte final de la línea sustituimos $16, $02, $0A por $16, $04, $0A.
En Main.asm vamos a añadir el tratamiento de la nueva opción del menú. Localizamos menu_Time y dos líneas más abajo sutituimos JR NZ, menu_op por:
jr nz, menu_Ties ; No, salta
Localizamos menu_TimeDo y tres líneas más abajo, después de JR menu_op, añadimos el tratamiento de la nueva opción de menú:
menu_Ties:
cp KEY4 ; ¿Pulsado 4?
jr nz, menu_op ; No, bucle
ld a, (MaxTies) ; A = tablas
add a, $02 ; A += 2
cp $0a ; ¿A < 10?
jr c, menu_TiesDo ; Sí, salta
ld a, $03 ; A = 3
menu_TiesDo:
ld (MaxTies), a ; Actualiza tablas
jr menu_op ; Bucle
Si se ha pulsado la tecla cuatro, se añade dos al número máximo de tablas, si es mayor de nueve se pone a tres, y se carga en memoria.
Tanto este rango de valores, como la diferencia entre uno y otro valor lo podéis modificar a vuestro gusto. También lo podéis cambiar a vuestro gusto en puntos y tiempo.
Una vez que tenemos el número máximo de tablas guardado en memoria, hay que usarlo. Localizamos la etiqueta loop_reset, la línea LD B, MAXTIES y la sutituímos por las siguientes:
ld a, (MaxTies) ; A = máximo tablas
ld b, a ; B = A
Ya solo nos queda pintar el valor seleccionado. En Screen.asm, localizamos PrintOptions y, justo por encima de JP PrintBCD, añadimos las líneas siguientes:
call PrintBCD ; Lo pinta
ld b, INI_TOP-$10 ; B = coord Y
call AT ; Posiciona cursor
ld hl, MaxTies ; HL = valor tiempo
Compilamos, cargamos en el emulador y comprobamos que ya podemos definir el número de tablas, y que al alcanzarse se finaliza la partida.
Detección de tablas
Actualmente, para que se detecten tablas tienen que estar todas las celdas ocupadas y que no haya tres en raya. En realidad, es posible saber si el punto terminará en tablas antes de que el tablero este lleno.
Vamos a Game.asm e implementamos la detección de tablas.
Para saber si hay posibilidad de algún movimiento, sólo hay que saber si queda alguna combinación en la que las celdas estén ocupadas por un solo jugador, o por ninguno.
; -------------------------------------------------------------------
; Comprueba si hay tablas.
; Para poder comprobar si hay tablas.
;
; Salida: Z -> No hay tablas.
; NZ -> Hay tablas.
;
; Altera el valor de los registros AF, BC, DE, HL e IX.
; -------------------------------------------------------------------
CheckTies:
ld b, $f0 ; B = máscara celdas un jugador
ld c, $0f ; B = máscara celdas otro jugador
Cargamos en B la máscara para quedarnos con las celdas que ocupa un jugador, y en C la máscara para el jugador dos.
checkTies_check123:
ld hl, Grid ; HL = dirección grid
ld a, (hl) ; A = valor celda 1
inc hl ; HL = dirección celda 2
add a, (hl) ; A+= valor celda 2
inc hl ; HL = dirección celda 3
add a, (hl) ; A+= valor celda 2
ld d, a ; Preserva A en D
and b ; A = celdas ocupadas por un jugador
ret z ; Ninguna, sale
ld a, d ; Recupera A desde D
and c ; A = celdas ocupadas por otro jugador
ret z ; Ninguna, sale
Apuntando HL a Grid, sumamos en A el valor de las tres celdas, incrementanto HL para pasar de una celda a otra. Comprobamos si hay alguna celda ocupada por un jugador, y de no ser así salimos. Si sí las hay, comprobamos si hay celdas ocupadas por el otro jugador, y si no las hay salimos.
Las dos comprobaciones siguientes tiene la misma estructura.
checkTies_check456:
inc hl ; A = dirección celda 4
ld a, (hl) ; A = valor celda 4
inc hl ; HL = dirección celda 5
add a, (hl) ; A+= valor celda 5
inc hl ; HL = dirección celda 6
add a, (hl) ; A+= valor celda 6
ld d, a ; Preserva A en D
and b ; A = celdas ocupadas por un jugador
ret z ; Ninguna, sale
ld a, d ; Recupera A desde D
and c ; A = celdas ocupadas por otro jugador
ret z ; Ninguna, sale
checkTies_check789:
inc hl ; A = dirección celda 7
ld a, (hl) ; A = valor celda 7
inc hl ; HL = dirección celda 8
add a, (hl) ; A+= valor celda 8
inc hl ; HL = dirección celda 9
add a, (hl) ; A+= valor celda 9
ld d, a ; Preserva A en D
and b ; A = celdas ocupadas por un jugador
ret z ; Ninguna, sale
ld a, d ; Recupera A desde D
and c ; A = celdas ocupadas por otro jugador
ret z ; Ninguna, sale
Fijaos bien en como vamos recorriendo las celdas con HL, esto nos va ayuda a ahorrar unos bytes en una implementación que hicimos anteriormente.
Las siguientes comprobaciones no las podemos hacer cambiando de celda con HL, ya que no son numéricamente contiguas; usamos IX.
checkTies_check147:
ld ix, Grid-$01 ; IX = dirección Grid - 1
ld a, (ix+$01) ; A = valor celda 1
add a, (ix+$04) ; A+= valor celda 4
add a, (ix+$07) ; A+= valor celda 7
ld d, a ; Preserva A en D
and b ; A = celdas ocupadas por un jugador
ret z ; Ninguna, sale
ld a, d ; Recupera A desde D
and c ; A = celdas ocupadas por otro jugador
ret z ; Ninguna, sale
El resto de comprobaciones tienen la misma estructura. checkTies_check258:
checkTies_check258:
ld a, (ix+$02) ; A = valor celda 2
add a, (ix+$05) ; A+= valor celda 5
add a, (ix+$08) ; A+= valor celda 8
ld d, a ; Preserva A en D
and b ; A = celdas ocupadas por un jugador
ret z ; Ninguna, sale
ld a, d ; Recupera A desde D
and c ; A = celdas ocupadas por otro jugador
ret z ; Ninguna, sale
checkTies_check369:
ld a, (ix+$03) ; A = valor celda 3
add a, (ix+$06) ; A+= valor celda 6
add a, (ix+$09) ; A+= valor celda 9
ld d, a ; Preserva A en D
and b ; A = celdas ocupadas por un jugador
ret z ; Ninguna, sale
ld a, d ; Recupera A desde D
and c ; A = celdas ocupadas por otro jugador
ret z ; Ninguna, sale
checkTies_check159:
ld a, (ix+$01) ; A = valor celda 1
add a, (ix+$05) ; A+= valor celda 5
add a, (ix+$09) ; A+= valor celda 9
ld d, a ; Preserva A en D
and b ; A = celdas ocupadas por un jugador
ret z ; Ninguna, sale
ld a, d ; Recupera A desde D
and c ; A = celdas ocupadas por otro jugador
ret z ; Ninguna, sale
checkTies_check357:
ld a, (ix+$03) ; A = valor celda 3
add a, (ix+$05) ; A+= valor celda 5
add a, (ix+$07) ; A+= valor celda 7
ld d, a ; Preserva A en D
and b ; A = celdas ocupadas por un jugador
ret z ; Ninguna, sale
ld a, d ; Recupera A desde D
and c ; A = celdas ocupadas por otro jugador
ret ; Sale con Z en estado correcto
Con esto ya tenemos la predicción de si va a haber tablas, sólo queda ir a Main.asm y usarla.
Localizamos loop_tie y tres líneas más abajo sustituimos:
ld a, $09 ; A = 9
cp (hl) ; ¿Contador = 9?
jr nz, loop_cont ; No, salta
Por:
call CheckTies ; ¿Algún movimiento posible?
jr z, loop_cont ; No, salta
Ya no hace falta esperar a que el tablero esté lleno para saber si hay o no tablas. Llamamos a CheckTies y si no hay tablas seguimos con el punto.
Compilamos, cargamos en el emulador y vemos los resultados.
Quizá veáis un comportamiento que os hace pensar que algo no funciona. Veis la línea uno, dos, tres libre, pero sabes que el Spectrum va a ocupar una celda, luego tú otra y por eso debería marcar tablas.
El sistema precide pero no adivinia. Imaginaos que en una fila hay dos celdas libres y le toca mover al jugador que no tiene la celda ocupada. Bueno, va a ocupar una de las celdas así que ya sabemos que son tablas, pero imaginad que el jugador se duerme en los laureles y pierde el turno, el otro jugador ocupa otra celda y solo queda una libre, y sigue sin poderse predecir tablas, si el otro jugador se vuelve a dormir, pierde turno otra vez y el primer jugador consigue tres en raya.
Ahorramos bytes y ciclos de reloj
Vamos a implementar varias modificaciones para ahorrar bytes y ciclos de reloj.
Las primeras de ellas van a ser en Game.asm, sobre el conjunto de rutinas CheckWinner y ZxMove, aplicando la forma en la que hemos implementado CheckTies en las líneas horizontales, las numéricamente contiguas.
Vamos a la etiqueta, ChechWinner_check, localizamos las líneas siguientes:
ld a, (ix+1) ; A = celda 1
add a, (ix+2) ; A+= celda 2
add a, (ix+3) ; A+= celda 3
Y las sustituimos por:
ld hl, Grid ; HL = dirección celda 1
ld a, (hl) ; A = valor celda 1
inc hl ; HL = dirección celda 2
add a, (hl) ; A+= valor celda 2
inc hl ; HL = dirección celda 3
add a, (hl) ; A+= valor celda 3
Localizamos las líneas:
ld a, (ix+4) ; A = celda 4
add a, (ix+5) ; A+= celda 5
add a, (ix+6) ; A+= celda 6
Y las sustituimos por:
inc hl ; HL = dirección celda 4
ld a, (hl) ; A = valor celda 4
inc hl ; HL = dirección celda 5
add a, (hl) ; A+= valor celda 5
inc hl ; HL = dirección celda 6
add a, (hl) ; A+= valor celda 6
Localizamos las líneas:
ld a, (ix+7) ; A = celda 7
add a, (ix+8) ; A+= celda 8
add a, (ix+9) ; A+= celda 9
Y las sustituimos por:
inc hl ; HL = dirección celda 7
ld a, (hl) ; A = valor celda 7
inc hl ; HL = dirección celda 8
add a, (hl) ; A+= valor celda 8
inc hl ; HL = dirección celda 9
add a, (hl) ; A+= valor celda 9
Esta parte está lista, añadid en los comentarios de la rutina a HL como registro afectado.
Esta es la comparativa entre ambas versiones:
Versión | Ciclos | Bytes |
1 | 688 / 641 | 118 |
2 | 638 / 591 | 111 |
Como se observa, estamos ahorrando cincuenta ciclos de reloj y siete bytes. Compilamos, cargamos en el emulador y vemos como todo sigue funcionando.
Procedemos a modificar ZxMove, en concreto dos partes de este conjunto de rutinas. Localizamos zxMoveToWin_123 y sustituimos:
ld a, (ix+$01) ; A = valor celda 1
add a, (ix+$02) ; A+= valor celda 2
add a, (ix+$03) ; A+= valor celda 3
Por:
ld hl, Grid ; HL = dirección celda 1
ld a, (hl) ; A = valor celda 1
inc hl ; HL = dirección celda 2
add a, (hl) ; A+= valor celda 2
inc hl ; HL = dirección celda 3
add a, (hl) ; A+= valor celda 3
Localizamos zxMoveToWin_456 y sustituimos:
ld a, (ix+$04) ; A = valor celda 4
add a, (ix+$05) ; A+= valor celda 5
add a, (ix+$06) ; A+= valor celda 6
Por:
ld hl, Grid+$03 ; HL = dirección celda 4
ld a, (hl) ; A = valor celda 4
inc hl ; HL = dirección celda 5
add a, (hl) ; A+= valor celda 5
inc hl ; HL = dirección celda 6
add a, (hl) ; A+= valor celda 6
Localizamos zxMoveToWin_789 y sustituimos:
ld a, (ix+$07) ; A = valor celda 7
add a, (ix+$08) ; A+= valor celda 8
add a, (ix+$09) ; A+= valor celda 9
Por:
ld hl, Grid+$06 ; HL = dirección celda 7
ld a, (hl) ; A = valor celda 7
inc hl ; HL = dirección celda 8
add a, (hl) ; A+= valor celda 8
inc hl ; HL = dirección celda 9
add a, (hl) ; A+= valor celda 9
Ya hemos terminado con la primera parte. La diferencia principal con CheckTies y CheckWinner es que, aunque son celdas numéricamente contiguas, el paso de la celda tres a la cuatro y de la seis a la siete no es con INC HL, ya que ToMove altera el valor de HL.
Localizamos zxMoveAttack_123 y sustituimos:
ld a, (ix+$01) ; A = valor celda 1
add a, (ix+$02) ; A+= valor celda 2
add a, (ix+$03) ; A+= valor celda 3
Por:
ld hl, Grid ; HL = dirección celda 1
ld a, (hl) ; A = valor celda 1
inc hl ; HL = dirección celda 2
add a, (hl) ; A+= valor celda 2
inc hl ; HL = dirección celda 3
add a, (hl) ; A+= valor celda 3
Localizamos zxMoveAttack_456 y sustituimos:
ld a, (ix+$04) ; A = valor celda 4
add a, (ix+$05) ; A+= valor celda 5
add a, (ix+$06) ; A+= valor celda 6
Por:
ld hl, Grid+$03 ; HL = dirección celda 4
ld a, (hl) ; A = valor celda 4
inc hl ; HL = dirección celda 5
add a, (hl) ; A+= valor celda 5
inc hl ; HL = dirección celda 6
add a, (hl) ; A+= valor celda 6
Localizamos zxMoveAttack_789 y sustituimos:
ld a, (ix+$07) ; A = valor celda 7
add a, (ix+$08) ; A+= valor celda 8
add a, (ix+$09) ; A+= valor celda 9
Por:
ld hl, Grid+$06 ; HL = dirección celda 7
ld a, (hl) ; A = valor celda 7
inc hl ; HL = dirección celda 8
add a, (hl) ; A+= valor celda 8
inc hl ; HL = dirección celda 9
add a, (hl) ; A+= valor celda 9
Hacemos otra modificación, en zxMoveDefence_cornerBlock34 y en zxMoveDefence_cornerBlock67, que nos hace ahorrar ciclos de reloj, pero ningún byte.
Sustituimos en zxMoveDefence_cornerBlock34:
ld a, (ix+$03) ; A = valor celda 3
add a, (ix+$04) ; A+= valor celda 4
Por:
ld hl, Grid+$02 ; A = dirección celda 3
ld a, (hl) ; A = valor celda 3
inc hl ; HL = dirección celda 4
add a, (hl) ; A+= valor celda 4
En zxMoveDefence_cornerBlock67 sustituimos:
ld a, (ix+$06) ; A = valor celda 6
add a, (ix+$07) ; A+= valor celda 7
Por:
ld hl, Grid+$05 ; A = dirección celda 6
ld a, (hl) ; A = valor celda 6
inc hl ; HL = dirección celda 7
add a, (hl) ; A+= valor celda 7
Las modificaciones en Game.asm ya están. Como ya comenté, añadid en los comentarios de la rutina a HL como registro afectado. Ya se nos olvidó antes, pues el registro HL ya se veía afectado por la rutina zxMoveGeneric.
Esta es la comparativa entre las dos versiones de ZxMove:
Versión | Ciclos | Bytes |
1 | 4632 / 4079 | 808 |
2 | 4532 / 3979 | 802 |
La versión dos ocupa seis bytes menos y tarda cien ciclos menos que la uno, lo que parece poco si lo comparamos con el ahorro en bytes de CheckWinner, con menos modificaciones. Recordad que allí el paso de la celda tres a la celda cuatro y de la seis a la siete lo hacemos con INC HL y aquí no podemos.
La diferencia de INC HL con LD HL, Grid es de cuatro ciclos y dos bytes, y eso nos limita el ahorro.
Si tuviéramos problemas de capacidad, que no es así, podemos ahorrar otro buen puñado de bytes pintando la misma ficha, con distinto color, para ambos jugadores.
Vamos a Sprite.asm y comentamos la definición Sprite_P2. Por otro lado, las definiciones de Sprite_CROSS, Sprite_SLASH y Sprite_MINUS las ponemos al inicio del archivo, quedando así:
; -------------------------------------------------------------------
; Fichero: Sprite.asm
;
; Definición de los gráficos.
; -------------------------------------------------------------------
; Sprite de la cruceta
Sprite_CROSS:
db $18, $18, $18, $ff, $ff, $18, $18, $18 ; $90
; Sprite de la línea vertical
Sprite_SLASH:
db $18, $18, $18, $18, $18, $18, $18, $18 ; $91
; Sprite de la línea hortizontal
Sprite_MINUS:
db $00, $00, $00, $ff, $ff, $00, $00, $00 ; $92
; Sprite del jugador 1
Sprite_P1:
db $c0, $e0, $70, $38, $1c, $0e, $07, $03 ; $93
db $03, $07, $0e, $1c, $38, $70, $e0, $c0 ; $94
; ; Sprite del jugador 2
; Sprite_P2:
; db $03, $0f, $1c, $30, $60, $60, $c0, $c0 ; $95 Arriba/Izquierda
; db $c0, $f0, $38, $0c, $06, $06, $03, $03 ; $96 Arriba/Derecha
; db $c0, $c0, $60, $60, $30, $1c, $0f, $03 ; $97 Abajo/Izquierda
; db $03, $03, $06, $06, $0c, $38, $f0, $c0 ; $98 Abajo/Derecha
Comentar el sprite del jugador dos no sólo obliga a modificar la rutina que pinta las fichas, también obliga a modificar la rutina que pinta el tablero, ya que cambian los UDG. Hemos subido a la parte de arriba los sprites del tablero para sólo modificar una vez la rutina que lo pinta, y para que si más adelante decidimos volver a pintar las dos fichas, no se vea afectada.
Al comentar Sprite_P2 ahorramos treinta y dos bytes. No parece mucho, pero en ciertas situaciones nos puede salvar.
Vamos a Var.asm y modificamos la definición de Board_1 y _2 y la dejamos así:
; Líneas verticales del tablero.
Board_1:
db $12, $00, $13, $00
db $20, $20, $20, $20, $91, $20, $20, $20, $20, $91, $20, $20, $20
db $20, $ff
; Líneas horizontales del tablero.
Board_2:
db $92, $92, $92, $92, $90, $92, $92, $92, $92, $90, $92, $92, $92
db $92, $ff
Compilamos, cargamos en el emulador y vemos los resultados, concretamente el desastre que hemos organizado.
Que la ficha del jugador dos se pinte mal lo esperábamos, pero ¿qué pasa con el tablero? Hemos cambiado la posición de la definición de los sprites, pero UDG sigue apuntando a Sprite_P1.
Vamos a Main.asm, y debajo de la etiqueta Main sustituimos:
ld hl, Sprite_P1 ; HL = dirección Sprite_P1
Por:
ld hl, Sprite_CROSS ; HL = dirección Sprite_CROSS
Cambiad también el comentario de la línea de abajo.
Compilamos, cargamos en el emulador y vemos que el tablero se pinta bien, pero las fichas no. Tranquilos, era lo esperado.
Vamos a modificar la rutina que pinta las fichas para que lo haga bien, o no, lo podéis dejar así; tres en raya malditas.
Localizamos printOXO_X y las líneas en las que se carga el UDG en A. Le sumamos tres al valor que carga:
ld a, $90 pasa a ld a, $93
ld a, $91 pasa a ld a, $94
Localizamos printOXO_Y y las líneas en las que se carga el UDG en A. Le sumamos tres al valor que carga:
ld a, $92 pasa a ld a, $95
ld a, $93 pasa a ld a, $96
ld a, $94 pasa a ld a, $97
ld a, $95 pasa a ld a, $98
Con esto, si decidimos pintar dos fichas distintas, se pintaría el círculo.
Como ahora estamos en la situación de pintar una sola ficha, comentamos las líneas:
ld a, $95 ; A = 1er sprite
ld a, $98 ; A = 4º sprite
Y debajo de las mismas añadimos la línea:
ld a, $93 ; A = 1er sprite
Comentamos las líneas:
ld a, $96 ; A = 2º sprite
ld a, $97 ; A = 3er sprite
Y debajo de las mismas añadimos la línea:
ld a, $94 ; A = 2º sprite
Un poco de lío, ¿verdad? El aspecto final es este:
printOXO_X:
ld a, INKPLAYER1 ; A = tinta jugador 1
call INK ; Cambia tinta
ld a, $93 ; A = 1er sprite
rst $10 ; Lo pinta
ld a, $94 ; A = 2º sprite
rst $10 ; Lo pinta
dec b ; B = línea inferior
call AT ; Posiciona cursor
ld a, $94 ; A = 2º sprite
rst $10 ; Lo pinta
ld a, $93 ; A = 2º sprite
rst $10 ; Lo pinta
ret ; Sale
printOXO_Y:
ld a, INKPLAYER2 ; A = tinta jugador 2
call INK ; Cambia tinta
;ld a, $95 ; A = 1er sprite
ld a, $93 ; A = 1er sprite
rst $10 ; Lo pinta
;ld a, $96 ; A = 2º sprite
ld a, $94 ; A = 2º sprite
rst $10 ; Lo pinta
dec b ; B = línea inferior
call AT ; Posiciona cursor
;ld a, $97 ; A = 3er sprite
ld a, $94 ; A = 2º sprite
rst $10 ; Lo pinta
;ld a, $98 ; A = 4º sprite
ld a, $93 ; A = 1er sprite
rst $10 ; Lo pinta
ret ; Sale
De esta manera, si queremos volver a dibujar las dos fichas, en Sprite.asm descomentamos Sprite_P2, y en printOXO_Y alternamos los comentarios en las líneas LD A, ….
Con esto ya se pintan bien las fichas en el tablero, pero no en la parte de información de la partida.
Volvemos a Var.asm y modificamos player1_figure que ahora es así:
player1_figure: db $16, $04, $00, $90, $91, $0d, $91, $90
Y la dejamos así:
player1_figure: db $16, $04, $00, $93, $94, $0d, $94, $93
Modificamos player2_figure que ahora es así:
player2_figure: db $16, $04, $1b, $92, $93
db $16, $05, $1b, $94, $95, $ff
Y la dejamos así:
player2_figure: db $16, $04, $1b, $93, $94
db $16, $05, $1b, $94, $93, $ff
Por último, añadimos, comentada, la definición para pintar fichas distintas.
; player2_figure: db $16, $04, $1b, $95, $96
; db $16, $05, $1b, $97, $98, $ff
Compilamos, cargamos en el emulador y vemos los resultados.
Es decisión vuestra pintar la misma ficha en distinto color para los dos jugadores, o pintar distintas para cada jugador. Si utilizáramos una televisión en blanco y negro no habría ninguna duda.
Si mantenéis todas las modificaciones, hemos ahorrado cuarenta y cinco bytes y ciento cincuenta ciclos de reloj. Si decidís pintar distintas fichas para cada jugador, el ahorro de bytes se queda en trece.
Movimiento del Spectrum
Si ya habéis jugado varias partidas contra el Spectrum, habréis averiguado la forma de ganarle, siempre, al iniciar vosotros la partida, y es que hay al menos un movimiento que el Spectrum no sabe defender, no se lo hemos programado.
El movimiento en concreto es ocupar dos celdas de la esquina: la dos y la seis o la seis y la ocho. Si ocupamos la celda dos, el Spectrum ocupa la cinco, movemos a la celda seis y el Spectrum mueve a la siete, movemos a la tres y ya tenemos dos jugadas de tres en raya: celdas uno, dos y tres y celdas tres, seis y nueve.
En Game.asm, localizamos zxMoveAttack_123 y por encima de ella implementamos las líneas que hacen que ya no se pueda ganar al Spectrum realizando ese movimiento.
; -------------------------------------------------------------------
; Movimiento defensivo de esquina.
; -------------------------------------------------------------------
zxMoveDefence_corner24:
ld a, (ix+$02) ; A = valor celda 2
add a, (ix+$04) ; A+= valor celda 4
cp b ; ¿A = B?
jr nz, zxMoveDefence_corner26 ; No, salta
ld c, KEY1 ; C = tecla 1
call ToMove ; Mueve a celda 1
ret z ; Si es correcto, sale
Comprobamos si el jugador uno tiene ocupadas las casillas dos y cuatro, en cuyo caso movemos a la casilla uno si es posible.
El resto de comprobaciones tienen la misma estructura.
zxMoveDefence_corner26:
ld a, (ix+$02) ; A = valor celda 2
add a, (ix+$06) ; A+= valor celda 6
cp b ; ¿A = B?
jr nz, zxMoveDefence_corner84 ; No, salta
ld c, KEY3 ; C = tecla 3
call ToMove ; Mueve a celda 3
ret z ; Si es correcto, sale
zxMoveDefence_corner84:
ld a, (ix+$08) ; A = valor celda 8
add a, (ix+$04) ; A+= valor celda 4
cp b ; ¿A = B?
jr nz, zxMoveDefence_corner86 ; No, salta
ld c, KEY7 ; C = tecla 7
call ToMove ; Mueve a celda 3
ret z ; Si es correcto, sale
zxMoveDefence_corner86:
ld a, (ix+$08) ; A = valor celda 8
add a, (ix+$06) ; A+= valor celda 6
cp b ; ¿A = B?
jr nz, zxMoveAttack_123 ; No, salta
ld c, KEY9 ; C = tecla 9
call ToMove ; Mueve a celda nueve
ret z ; Si es correcto, sale
Aunque la jugada se da en dos esquinas, cubrimos las cuatro.
Justo encima de zxMoveDefence_cornerBlock2938Cont está la línea JR NZ, zxMoveAttack_123, la modificamos y la dejamos así:
jr nz, zxMoveDefence_corner24 ; No, no mov cruz, salta
Si compiláis y jugáis algunas partidas contra el Spectrum veréis que ahora no es posible ganarle con esa jugada, no obstante sigue siendo posible ganarle, sigue habiendo jugadas que no sabe defender.
Dificultad
Ahora es más difícil ganar al Spectrum, debemos esperar a que sea él el que empiece la partida y, dependiendo del movimiento que haga, podremos ganarle.
Vamos a añadir otra opción en el menú para poder seleccionar el nivel de dificultad: uno para que no cubra la jugada en la esquina, y dos para que si lo haga.
En Var.asm, después de MaxTies, añadimos la variable para el nivel de dificultad:
Level: db $02 ; Nivel de dificultad
Al añadir una nueva opción de menú, está última queda pegada a la línea Espamática. Subimos todas las opciones del menú una línea, quedando así:
TitleOptionStart: db $10, INK1, $13, $01, $16, $07, $08, "0. "
db $10, INK5
defm "Empezar"
TitleOptionPlayer: db $10, INK7, $13, $01, $16, $09, $08, "1. "
db $10, INK6
defm "Jugadores"
TitleOptionPoint: db $10, INK7, $16, $0b, $08, "2. ", $10, INK6
defm "Puntos"
TitleOptionTime: db $10, INK7, $16, $0d, $08, "3. ", $10, INK6
defm "Tiempo"
TitleOptionTies: db $10, INK7, $16, $0f, $08, "4. ", $10, INK6
defm "Tablas"
Tras la definición de TitleOptionTies añadimos la de la dificultad:
TitleOptionLevel: db $10, INK7, $16, $11, $08, "5. ", $10, INK6
defm "Dificultad"
En Screen.Asm localizamos PrintOptions y restamos una a todas las asignaciones de la coordenada Y:
LD B, INI_TOP-$0A pasa a LD B,INI_TOP-$09
LD B, INI_TOP-$0C pasa a LD B,INI_TOP-$0B
LD B, INI_TOP-$0E pasa a LD B,INI_TOP-$0D
LD B, INI_TOP-$10 pasa a LD B,INI_TOP-$0F
Por encima de JP PrintBCD añadimos las líneas siguientes:
call PrintBCD ; Lo pinta
ld b, INI_TOP-$11 ; B = coord Y
call AT ; Posiciona cursor
ld hl, Level ; HL = valor dificultad
Siendo el aspecto final de la rutina el siguiente:
; -------------------------------------------------------------------
; Pinta los valores de las opciones.
;
; Altera el valor de los registros AF, BC y HL.
; -------------------------------------------------------------------
PrintOptions:
ld a, INK4 ; A = tinta verde
call INK ; Cambia tinta
ld b, INI_TOP-$09 ; B = coord Y
ld c, INI_LEFT-$15 ; C = coord X
call AT ; Posiciona cursor
ld hl, MaxPlayers ; HL = valor jugadores
call PrintBCD ; Lo pinta
ld b, INI_TOP-$0b ; B = coord Y
call AT ; Posiciona cursor
ld hl, MaxPoints ; HL = valor puntos
call PrintBCD ; Lo pinta
ld b, INI_TOP-$0d ; B = coord Y
call AT ; Posiciona cursor
ld hl, seconds ; HL = valor tiempo
call PrintBCD ; Lo pinta
ld b, INI_TOP-$0f ; B = coord Y
call AT ; Posiciona cursor
ld hl, MaxTies ; HL = valor tiempo
call PrintBCD ; Lo pinta
ld b, INI_TOP-$11 ; B = coord Y
call AT ; Posiciona cursor
ld hl, Level ; HL = valor dificultad
jp PrintBCD ; Lo pinta y sale
En Main.asm, localizamos menu_Ties y debajo la línea CP KEY4. Tras esta línea sustituimos JR NZ, menu_op por:
jr nz, menu_Level ; No, bucle
Después de menu_TiesDo, tras la línea JR menu_op, añadimos las líneas de tratamiento de la nueva opción de menú:
menu_Level:
cp KEY5 ; ¿Pulsado 5?
jr nz, menu_op ; No, bucle
ld a, (Level) ; A = dificultad
xor $03 ; Alterna entre 1 y 2
ld (Level), a ; Actualiza en memoria
jr menu_op ; Bucle
Por último, en Game.asm localizamos zxMoveDefence_corner24, y justo debajo de ella añadimos:
ld a, (Level) ; A = dificultad
cp $01 ; ¿Dificultad = 1?
jr z, zxMoveAttack_123 ; Sí, salta
Con estas líneas, si el nivel de dificultad es uno no se comprueba la jugada de esquina, compilad y probad.
Pantalla de carga
Por último, vamos a añadir la pantalla de carga. ¿Y que aporta si ya hemos visto como se hace en ZX-Pong y Batalla espacial? Pues esta vez lo vamos a hacer distinto, ya que no vamos a cargar la pantalla en la VideoRAM, la cargaremos en otra dirección de memoria y luego haremos que aparezca de golpe.
Esta es la pantalla de carga, que podéis descargar desde aquí.
Por favor, sed benévolos, el arte nunca ha sido lo mío. Veré con buenos ojos que diseñéis vuestra propia pantalla de carga, que seguro que es mejor que ésta; difícil no debe ser.
Para poder cargar la pantalla de carga en un área de memoria y después volcarla de golpe en la VideoRAM, necesitamos implementar la rutina que lo haga en un archivo aparte, y compilarlo por separado.
El proceso de carga hará lo siguiente:
- Carga el cargador.
- Carga la rutina que vuelca la pantalla de carga en la dirección de memoria 24200.
- Carga la pantalla de carga en la dirección 24250.
- Ejecuta la rutina que vuelca la pantalla de carga a la VideoRAM.
- Carga el programa de tres en raya en la dirección 24200.
- Carga la rutina de interrupciones en la dirección 32348.
- Ejecuta el programa de tres en raya.
Lo primero que vamos a ver es el cargador que desarrollamos en Basic para hacer todo lo expuesto arriba.
10 CLEAR 24200
20 INK 0: PAPER 4: BORDER 4: CLS
30 POKE 23610,255: POKE 23739,111
40 LOAD ""CODE 24200
50 LOAD ""CODE 24250
60 RANDOMIZE USR 24200
70 LOAD ""CODE 24200
80 LOAD ""CODE 32348
90 CLS
100 RANDOMIZE USR 24200
No olvidéis guardarlo con SAVE”TresEnRaya” LINE 10.
El siguiente paso es implemetar la rutina que vuelca la pantalla de carga a la VideoRAM. Creamos el archivo LoadScr.asm y agregamos las siguientes líneas:
; -------------------------------------------------------------------
; LoadScr.asm
;
; La pantalla de carga se cargará en $5eba, y esta rutina la pasará
; a la VideoRAM para que aparezca de golpe y luego limpiará la zona
; dónde se había cargado inicialmente.
; La limpieza de ese área de memoria no es necesaria, pero lo hacemos
; por si tenemos que depurar, no encontrar código que en realidad sean
; restos de la pantalla de carga.
; -------------------------------------------------------------------
org $5e88 ; Dirección de carga
ld hl, $5eba ; HL = dirección dónde está la pantalla
ld de, $4000 ; DE = dirección VideoRAM
ld bc, $1b00 ; Longitud de la pantalla
ldir ; Vuelca la pantalla
ld hl, $5eba ; HL = dirección dónde está la pantalla
ld de, $5ebb ; Dirección siguiente
ld bc, $1aff ; Longitud de la pantalla - 1
ld (hl), $00 ; Limpia la primera posición
ldir ; Limpia el resto
ret
La pantalla de carga la cargamos en $5eba. Esta rutina copia a la VideoRAM desde esa dirección $1B00 posiciones (6912 bytes), todo el área de píxeles y atributos.
Una vez copiado, limpia el área de memoria en la que se cargó la pantalla; no es necesario, pero si tenemos que depurar evitamos que quede código residual que nos pueda confundir.
Ya solo nos queda modificar el script que tenemos para compilar y generar el .tap final. La versión para Windows queda así:
echo off
cls
echo Compilando oxo
pasmo --name TresEnRaya --tap Main.asm oxo.tap oxo.log
echo Compilando int
pasmo --name Int --tap Int.asm int.tap int.log
echo Compilando loadscr
pasmo --name LoadScr --tap LoadScr.asm loadscr.tap loadscr.log
echo Generando TresEnRaya
copy /b /y cargador.tap+loadscr.tap+TresEnRayaScr.Tap+oxo.tap+int.tap TresEnRaya.tap
echo Proceso finalizado
El aspecto de la versión para Linux sería éste:
clear
echo Compilando oxo
pasmo --name TresEnRaya --tap Main.asm oxo.tap oxo.log
echo Compilando int
pasmo --name Int --tap Int.asm int.tap int.log
echo Compilando loadscr
pasmo --name LoadScr --tap LoadScr.asm loadscr.tap loadscr.log
echo Generando TresEnRaya
cat cargador.tap loadscr.tap TresEnRayaScr.Tap oxo.tap int.tap > TresEnRaya.tap
echo Proceso finalizado
Y con esto hemos terminado. Compilad, cargad en el emulador y a jugar.
Ensamblador ZX Spectrum, conclusión
En este capítulo hemos añadido dos opciones al menú de inicio, somos capaces de predecir si va a haber tablas antes de que el tablero esté lleno, hemos ahorrado bytes y ciclos de reloj, modificado los movimientos del Spectrum para que sea más difícil ganarle y añadido un pantalla de carga que no se muestra poco a poco, se muestra de golpe.
En este tutorial hemos ido mucho más rápido que en los anteriores, ya tenemos los conocimientos suficientes como para que no sea necesario explicar las instrucciones una a una.
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.