Había olvidado completamente este juego, pero por suerte había hecho respaldos de mis discos flexibles para propósitos históricos. Recientemente miré mi indice de respaldo de discos flexibles y encontré cosas tan antiguas como 1989. Un nombre atrapó mi atención: Viboritas.
Este juego fue desarrollado en 1990, impreso, después guardado en un disco flexible de 5 1/4", y después copiado nuevamente en un disco flexible de 3 1/2" alrededor de 1992 cuando se volvieron populares, y finalmente, se respaldó en algún momento de 2011. Estos respaldos tuvieron suerte de sobrevivir varios discos duros que fallaron hasta que llegamos a 2024.
Abrí el archivo, extraje el binario de 2K, y mis recuerdos comenzaron a brotar. Incluso atrapé un error que duró 34 años.
La historia
Después de hacer mi primer juego en ensamblador Z80 en 1988, obtuve mucha confianza en mis habilidades y comencé a escribir varios programas para Z80. Sin embargo, no tenía acceso a un programa ensamblador así que codificaba directamente en lenguaje máquina, usando un programa monitor justo como el que se muestra en la foto de la derecha. ¡De hecho se puede ver incluso el código Z80 tecleado!
Un programa monitor era una pequeña aplicación en la ROM de la computadora donde se tenían algunas órdenes básicas como listado de memoria, escribir a la memoria y ejecutar tu programa. Algunos extras eran puntos de salida para tu programa y mostraban el contenido de los registros Z80 y las banderas.
Mi padre dio de nuevo su curso sobre construcción de computadoras en 1990, y yo mostré mi juego Karateka, pero quería hacer algo mejor. Tenía 11 años, a punto de volverme un adolescente, y tenía una gran imaginación de lo que debía ser un juego impresionante.
Mi entorno de desarrollo para este juego era una computadora homebrew para estudiantes con un teclado, una TV para visualizar, y un conjunto de hojas de Zilog con los nemónicos Z80 y su código máquina respectivo.
Las especificaciones de esta computadora homebrew eran un CPU Zilog Z80, 2K de EPROM, 2K de RAM, un VDP TMS9118 y un teclado Commodore. Se podía enchufar un tablero de expansión con un chip de sonido AY-3-8910 que se alimentaba del reloj del Z80. El chip VDP, el chip AY y el teclado estaban disponibles a causa del crash de 1984.
Estudiantes trabajando en la computadora homebrew. Se puede ver un joven @nanochess en el frente a la derecha. Diciembre de 1990.
Dentro del binario
En ese momento estaba muy inspirado por mi revista favorita: Input MSX, en este caso el número 12, conteniendo fotos de un juego llamado Future Knight, y yo estaba emocionado imaginando lo fabuloso que era por las pantallas y el arte del juego (después descubrí que era un juego bastante aburrido). En el mismo número había un juego en lenguaje BASIC llamado "El Castillo Embrujado" que ya había portado al lenguaje BASIC de otra computadora homebrew construida por mi padre.
Para este proyecto quería hacer un juego de ciencia ficción en 2K, donde el jugador usara escaleras, evitara enemigos, y... no tenía más. Pero estaba inspirado por el montón de escaleras en Future Knight. Y no tenía ni idea de lo que era el "size coding", pero ya tenía el límite de 2K en mi mente, y el otro objetivo era que el juego debía ser escrito directamente en la computadora de los estudiantes, ahora más conocido como escribir en hardware real.
No me imaginé un final para el juego, ni una historia, ni siquiera como se iba a jugar. Después de todo solo quería divertirme escribiendo juegos, y por supuesto, mostrar mi juego a los estudiantes. Nótese que divertirse escribiendo juegos no es lo mismo que escribir juegos divertidos.
Creo que este es mi tercer juego en código máquina para Z80. El binario viene de un disco de demostración de 1992 para estudiantes, pero originalmente se escribió en 1990. Solo extraje los 2K del juego de la imagen de disco de 720K.
El binario del juego luce como esto:
Dada la falta de información acerca de este juego ¡Tendré que hacer ingeniería en reversa de mi propio juego! El primer paso fue desensamblarlo por completo. El total fue de casi mil líneas de código ensamblador Z80 pero ya es más legible así:
Separé las llamadas de ROM y no recuerdo tener una copia de la ROM de esta computadora, pero todavía recuerdo la función de cada llamada (son las mismas que en mi primer juego Z80). L0100 pone una dirección del VDP (para VRAM o registro del VDP), L0169 lee bytes y envía datos a la VRAM, L04CC limpia la pantalla y L0447 lee el teclado. Una cosa que cambió entre las computadoras de 1988 y 1990 fueron los números de puerta del VDP.
Me aseguré de que el listado desensamblado se ensamblara al mismo binario que el original. Entonces comencé por separado la adaptación para computadoras MSX y consolas Colecovision, ya que ambas tienen el mismo procesador de video, y para que puedas jugar el juego. Sólo no esperes demasiado de un niño.
Primeros pasos
Para comenzar reutilizaré la capa de traslación que hice para mi juego Karateka en ensamblador Z80. Esto nos ayudará a jugar este viejo código en un MSX o Colecovision.
;
; Viboritas (little snakes)
;
; by Oscar Toledo G.
; (c) Copyright Oscar Toledo G. 1990-2024
; https://nanochess.org/
;
; Creation date: Oct/1990. I was 11 years old.
; Revision date: Jan/31/2024. Disassembled.
; Revision date: Feb/01/2024. Ported to MSX/Colecovision.
;
COLECO: EQU 1 ; Define this to 0 for MSX, 1 for Colecovision
RAM_BASE: EQU $E000-$7000*COLECO
VDP: EQU $98+$26*COLECO
PSG: EQU $FF ; Colecovision
PSG_ADDR: EQU $A0 ; MSX
PSG_DATA: EQU $A1 ; MSX
KEYSEL: EQU $80
JOYSEL: EQU $C0
JOY1: EQU $FC
JOY2: EQU $FF
if COLECO
fname "viboritas_cv.ROM"
org $8000,$9fff
dw $aa55 ; No BIOS title screen
dw 0
dw 0
dw 0
dw 0
dw START
jp 0 ; RST $08
jp 0 ; RST $10
jp 0 ; RST $18
jp 0 ; RST $20
jp 0 ; RST $28
jp 0 ; RST $30
jp 0 ; RST $38
jp 0 ; No NMI handler
else
fname "viboritas_msx.ROM"
org $4000,$5fff
dw $4241
dw START
dw 0
dw 0
dw 0
dw 0
dw 0
dw 0
WRTPSG: equ $0093
SNSMAT: equ $0141
endif
WRTVDP:
ld a,b
out (VDP+1),a
ld a,c
or $80
out (VDP+1),a
ret
SETWRT:
ld a,l
out (VDP+1),a
ld a,h
or $40
out (VDP+1),a
ret
WRTVRM:
push af
call SETWRT
pop af
out (VDP),a
ret
FILVRM:
push af
call SETWRT
.1: pop af
out (VDP),a
push af
dec bc
ld a,b
or c
jp nz,.1
pop af
ret
; Setup VDP before game
setup_vdp:
LD BC,$0200
CALL WRTVDP
LD BC,$C201 ; No interrupts
CALL WRTVDP
LD BC,$0F02 ; $3C00 for pattern table
CALL WRTVDP
LD BC,$FF03 ; $2000 for color table
CALL WRTVDP
LD BC,$0304 ; $0000 for bitmap table
CALL WRTVDP
LD BC,$3605 ; $1b00 for sprite attribute table
CALL WRTVDP
LD BC,$0706 ; $3800 for sprites bitmaps
CALL WRTVDP
LD BC,$0407 ; Blue border
CALL WRTVDP
IF COLECO
LD HL,($006C) ; MSX BIOS chars
LD DE,-128
ADD HL,DE
ELSE
LD HL,($0004) ; MSX BIOS chars
INC H
ENDIF
PUSH HL
LD DE,$0100
LD BC,$0300
CALL LDIRVM
POP HL
PUSH HL
LD DE,$0900
LD BC,$0300
CALL LDIRVM
POP HL
LD DE,$1100
LD BC,$0300
CALL LDIRVM
LD HL,$2000
LD BC,$1800
LD A,$F4
CALL FILVRM
RET
LDIRVM:
EX DE,HL
.1: LD A,(DE)
CALL WRTVRM
INC DE
INC HL
DEC BC
LD A,B
OR C
JR NZ,.1
RET
GTTRIG:
if COLECO
out (KEYSEL),a
ex (sp),hl
ex (sp),hl
in a,(JOY1)
ld c,a
in a,(JOY2)
and c
ld c,a
out (JOYSEL),a
ex (sp),hl
ex (sp),hl
in a,(JOY1)
and c
ld c,a
in a,(JOY2)
and c
rlca
rlca
ccf
ld a,0
sbc a,a
ret
else
xor a
call $00d8
or a
ret nz
ld a,1
call $00d8
or a
ret nz
ld a,2
call $00d8
or a
ret nz
ld a,3
call $00d8
or a
ret nz
ld a,4
call $00d8
ret
endif
;
; Gets the joystick direction
; 0 - No movement
; 1 - Up
; 2 - Up + right
; 3 - Right
; 4 - Right + down
; 5 - Down
; 6 - Down + left
; 7 - Left
; 8 - Left + Up
;
GTSTCK:
if COLECO
out (JOYSEL),a
ex (sp),hl
ex (sp),hl
in a,(JOY1)
ld b,a
in a,(JOY2)
and b
and $0f
ld c,a
ld b,0
ld hl,joy_map
add hl,bc
ld a,(hl)
ret
joy_map:
db 0,0,0,6,0,0,8,7,0,4,0,5,2,3,1,0
else
xor a
call $00d5
or a
ret nz
ld a,1
call $00d5
or a
ret nz
ld a,2
jp $00d5
endif
; ROM routines I forgot
; Clean screen
L04CC: ; $04cc
LD HL,$3C00
LD BC,$0300
XOR A
JP FILVRM
; Select address or register in VDP
L0100:
LD A,L
OUT (VDP+1),A
LD A,H
ADD A,$40
OUT (VDP+1),A
RET
; Copy string to VDP
L0169: ; $0169
EX (SP),HL
.0: LD A,(HL)
INC HL
OR A
JR Z,.1
PUSH AF
POP AF
OUT (VDP),A
JR .0
.1: EX (SP),HL
RET
;
; Start of the game
;
START: ; 8000
DI ; We don't need interruptions.
LD SP,L87F0
if COLECO
CALL $1FD6 ; Turn off sound.
endif
CALL setup_vdp ; Not in original but needed to setup VDP.
ld hl,$7513
ld (L8780),hl
ld hl,$983f
ld (L8782),hl
ld hl,$c9bf
ld (L8784),hl
Ahora comenzamos a analizar el código, intentando descubrir como funciona. Iremos adelante y atrás en el mapa de memoria de 2K y como está escrito en código máquina no hay nombres para las etiquetas excepto por su dirección correspondiente en el binario original.
La primera llamda es bastante obvia, simplemente limpia la pantalla. Nótese que el juego original asume que el VDP ya estaba inicializado, pero ya tomamos cuidado de esto con CALL setup_vdp.
La siguiente llamada L801B aparentemente inicia los gráficos.
L801B: LD HL,$0400 ; VRAM bitmap data $80 character.
LD DE,L806A
LD BC,$00C8
CALL L805D
LD HL,$2400 ; VRAM color data $80 character (1st).
LD DE,L8112
LD BC,$001B
CALL L8148
LD HL,$2C00 ; VRAM color data $80 character (2nd).
LD DE,L8112
LD BC,$001B
CALL L8148
LD HL,$3400 ; VRAM color data $80 character (3rd).
LD DE,L8112
LD BC,$001B
CALL L8148
LD HL,$3800 ; VRAM sprite bitmaps.
LD DE,L8404
LD BC,$0100
CALL L805D
LD HL,$4400 ; Obviously a patch.
JP L83FB
L83FB: CALL L0100
LD HL,$41C2
JP L0100
La dirección destino para la VRAM está en el registro HL, mientras que la dirección origen está en DE y la cuenta de bytes es en BC. Esto esta al revés de las definiciones Z80 estándar para LDIR, o la subrutina LDIRVM del BIOS de MSX.
Como esta codificado en código máquina cada error era latoso de corregir ¡En especial si se necesitaba insertar instrucciones adicionales! Como puedes ver el salto a $83FB continua poniendo el registro 4 del VDP a cero y entonces procede a poner el registro 1 del VDP para sprites de 16x16.
El modo de alta resolución del VDP necesita tener definiciones de bitmap separadas para tres áreas de 64 pixeles de alto para hacer un total de 192 líneas verticales. Poner el registro 4 del VDP es un truco para que el VDP repita el bitmap de la primera área en las otras dos áreas. Encontré ese truco unos pocos meses antes experimentando diferentes valores para los registros del VDP.
Así que marcamos L801B como "Inicializar gráficos".
Entonces tenemos la subrutina L805D que simplemente copia datos de la memoria hacia el VRAM.
L805D: CALL L0100
L8060: LD A,(DE)
OUT (VDP),A
INC DE
DEC BC
LD A,B
OR C
JR NZ,L8060
RET
Ahora puede verse porque lo hice de esa forma, ya que la subrutina L0100 usa HL como dirección del VDP y entonces era más fácil para mí tener los datos fuentes apuntados por el registro DE. Reemplacé la puerta original $b0 (escritura al VDP) y $c0 (lectura del VDP) con la definición del VDP para la consola (MSX o Colecovision).
A continuación siguen 200 bytes de bitmaps para el juego:
L806A: db $FF,$FF,$FF,$FF ; $806A
db $FF,$FF,$FF,$FF ; $806E
db $E7,$E7,$E7,$E7 ; $8072
db $E7,$E7,$E7,$E7 ; $8076
db $FF,$FF,$00,$FF ; $807A
db $FF,$00,$FF,$FF ; $807E
db $42,$42,$7E,$42 ; $8082
db $42,$7E,$42,$42 ; $8086
db $FE,$82,$BA,$AA ; $808A
db $BA,$82,$FE,$00 ; $808E
db $BA,$BA,$BA,$BA ; $8092
db $BA,$BA,$BA,$BA ; $8096
db $EE,$00,$FF,$FF ; $809A
db $FF,$00,$00,$00 ; $809E
db $42,$42,$7E,$42 ; $80A2
db $42,$7E,$42,$42 ; $80A6
db $EF,$EF,$EF,$00 ; $80AA
db $FE,$FE,$FE,$00 ; $80AE
db $7E,$7E,$7E,$00 ; $80B2
db $6E,$6E,$6E,$00 ; $80B6
db $00,$FF,$FF,$AA ; $80BA
db $44,$00,$00,$00 ; $80BE
db $42,$42,$7E,$42 ; $80C2
db $42,$7E,$42,$42 ; $80C6
db $EE,$EE,$EE,$00 ; $80CA
db $EE,$EE,$EE,$00 ; $80CE
db $40,$30,$0C,$03 ; $80D2
db $0C,$30,$40,$40 ; $80D6
db $00,$FF,$00,$AA ; $80DA
db $55,$00,$FF,$00 ; $80DE
db $81,$81,$C3,$BD ; $80E2
db $81,$81,$C3,$BD ; $80E6
db $81,$58,$37,$47 ; $80EA
db $39,$27,$49,$27 ; $80EE
db $47,$49,$27,$40 ; $80F2
db $28,$15,$12,$27 ; $80F6
db $00,$FE,$FE,$00 ; $80FA
db $EF,$EF,$00,$00 ; $80FE
db $0C,$0C,$18,$18 ; $8102
db $30,$30,$18,$18 ; $8106
db $54,$FE,$54,$FE ; $810A
db $54,$FE,$54,$00 ; $810E
Esto incluye paredes, columnas, pisos y escaleras (4 caracteres para cada nivel), para un total de 5 niveles, más una clase de cubierta de drenaje. Sigue la tabla de color para estos bitmaps.
L8112: db $08,$22,$08,$3C ; $8112
db $08,$A1,$08,$F1 ; $8116
db $08,$74,$08,$E1 ; $811A
db $01,$F1,$01,$11 ; $811E
db $03,$E1,$03,$11 ; $8122
db $08,$F1,$08,$6E ; $8126
db $08,$E1,$10,$F1 ; $812A
db $08,$61,$08,$A1 ; $812E
db $03,$F1,$02,$51 ; $8132
db $03,$F1,$08,$E1 ; $8136
db $08,$98,$08,$32 ; $813A
db $01,$11,$06,$6E ; $813E
db $01,$11,$08,$31 ; $8142
db $08,$F1 ; $8146
¿Qué es esto? Estos datos no pueden ser copiados directamente al VDP, en lugar de eso parece que tenemos contadores de bytes.
L8148: CALL L0100
LD B,C
L814C: PUSH BC
LD A,(DE)
LD B,A
INC DE
LD A,(DE)
INC DE
L8152: OUT (VDP),A
NOP
DJNZ L8152
POP BC
DJNZ L814C
RET
Y es correcto, el niño era suficientemente inteligente para crear un decompresor que lee una cuenta de bytes y un byte por replicar. Así que 73 bytes reemplazan 200 bytes de color.
Bitmaps usados para los fondos de nivel. Nótese el orden de pared, columna, piso y escalera.
¿Recuerdas que mencioné "El Castillo Embrujado" de Input MSX? A mis 11 años no tenía mucha confianza en mis habilidades de diseño gráfico, así que para mi juego reutilicé los sprites del jugador y las serpientes. Muchos años después descubrí que los gráficos del "El Castillo Embrujado" eran de hecho una copia de otro juego, el famoso Abu-Simbel Profanation para ZX Spectrum.
Sin embargo para este artículo ayudaré a un joven yo diseñando nuevos gráficos. Si tienes curiosidad puedes ver el conjunto original en el archivo viboritas_orig.asm
A la izquierda puedes ver los sprites de 1990 y a la derecha los sprites actualizados.
Nuevo conjunto de sprites para Viboritas.
;
; Sprites for the player and half of the snakes.
;
L8404:
; $00 - Player going right (frame 1).
DB $00,$01,$05,$03,$07,$03,$07,$1e
DB $37,$67,$77,$74,$03,$0e,$0e,$0f
DB $00,$50,$f0,$f0,$d0,$70,$10,$e0
DB $00,$b8,$b8,$00,$c0,$f8,$7c,$00
; $04 - Player going right (frame 2).
DB $00,$02,$01,$03,$01,$03,$03,$07
DB $07,$06,$06,$07,$03,$03,$03,$03
DB $a8,$f8,$f8,$e8,$b8,$88,$70,$80
DB $c0,$e0,$e0,$00,$c0,$00,$c0,$e0
; $08 - Player going left (frame 1).
DB $00,$0a,$0f,$0f,$0b,$0e,$08,$07
DB $00,$1d,$1d,$00,$03,$07,$1e,$3e
DB $00,$80,$a0,$c0,$e0,$c0,$e0,$78
DB $ec,$e6,$ee,$2e,$c0,$70,$70,$f0
; $0c - Player going right (frame 2).
DB $15,$1f,$1f,$17,$1d,$11,$0e,$01
DB $03,$07,$07,$00,$03,$00,$03,$07
DB $00,$40,$80,$c0,$80,$c0,$c0,$e0
DB $e0,$60,$60,$e0,$c0,$c0,$c0,$c0
; $10 - Player using ladder (frame 1).
DB $0a,$07,$0f,$0f,$07,$07,$03,$0c
DB $1b,$70,$73,$02,$06,$06,$1e,$3e
DB $a0,$c0,$e0,$e0,$ce,$ce,$98,$70
DB $c0,$00,$c0,$60,$38,$3c,$00,$00
; $14 - Player using ladder (frame 2).
DB $05,$03,$07,$07,$73,$73,$19,$0e
DB $03,$00,$03,$06,$1c,$3c,$00,$00
DB $50,$e0,$f0,$f0,$e0,$e0,$c0,$30
DB $d8,$0e,$ce,$40,$60,$60,$78,$7c
; $18 - Snake going left (frame 1).
DB $1b,$2d,$2d,$36,$1f,$7d,$9b,$03
DB $0f,$1f,$3e,$3c,$3c,$3f,$1f,$0f
DB $00,$00,$00,$00,$00,$80,$80,$82
DB $02,$06,$06,$0e,$cc,$ec,$fc,$38
; $1c - Snake going right (frame 2).
DB $00,$0d,$16,$16,$1b,$0f,$1e,$5d
DB $61,$0f,$1f,$1e,$1e,$1f,$0f,$07
DB $00,$80,$80,$80,$00,$80,$c0,$c0
DB $c0,$84,$0c,$cc,$d8,$f8,$b8,$30
Reproductor de música
En este momento del análisis aún no se ejecuta la siguiente rutina.
L815B: LD HL,L87FA
INC (HL)
LD A,(HL)
CP $08
JR NZ,L8194
LD (HL),$00
DEC HL
INC (HL)
LD A,(HL)
CP $30
JR NZ,L816F
LD (HL),$01
L816F: LD A,(HL)
ADD A,255 AND (L8744-1)
LD L,A
LD H,(L8744-1)>>8
CALL L8197
NOP
LD A,(HL)
OUT ($80),A
INC HL
LD A,$01
OUT ($00),A
LD A,(HL)
OUT ($80),A
LD A,$07
OUT ($00),A
LD A,$B8
OUT ($80),A
LD A,$08
OUT ($00),A
LD A,$0A
OUT ($80),A
L8194: JP L8398
L8197: LD A,(HL)
ADD A,A
ADD A,255 AND (L81A2-2)
LD L,A
LD H,(L81A2-2)>>8
XOR A
OUT ($00),A
RET
L81A2: dw $01ac
dw $0153
dw $011d
dw $00fe
dw $00f0
dw $0140
dw $00d6
dw $00be
dw $00b4
dw $00aa
dw $00a0
dw $00e2
L8744: db $01,$02,$03,$04 ; $8744
db $05,$04,$03,$02 ; $8748
db $01,$02,$03,$04 ; $874C
db $05,$04,$03,$02 ; $8750
db $06,$04,$07,$08 ; $8754
db $09,$08,$07,$04 ; $8758
db $06,$04,$07,$08 ; $875C
db $09,$08,$07,$04 ; $8760
db $03,$0C,$08,$0A ; $8764
db $0B,$0A,$08,$0C ; $8768
db $06,$04,$07,$08 ; $876C
db $09,$08,$07,$04 ; $8770
Bien, incrementa un byte en L87FA y cuando alcanza el valor 8 lo reinicia a cero y procede a incrementar L87F9 hasta que alcance 48 y lo reinicia a uno. ¡Por supuesto! L87F9 es el indice en la tabla de la música y L87FA es el contador de duración de cada nota.
Entonces usa el indice para obtener la nota a ejecutar de L8744. Aquí hay un truco de código máquina que no es útil al convertidor a nemónicos ensamblador: Si sabes que los datos están en una dirección fija, no hay manejo de acarreo para el byte alto de la direccion.
Ahora tenemos otro parche, esta vez llamando L8197 para tener la frecuencia de la nota a ejecutar, y entonces escribir al chip de sonido AY-3-8910. La dirección de puerta $00 pone el registro del AY-3-8910, y la dirección de puerta $80 pone el dato para el registro del AY-3-8910. Incluso pone el registro 7 del PSG a $38 para desactivar el ruido blanco, y este valor puede quemar de hecho algunas computadoras MSX1. Reemplacemos el código de sonido:
CALL L8197
if COLECO
LD A,(HL)
INC HL
LD H,(HL)
LD L,A
AND $0F
OR $80
OUT (PSG),A
SRL H
RR L
SRL H
RR L
SRL H
RR L
SRL H
RR L
LD A,L
OUT (PSG),A
LD A,$93
OUT (PSG),A
else
LD E,(HL)
LD A,0
CALL WRTPSG
INC HL
LD E,(HL)
LD A,1
CALL WRTPSG
LD E,$0A
LD A,$08
CALL WRTPSG
endif
L8194: JP L8398
L8197: LD A,(HL)
ADD A,A
ADD A,255 AND (L81A2-2)
LD L,A
LD H,(L81A2-2)>>8
RET
También al primer vistazo pensé que esta rutina se había escrito primero ¡Pero realicé que ocupa el espacio de la tabla de color sin comprimir! Cuando optimicé la definición de color para usar compresión ¡agregué el reproductor de música en el espacio libre!
Esto significa que la primera versión de mi juego no tenía ninguna música, y que puede existir en algún lugar de mis archivos una impresión. Y justo recordé una cosa: codifiqué el juego sin música, y un estudiante conocía de música, así que me escribió unas notas musicales que implementé terriblemente debido a que no sabía nada de tiempos musicales.
La música es una versión de un boogie-woogie. El reproductor musical sigue y hay un parche muy raro saltando a L8398. Pero lo veremos más tarde, ya que parece código de teclado.
Inicialización
Ahora vayamos al siguiente código sin explorar en L81BA:
Esto luce como un código de inicialización (agreguemos una nota a L81BA como inicialización del juego).
Pronto descubriremos la función de cada variable. De nuevo limpia la pantalla, prepara el VDP a la última línea de la pantalla y muestra el mensaje de copyright. OTEK viene de las iniciales de mi padre (Oscar Toledo Esteva) que usamos al estilo de un nombre de empresa.
También muestra el número de vidas restantes, y... otro parche salta a L82A5.
L82A5: LD (L87FC),A
XOR A
LD (L87FE),A
LD (L87FF),A
RET
Son solo más inicializaciones de variables.
Dibujo de la pantalla
Ahora otra rutina L8217 que es llamada inmediatamente después en L8009:
L8217: LD A,(L87FC)
ADD A,A
ADD A,A
ADD A,$7C
LD (L87FD),A
LD HL,$3C00
CALL L0100
LD B,$A0
L8229: PUSH BC
LD B,$03
L822C: LD A,(L87FD)
OUT (VDP),A
INC HL
DJNZ L822C
LD A,(L87FD)
INC A
OUT (VDP),A
INC HL
POP BC
DJNZ L8229
LD HL,$3C80
LD B,$04
L8243: PUSH BC
PUSH HL
CALL L0100
LD B,$20
L824A: LD A,(L87FD)
ADD A,$02
OUT (VDP),A
INC HL
DJNZ L824A
POP HL
LD BC,$00A0
ADD HL,BC
POP BC
DJNZ L8243
LD HL,$4701
CALL L0100
LD HL,$2000
CALL L8277
LD HL,$2800
CALL L8277
LD HL,$3000
CALL L8277
JP L8288
Comienza por obtener el valor de la variable de L87FC, multiplicarla por 4, sumar $7c y salvar el resultado en L87FD. L87FC es inicializada a uno. Así que comienza con $80, y suena como el número de caracter para la definición de nivel. Entonces dibuja en secuencia 160 paredes (3 caracteres) + columnas (un caracter), y entonces dibuja encima 4 pisos comenzando en la línea 4 ($3c80), cada uno de 32 caracteres de largo, usando el caracter de L87FD sumado 2.
Pone el borde a negro ($4701), y entonces colorea a negro el conjunto de caracteres base ($00-$7f) para las tres zonas de la pantalla (tres llamadas a L8277).
L8277: LD A,H
ADD A,$04
LD B,A
CALL L0100
L827E: LD A,$F1
OUT (VDP),A
INC HL
LD A,H
CP B
JR NZ,L827E
RET
L8288: LD HL,$3E5E
CALL L0100
LD A,$94
OUT (VDP),A
LD HL,$3C80
CALL L82B0
LD HL,$3D20
CALL L82B0
LD HL,$3DC0
CALL L82B0
RET
El parche L8288 añade el caracter "drenaje" $94 en la parte inferior derecha de la pantalla. Todavía no comprendo porque no solo dibujé una puerta de 2x2, pero probablemente sentí que había limitaciones de espacio (definir los gráficos y dibujar los tiles).
Al final llama 3 veces la subrutina L82B0 con diferentes líneas de la pantalla como base:
L82B0: LD A,(L87FC)
LD B,A
LD A,$06
SUB B
LD B,A
L82B8: PUSH BC
PUSH HL
NOP
LD D,$00
NOP
CALL L82CB
LD E,A
ADD HL,DE
CALL L830F
POP HL
POP BC
DJNZ L82B8
RET
La subrutina L82B0 sustrae el número de nivel de 6 y llama una subrutina L82CB para obtener un desplazamiento, y entonces llama L830F para hacer algo.
L82CB: PUSH BC
PUSH DE
PUSH HL
LD HL,(L8780)
LD DE,(L8782)
LD BC,(L8784)
ADD HL,HL
ADD HL,HL
ADD HL,BC
ADD HL,DE
LD (L8780),HL
ADD HL,DE
ADD HL,DE
ADD HL,BC
ADD HL,BC
ADD HL,BC
ADD HL,HL
ADD HL,HL
ADD HL,HL
ADD HL,HL
ADD HL,DE
ADD HL,BC
LD (L8782),HL
ADD HL,DE
ADD HL,DE
ADD HL,BC
ADD HL,HL
ADD HL,DE
ADD HL,BC
ADD HL,BC
ADD HL,BC
ADD HL,BC
LD (L8784),HL
LD HL,(L8780)
LD DE,(L8782)
LD BC,(L8784)
ADD HL,DE
ADD HL,BC
LD A,H
ADD A,L
AND $1F
POP HL
POP DE
POP BC
RET
Resulta que L82CB luce como un generador de números aleatorios y solo genera un número entre 0 y 31 en el registro acumulador.
L830F: LD B,$05
LD A,(L87FD)
ADD A,$03
L8316: PUSH AF
CALL L0100
POP AF
OUT (VDP),A
LD DE,$0020
ADD HL,DE
DJNZ L8316
RET
¡Misterio resuelto! L830F dibuja una escalera en la pantalla. Cada escalera mide 5 líneas de alto y es dibujada usando el caracter base de nivel más 3. En el primer nivel dibuja 5 escaleras en cada piso, mientras que en el quinto nivel solo dibuja una escalera por piso. Es un intento de hacer el juego más díficil.
Con todo este código analizado podemos marcar con seguridad L8217 como el código para dibujar el nivel.
El héroe y los villanos
Ahora tenemos la primera rutina llamada del bucle principal y es L8324:
Carga HL con $1b00, apuntando a la Tabla de Atributos de Sprites. El lugar de VRAM donde se posicionan los sprites en la pantalla. Y comienza a leer variables y escribir la VRAM usando L8390 (muy similar a WRTVRM de MSX).
L8786 es la coordenada Y para el jugador, L8787 es la coordenada X para el jugador, L87FE es el cuadro de sprite para el jugador. El jugador es color blanco.
Ahora se deduce fácilmente que los enemigos se ponen en posiciones verticales fijas en la pantalla ($38, $60 y $88) usando la rutina genérica L86C5. Nótese también que salta en cadena a la subrutina L815B para reproducir la música de fondo, que a su vez salta en cadena a la subrutina de decodificación del teclado (L8398).
L86C5: LD A,(DE)
CALL L8390
INC DE
INC HL
LD A,(DE)
LD B,$18
CP $00
JR NZ,L86D4
LD B,$20
L86D4: LD A,(L8788)
XOR $01
LD (L8788),A
BIT 0,A
LD A,$00
JR Z,L86E4
LD A,$04
L86E4: ADD A,B
CALL L8390
INC HL
PUSH HL
LD HL,$390C
LD DE,L8704
LD BC,$0034
CALL L805D
POP HL
RET
El primer byte apuntado por DE es usado para la coordenada X del enemigo. Y el siguiente byte indica la dirección de movimiento para seleccionar el cuadro de sprite para el enemigo. También anima los cuadros usando L8788 para obtener cuadros de movimiento alternos (junto con B situando el cuadro base $18 o $20). Entonces hace algo realmente extraño, copia el área de memoria L8704 en la dirección de VRAM $390c. Oh, ya veo, define dos sprites muy tarde en el juego (son los dos cuadros de sprite para viboritas moviéndose a la derecha), es bastante obvio que no preví todos los sprites requeridos para el juego.
Para esta versión actualizada del juego modificaré ligeramente el código:
LD HL,$3900 ; Define frame sprites $20 and $24
LD DE,L8700 ; Data for snake going to right.
LD BC,$0040 ; Length of data.
CALL L805D ; Copy to VRAM.
Esto no cabe por 8 bytes, o hubiera tenido que mover una porción significativa del código para hacer espacio. En este caso mi opción podía ser mover la porción de código en $8504-$8533 (el complejo código para movimiento de enemigos) pero no tenía una orden MOVER en el programa monitor. Tenía que copiar código máquina manualmente a la nueva posición.
;
; Extra sprites for snakes going right.
;
L8700:
DB $00,$01,$01,$01,$00,$01,$03,$03
DB $03,$21,$30,$33,$1b,$1f,$1d,$0c
DB $00,$b0,$68,$68,$d8,$f0,$78,$ba
DB $86,$f0,$f8,$78,$78,$f8,$f0,$e0
DB $00,$00,$00,$00,$00,$01,$01,$41
DB $40,$60,$60,$70,$33,$37,$3f,$1c
DB $d8,$b4,$b4,$6c,$f8,$be,$d9,$c0
DB $f0,$f8,$7c,$3c,$3c,$fc,$f8,$f0
Movimiento del jugador
El reproductor de música encadena al código del teclado:
L83B5: LD HL,L8787
INC (HL)
INC (HL)
NOP
LD A,(HL)
CP $00
JR NZ,L83C2
LD (HL),$FE
L83C2: LD A,(L87FE)
CP $04
JR NZ,L83CD
LD A,$00
JR L83CF
L83CD: LD A,$04
L83CF: LD (L87FE),A
RET
En este punto sabemos que L8787 es la coordenada X del jugador (basados en la escrituras a la Tabla de Atributos de Sprites) y que el incremento doble nos indica que el jugador se mueve a la derecha. Si la coordenada X se vuelve 0, se reescribe con el límite $fe (254 pixeles). También anima el jugador entre los cuadros de sprite $00 y $04.
L83DD: LD HL,L8787
DEC (HL)
DEC (HL)
NOP
LD A,(HL)
CP $FE
JR NZ,L83EA
LD (HL),$00
L83EA: LD A,(L87FE)
CP $0C
JR NZ,L83F5
LD A,$08
JR L83F7
L83F5: LD A,$0C
L83F7: LD (L87FE),A
RET
La siguiente rutina es lo opuesto: mover el jugador a la izquierda por dos pixeles. También checa si excede el borde izquierdo y pone la coordenada X a cero. Anima el jugador intercambiando entre los cuadros de sprite $08 y $0c.
La instrucción NOP después de los decrementos me hace pensar que consideré mover el jugador horizontalmente a una velocidad de tres pixeles.
Usando escaleras
El código para permitir que el jugador vaya arriba y abajo sobre las escaleras esta muy parcheado, así que probablemente me costó mucho esfuerzo y experimentos.
Comencemos con el código para ir abajo (se codificó primero porque el jugador necesita ir abajo desde el piso de arriba):
;
; Move the player downward.
;
L8567: LD HL,$1B00
CALL L85A0
CALL L85AE
NOP
LD D,A
INC HL
CALL L85A0
CALL L85FF
RRCA
RRCA
LD E,A
LD A,D
LD L,A
LD H,$00
ADD HL,HL
ADD HL,HL
ADD HL,HL
ADD HL,HL
ADD HL,HL
LD D,$00
ADD HL,DE
LD DE,$3C40
ADD HL,DE
CALL L85A0
LD B,A
LD A,(L87FD)
ADD A,$03
CP B
RET NZ
CALL L85E9
ADD A,$02
L859C: LD (L8786),A
RET
L85A0: LD A,L
OUT (VDP+1),A
LD A,H
OUT (VDP+1),A
NOP
NOP
NOP
NOP
NOP
IN A,(VDP)
RET
L85AE: INC A
AND $F8
RRCA
RRCA
RRCA
RET
L85E9: LD D,$00
ADD HL,DE
LD A,(L87FE)
LD B,$14
CP $10
JR Z,L85F7
LD B,$10
L85F7: LD A,B
LD (L87FE),A
LD A,(L8786)
RET
L85FF: ADD A,$04
AND $F8
RRCA
RET
L8605: ADD A,$03
CP B
RET Z
POP HL
LD A,(L8786)
INC A
AND $F8
JP L8631
L8631: DEC A
NOP
LD (L8786),A
RET
El inocente niño lee de VRAM las coordenadas para el jugador, pero ¿por qué hice eso? Estas variables ya estaban disponibles en RAM.
Primero lee de VRAM $1b00 la coordenada Y del jugador en el registro D y la convierte al número de línea de la pantalla, y también lee la coordenada X en el registro E y lo convierte al número de columna. Finalmente toma ambos números y crea un apuntador a la pantalla en VRAM.
D = (Y + 1) / 8
E = (X + 4) / 8
HL = D * 32 + E + $3c20
Puedes ver que hice LD A,D seguido de LD L,A cuando bastaba simplemente LD L,D.
Lee el caracter de VRAM (CALL L85A0) y checa si el caracter es una escalera (el contenido de L87FD más 3). Llama el parche L85E9 que por alguna razón agrega un valor al contenido de HL, anima el jugador escalando (cuadros de sprite $10 y $14), y obtiene la coordenada Y del jugador para moverlo dos pixeles hacia abajo.
El código para mover el jugador arriba es bastante similar, y de alguna forma hice lo correcto al usar las coordenadas existentes en RAM. Esto significa que estaba alcanzando mis límites ¡y tener 2K de código máquina en la cabeza no es fácil!
Hay unas pocas diferencias más como que es un offset diferente de la pantalla ($3c20 en vez de $3c40), y el hecho de que llama L8605 para hacer la comparación el caracter de la escalera. Si no es una escalera, alinea el jugador verticalmente (de nuevo usando otro parche), y usa POP HL para volver al bucle principal en lugar de donde se le llamó. Si es una escalera, mueve el jugador dos pixeles arriba.
Ahora para el gran momento de la vergüenza: el jugador puede caminar en el aire. Debido a que el código para manejar izquierda y derecha nunca verifica si el jugador esta sobre un piso. Como los pisos siempre están la misma posición vertical, sería un simple cosa de checar si el jugador esta sobre una de las coordenadas Y válidas, pero ahora puedo recordar vagamente que tenía miedo de mover el código de nuevo. ¡Niño perezoso!
Ganando el juego
Cuando el jugador alcanza la rejilla en la parte inferior derecha de la pantalla, se debe apretar el botón de disparo para pasar el nivel. Recuerdo que disfruté mucho viendo a las estudiantes olvidarse de presionar el botón y ser atrapados por la viborita.
;
; Button press to exit level.
;
L8684: LD A,(L8786)
CP $87
RET NZ
LD A,(L8787)
CP $E8
RET C
CP $F8
RET NC
LD SP,L87F0
LD A,$0F
LD (L8786),A
XOR A
LD (L8787),A
LD A,(L87FC)
CP $05
JP NZ,L8014
LD HL,$3D4A
CALL L0100
CALL L0169
db "HAS GANADO !",0
LD A,$08
OUT ($00),A
XOR A
JP L87B4
L87B4: OUT ($80),A
JP L878A
L878A: LD B,$05 ; Big delay.
L878C: PUSH BC
LD BC,$0000
L8790: DEC BC
LD A,B
OR C
JR NZ,L8790
POP BC
DJNZ L878C
LD SP,L87F0 ; Reset Stack Pointer.
LD A,$0F ; Reset Y-coordinate for the player.
LD (L8786),A
XOR A ; Reset X-coordinate for the player.
LD (L8787),A
LD A,$01 ; Restart at level 1.
LD (L87FC),A
LD HL,L87F9
LD (HL),$00
INC HL
LD (HL),$00
JP L8009
La subrutina primero checa que la coordenada Y sea $87, y que la coordenada X este entre $e8 and $f7 (buena tolerancia) y si las condiciones se cumplen entonces reinicia el apuntador de stack, pone el jugador de nuevo en la parte superior izquierda de la pantalla, y si el nivel no es 5 salta a L8014 para incrementar el nivel, de lo contrario muestra el mensaje "HAS GANADO" en la pantalla.
También apaga la música en otro tierno ejemplo de salto en cadena debido al código excesivamente parcheado.
El código de sonido debe ser reescrito como esto:
db "HAS GANADO !",0
if COLECO
ld a,$9f
out (PSG),a
else
ld e,$00
ld a,$08
call WRTPSG
endif
JP L87B4
L87B4:
JP L878A
La subrutina L878A hace un gran retardo para que el mensaje "HAS GANADO !" permanezca en la pantalla y entonces reinicia el juego y envía el jugador de vuelta al nivel 1.
Movimiento del enemigo
La segunda subrutina llamada por el bucle principal es L8504 y muestra serios parches.
L8504: CALL L850A
JP L83D3
Llama L850A y entonces L83D3. L83D3 es un bucle para hacer que el juego corra más humanamente (todavía no conocía la interrupción del VDP y no la tenía conectada al procesador Z80). Después de poner BC a $1000 también actualiza el número de vidas en la pantalla.
L83D3: CALL L861E
L83D6: DEC BC
LD A,B
OR C
JR NZ,L83D6
AND A
RET
L861E: LD BC,$1000
LD A,(L87FB)
ADD A,$30
LD HL,$3EB2
PUSH AF
CALL L0100
POP AF
OUT (VDP),A
RET
La subrutina L850A es más larga:
L850A: LD HL,L87F3
CALL L8517
LD L,255 AND L87F5
CALL L8517
LD L,255 AND L87F7
L8517: INC HL
LD A,(HL)
OR A
LD B,$03
JR Z,L8520
LD B,$FD
L8520: DEC HL
LD A,(HL)
ADD A,B
LD (HL),A
CP $FF
JR NZ,L852D
INC HL
LD (HL),$01
JR L8533
L852D: OR A
JR NZ,L8533
INC HL
LD (HL),$00
L8533: CALL L855B
CP B
RET C
CP C
RET NC
LD A,L
SUB 255 AND L87F3
RRCA
ADD A,A
ADD A,A
ADD A,$04
LD L,A
LD H,$1B
LD A,L
OUT (VDP+1),A
LD A,H
OUT (VDP+1),A
NOP
NOP
NOP
NOP
IN A,(VDP)
LD B,A
LD A,(L8786)
INC A
CP B
RET NZ
JP L8613
L855B: DEC HL
LD A,L
AND $FE
OR $01
LD L,A
LD B,(HL)
JP L8738
L8738: LD A,(L8787)
LD C,B
DEC B
DEC B
DEC B
INC C
INC C
INC C
INC C
RET
Utilice L8517 cada vez con un apuntador a uno de los enemigos (L87F3, L87F5 y L87F7). Por cada enemigo lee la dirección actual y selecciona un desplazamiento en X (-3 o +3 pixeles) en el registro B. Si alcanza una cierta coordenada entonces cambia la dirección de movimiento.
Una vez que se ha hecho esto, otro parche llama a L855B para hacer que HL apunte exactamente a la coordenada X del enemigo (este código depende la dirección en memoria de las coordenadas del enemigo). Lee la coordenada X actual en el registro B y salta a otro parche en L8738 donde lee la coordenada X del jugador en A, hace una copia de B en C, sustrae 3 de B y añade 4 a C.
Cuando ha creado este ancho de colisión (mínimo en B y máximo en C) hace una comparación de A (la coordenada X del jugador) con B y retorna si es menor y una comparación con C y retorna si es mayor o igual.
Como el estado del enemigo no contiene su coordenada Y, determina el sprite en base a la dirección de los datos del enemigo, y lee la Tabla de Atributos de Sprites de VRAM para obtener la coordenada Y comparándola con la coordenada Y del jugador (L8786), y retorna si ambos no son iguales, de lo contrario salta a L8613 para que el jugador muera.
Hay un error en este código y el jugador puede morir accidentalmente mientras camina. No pude encontrar este bug de muerte accidental por años hasta hoy (06-feb-2024) cuando usé herramientas de depuración de BlueMSX. Es fácil una vez encontrado, cuando una víbora se alinea con el jugador regresa correctamente porque el jugador no está en el mismo piso que la víbora, pero pierde el valor del registro HL por la lectura de VRAM, y la coordenada X de la próxima víbora se leerá de ROM creando una víbora invisible fija en el siguiente piso. Fallará de forma aleatoria dependiendo de la plataforma. ¿Quieres corregirlo? Sólo reemplaza mi "ingeniosa" optimización en L850A para cargar cada vez el valor completo de HL con la dirección de los datos del enemigo en lugar de sólo el registro L. Caso cerrado, solo me tomó 34 años.
Ahora continuemos:
L8613: CALL L837A
DEC (HL)
SCF
LD SP,L87F0
JP L8637
L837A: XOR A
OUT ($00),A
LD A,$AE
OUT ($80),A
LD A,$01
OUT ($00),A
LD A,$06
OUT ($80),A
JP L8774
L8774: LD BC,$0000
L8777: DEC BC
LD A,B
OR C
JR NZ,L8777
JP L866F
L866F: LD A,$08
OUT ($00),A
XOR A
OUT ($80),A
LD BC,$0000
L8679: DEC BC
LD A,B
OR C
JR NZ,L8679
LD HL,L87FB
JR L86F8
L86F8: XOR A
LD (L87F9),A
LD (L87FA),A
RET
L8637: LD A,$0F
LD (L8786),A
XOR A
LD (L8787),A
LD A,(L87FB)
CP $FF
JP NZ,L8009
LD HL,$3D4A
CALL L0100
CALL L0169
db "FIN DE JUEGO",0
LD B,$05
L8660: PUSH BC
LD BC,$0000
L8664: DEC BC
LD A,B
OR C
JR NZ,L8664
POP BC
DJNZ L8660
JP L8000
Estoy bastante apenado de este encadenamiento de código. Vayamos por partes.
La primera línea de código en L8613 llama a L837A, el propósito último es cargar HL con L87FB para apuntar al número de vidas del jugador y decrementarlo.
Pero L837A también crea un efecto de sonido (¡Una novedad!) y salta a L8774 para un pequeño retardo y luego salta a L866F para apagar el volumen, realiza otro pequeño retardo, carga HL con el apuntador al número de vidas y reinicia las variables que usa el reproductor de música.
Después de decrementar el número de vidas, pone el flag de carry pero obviamente me perdí en este camino porque nunca se usa. El apuntador de pila se reinicia, el jugador se pone de nuevo al comienzo en L8637 y si todavía tiene vidas salta a L8009 para continuar el juego o de lo contrario ilustra un mensaje "FIN DE JUEGO", espera un tiempo más largo y reinicia completamente el juego saltando a L8000.
Se requiere parchear las rutinas de sonido L837A y L866F con esto:
L837A:
if COLECO
ld a,$8E
out (PSG),a
ld a,$2a
out (PSG),a
else
ld e,$ae
ld a,$00
call WRTPSG
ld e,$06
ld a,$01
call WRTPSG
endif
JP L8774
L866F:
if COLECO
ld a,$9f
out (PSG),a
else
ld e,$00
ld a,$08
call WRTPSG
endif
LD BC,$0000
L8679: DEC BC
Las variables utilizadas
La lista final de variables dentro del código son:
L8780: rb 2 ; Generador aleatorio 1.
L8782: rb 2 ; Generador aleatorio 2.
L8784: rb 2 ; Generador aleatorio 3.
L8786: rb 1 ; Coordenada Y del jugador.
L8787: rb 1 ; Coordenada X del jugador.
L8788: rb 1 ; Bit de animación para viboritas.
L87F3: rb 1 ; Coordenada X del enemigo 1.
L87F4: rb 1 ; Dirección X del enemigo 1.
L87F5: rb 1 ; Coordenada X del enemigo 2.
L87F6: rb 1 ; Dirección X del enemigo 2.
L87F7: rb 1 ; Coordenada X del enemigo 3.
L87F8: rb 1 ; Dirección X del enemigo 3.
L87F9: rb 1 ; Indice de nota para el reproductor de música.
L87FA: rb 1 ; Contador de ticks para el reproductor de música.
L87FB: rb 1 ; Vidas actuales.
L87FC: rb 1 ; Nivel actual.
L87FD: rb 1 ; Caracter base para dibujar nivel.
L87FE: rb 1 ; Cuadro de sprite para el jugador.
L87FF: rb 1 ; No usada, pero inicializada.
El apuntador de pila solía ser $87f0 para las computadoras de estudiantes de 2K en 1990, después se movió a $fff0 para 32K de RAM (1992).
Puede descargar la ROM lista para jugarse en un Colecovision o MSX, también he incluído el código fuente comentado. La única diferencia entre esto y mi juego de 1990 son los gráficos rediseñados y los ajustes a los colores de los niveles para mejorar la visibilidad. Los colores originales se mezclan mal en emuladores modernos (en 1990 podía ajustar el contraste en la TV Sony Trinitron).
Descarga viboritas.zip (24 kb). Código fuente, ROM para Colecovision y MSX, y binario original y desensamblado.
Se puede ensamblar el código usando tniASM v0.44.
Epílogo
Se pudieron ahorrar muchos bytes en este juego rediseñando partes como usar un byte extra para preservar la posición vertical de los enemigos, mover código de inicialización fuera del bucle principal (actualización de vidas y definición de sprites extras), utilizar datos disponibles en RAM en lugar de leer la VRAM, y compactar el reproductor de música.
Por otra parte, refleja mis habilidades en la época. Podía tener casi 2K de código máquina en mi cabeza, y no hacía planeación (denotado por la multitud de parches). Todavía estaba aprendiendo como codificar un juego de plataformas y no era muy hábil para dibujar gráficos.
Escribir juegos u otro código directamente en código máquina no es nada práctico. A pesar de que a primera vista se puede tener todo en la cabeza, te olvidarás completamente en unos pocos años, y a menos que se haga algo de documentación en papel ¡no hay comentarios que ayuden!
Me hubiera gustado usar un programa ensamblador si estos estuvieran disponibles, pero no tuve ninguno hasta escribir el mío unos años después. Las tiendas de software en México eran escasas y nunca hubiera podido encontrar algo tan esotérico como un ensamblador Z80 cuando la IBM PC ya era la máquina dominante.
Sin embargo, mi objetivo de desarrollar un juego en 2K de RAM fue logrado. Los estudiantes se sorprendieron de que un juego de verdad pudiera funcionar en su computadora. Creo que distribuí unas pocas copias como hojas impresas y otras pocas copias en disco.
Aprendí así como desarrollaba el juego y nunca volví a cometer el error de permitir que el jugador caminara en el aire. Pero todavía por muchos años, seguí escribiendo en código máquina y haciendo código espagueti cuando necesitaba insertar código. Pero esa es otra historia.
Extras
Estaba seguro de que había impreso el juego, pero no lo encontraba en mis documentos. De alguna forma mi padre guardó el folder en sus archivos, y estuve muy feliz de tomarle foto a las hojas. El código fue impreso con una impresora de matriz de puntos usando papel continuo (pueden verse los cortes en la foto).
Hice una comparación del código máquina contra el binaro que saqué del disco flexible ¡y es una versión previa! Hay un byte de gráficos que es diferente, empieza con 4 vidas (en lugar de 2) y las direcciones de puerta del VDP son diferentes (datos es $01 en lugar de $b0 y $c0 y la dirección es $02). Falta el código para reiniciar el juego cuando es ganado y en lugar de eso simplemente apaga el sonido y detiene el procesador con HALT.
Se pueden ver algunas indicaciones con mi letra de niño para personalizar el juego como la velocidad de la música, el número de vidas y la velocidad del juego. Cubrí el nombre del estudiante que me dio las notas para la música, ya que no tengo forma de contactarlo para pedirle permiso de mostrar su nombre, y el otro nombre es del autor de El Castillo Embrujado debido a los sprites del jugador y las serpientes (que ahora ya sabemos que provenían de Abu-Simbel Profanation para ZX Spectrum).
Agregué el binario y su desensamblado al archivo viboritas.zip.
Extras
El 10 de marzo de 2024, estaba buscando más programas escritos in mis primeros años, y encontré esta impresión detrás de unas notas. Era típico para nostros que reciclaramos papel usando la parte blanca de atrás. Lo sorprendente del asunto es el nombre original del juego ¿recuerdas que me gustaba el nombre Future Knight? Utilicé "El Guerrero del Futuro" (Future Warrior) como el título inicial del juego.
Además, fue increíble descubrir que ¡esta es la versión con la tabla de color sin comprimir! Las direcciones $8112 a $81b9 contienen la tabla de color lista para copiar a VRAM, así que esta versión no tiene reproductor de música. También las variables del generador de números aleatorios están localizadas en $8700-$8705.
Es una lástima que solo encontré la primera página de la impresión, así que no es suficiente para reconstruir el binario debido a las variables desplazadas, y por lo tanto no lo agregué al archivo zip.