Viboritas: Un juego de 2K hecho en 1990

Viboritas actualizado en acción
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

Computadora homebrew de 1990 para estudiantes
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 de 1990
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:
Parte del binario original de Viboritas
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í:

            ORG $8000

        FNAME "viboritas.bin"

L04CC:      EQU $04CC
L0100:      EQU $0100
L0169:      EQU $0169
L0447:      EQU $0447

L8000:      CALL L04CC
            CALL L801B
            CALL L81BA
L8009:      CALL L8217
L800C:      CALL L8324
            CALL L8504
            JR NC,L800C
L8014:      LD HL,L87FC
            INC (HL)
            JP L8009
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.

L8000:      CALL L04CC	; Clear the screen.
            CALL L801B	
            CALL L81BA	
L8009:      CALL L8217
L800C:      CALL L8324
            CALL L8504
            JR NC,L800C
L8014:      LD HL,L87FC
            INC (HL)
            JP L8009
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.
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
Comparación de sprites de 1990 y los sprites actualizados.
A la izquierda puedes ver los sprites de 1990 y a la derecha los sprites actualizados.
Nuevo conjunto de sprites para Viboritas.
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:

L81BA:      XOR A
            LD (L87F3),A
            LD (L87F4),A
            LD (L87F7),A
            LD (L87F8),A
            LD A,$F0
            LD (L87F5),A
            LD A,$01
            LD (L87F6),A
            LD A,$0F
            LD (L8786),A
            LD A,$00
            LD (L8787),A
            LD HL,$0000
            LD (L87F9),HL
            LD A,$02
            LD (L87FB),A
            CALL L04CC
            LD HL,$3EE9
            CALL L0100
            CALL L0169
            db "(C) OTEK 1990",0
            LD HL,$3EAC
            CALL L0100
            CALL L0169
            db "VIDAS:0",0
            LD A,$01
            JP L82A5
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:

L8324:      LD HL,$1B00
            LD A,(L8786)
            CALL L8390
            INC HL
            LD A,(L8787)
            CALL L8390
            INC HL
            LD A,(L87FE)
            CALL L8390
            INC HL
            LD A,$0F
            CALL L8390
            INC HL
            LD A,$38
            CALL L8390
            INC HL
            LD DE,L87F3
            CALL L86C5
            LD A,$0E
            CALL L8390
            INC HL
            LD A,$60
            CALL L8390
            INC HL
            LD DE,L87F5
            CALL L86C5
            LD A,$0E
            CALL L8390
            INC HL
            LD A,$88
            CALL L8390
            INC HL
            LD DE,L87F7
            CALL L86C5
            LD A,$0E
            CALL L8390
            JP L815B

L8390:      PUSH AF
            CALL L0100
            POP AF
            OUT (VDP),A
            RET
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:

L8398:      CALL L0447
            CP $10
            JP Z,L83DD
            CP $0F
            JP Z,L83B5
            CP $37
            JP Z,L85B5
            CP $38
            JP Z,L8567
            CP $02
            JP Z,L8684
            RET
Cada momento este código luce más como código espagueti y también necesita ser reescrito para manejo de joystick portable:

L8398:      CALL GTSTCK
            CP $07		; Going left?
            JP Z,L83DD
            CP $03		; Going right?
            JP Z,L83B5
            CP $01		; Going up?
            JP Z,L85B5
            CP $05		; Going down?
            JP Z,L8567
	    CALL GTTRIG
            CP $ff		; Button pressed?
            JP Z,L8684
            RET
La primera rutina de joystick es L83B5:

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.

	;
	; Move the player upward.
	;
L85B5:      LD A,(L8786)
            CALL L85AE
            LD D,A
            INC HL
            LD A,(L8787)
            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,$3C20
            ADD HL,DE
            CALL L85A0
            LD B,A
            LD A,(L87FD)
            CALL L8605
            NOP
            CALL L85E9
            SUB $02
            JP L859C
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).
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).
Impresión del código máquina de Viboritas, página 1 Impresión del código máquina de Viboritas, página 2 Impresión del código máquina de Viboritas, página 3
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.

Ligas relacionadas

Última modificación: 20-feb-2024