Mientras escribía felizmente mi artículo sobre Viboritas, noté un juego Cubos en el mismo disco pero no podía recordar de que se trataba.
Unos pocos días después cuando agregué los dibujos faltantes para Karateka, me di cuenta de que tenía un dibujo previo completo de la pantalla para Cubos. Y de nuevo, comencé a recordar cosas y corregir bugs de hace más de 30 años.
Televideo
El año era 1986, y mi padre había reconstruido un Televideo PC de partes. Teníamos unos pocos discos flexibles, y uno de los juegos era J-Bird por Greg Kuperberg. Ilustraba un pájaro saltando en una pirámida construida de cubos, y tenías que llenar cada cubo. En niveles avanzados era necesario saltar varias veces sobre el mismo cubo para obtener el color correcto mientras evitabas pelotas y serpientes.
Por supuesto, estoy describiendo Q*Bert, pero en ese momento no lo sabía, y era un concepto nuevo para mí. Me tomó años descubrir que J-Bird era un clon del arcade.
Jugué bastante J-Bird y otros juegos antiguos de PC ese año. Recuerdo estar hipnotizado por la apariencia seudo-3D pero no tenía idea de como lograr ese efecto en mis juegos.
Sin embargo ya había dibujado la siguiente idea para mi juego de salta cubos:
Dibujo previo de Cubos en mi cuaderno.
Dibujo previo del sprite del jugador para Cubos.
Puedes ver que mi mente de niño no se había dado cuenta aún de que la pirámide podía ser ceñida a los bordes de los cuadros (la rejilla de caracteres en la pantalla), y hacer eso me hubiera despertado a la idea de dibujar la pirámide completa usando caracteres predefinidos. Mi idea era de que el hombre casette luchaba contra los discos malvados y la serpiente sería un joystick.
J-Bird era demasiado para mi conocimiento de esa época, pero la idea perduró por algunos años hasta principios de 1991 cuando tenía 12 años.
El juego
El binario viene de un disco que utilicé mientras trabajaba en una de los computadoras homebrew construidas por mi padre. El disco en si no tenía un sistema operativo, en lugar de eso mostraba un menú para escoger los juegos, y comandaba las subrutinas de disco para traer el track del disco y cargarlo en la direción $8000.
Trabajé directamente en código máquina usando un programa monitor, así que uno de los primeros pasos es desensamblar el juego completo. Para hacerlo funcionar con MSX y Colecovision reusaremos la capa de traslación que hice para mi primer juego Z80.
;
; Cubos
;
; by Oscar Toledo G.
; https://nanochess.org/
;
; Creation date: Early 1991. I was age 12.
; Revision date: Feb/12/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 "cubos_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 "cubos_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
SETRD:
ld a,l
out (VDP+1),a
ld a,h
and $3f
out (VDP+1),a
ret
WRTVRM:
push af
call SETWRT
pop af
out (VDP),a
ret
RDVRM:
push af
call SETRD
pop af
ex (sp),hl
ex (sp),hl
in a,(VDP)
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,$8201 ; No interrupts, disable screen.
CALL WRTVDP
LD BC,$0602 ; $1800 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,$0107 ; Black border
CALL WRTVDP
LD HL,$0000
LD BC,$1800
XOR A
CALL FILVRM
LD HL,$2000
LD BC,$1800
LD A,$F1
CALL FILVRM
LD HL,$1B00
LD A,$D0
CALL WRTVRM
LD HL,$1800
.1:
LD A,L
CALL WRTVRM
INC HL
LD A,H
CP $1B
JP NZ,.1
LD BC,$C201 ; No interrupts, enable screen
CALL WRTVDP
RET
;
; 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
; 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
L0309:
CALL SETWRT
.1:
LD A,(DE)
OUT (VDP),A
INC DE
DEC BC
LD A,B
OR C
JP NZ,.1
RET
L0099: EQU setup_vdp
L0AE8: ; ???
RET
L0109: ; ???
RET
L0454: EQU RDVRM
L0447: EQU WRTVRM
START:
LD SP,STACK
El juego desensamblado ocupa 1292 líneas de ensamblador. Cada etiqueta está nombrada de acuerdo a su dirección original en el juego. Necesitamos seguir el código a través de varias etiquetas debido a mi estilo de programación de ese tiempo basado en parches.
El juego utiliza solo 4 bytes de RAM para datos de trabajo, debido a que tomé una terrible decisión de diseño para mantener las coordenadas X,Y del jugador y los enemigos en la VRAM y esto utilizó más código que tenerlas directamente en RAM normal.
La llamada a L836A parece inicializar tres variables, dos de ellas a cero, y la otra a uno, entonces termina llamando L0099 y L0AE8, por suerte recuerdo que estas se usaban para poner el modo de video de alta resolución. Bien, al menos L0099, porque no recuerdo la función de L0AE8.
Por alguna razón no observé que L8003 de nuevo llama L0099 para reiniciar el VDP, y procede a limpiar el espacio de color con blanco y negro. Mi amor por el MSX se nota aquí en la puerta $98 para escribir datos en el VDP, pero en realidad, era $90 en la computadora de mi padre, y para este juego voy a reemplazarla con la etiqueta VDP para poder ensamblar este juego tanto para MSX como para Colecovision.
Ahora pone el borde como color negro llamanda L0100 en L86E0.
L86E0: CALL L0100
LD DE,L8700
LD B,$12
L86E8: LD A,(DE)
LD H,A
INC DE
LD A,(DE)
LD L,A
INC DE
PUSH BC
LD B,$08
L86F1: LD A,(DE)
CALL L0447
INC HL
INC DE
DJNZ L86F1
POP BC
DJNZ L86E8
JP L87B4
L8700:
db $00,$00,$00
db $00,$00,$00,$03 ; $8703
db $03,$0F,$0F,$00 ; $8707
db $08,$07,$3F,$FF ; $870B
db $FF,$FF,$FF,$FF ; $870F
db $FF,$00,$10,$C0 ; $8713
db $F6,$FF,$F3,$FF ; $8717
db $FC,$FF,$F0,$00 ; $871B
db $18,$00,$00,$00 ; $871F
db $00,$60,$C0,$D0 ; $8723
db $50,$01,$00,$0F ; $8727
db $0F,$0F,$0F,$0F ; $872B
db $0F,$0E,$0E,$01 ; $872F
db $08,$FF,$FF,$FF ; $8733
db $FF,$C3,$00,$00 ; $8737
db $00,$01,$10,$F5 ; $873B
db $FC,$E5,$CD,$FE ; $873F
db $00,$00,$00,$01 ; $8743
db $18,$A0,$50,$A0 ; $8747
db $60,$50,$60,$70 ; $874B
db $60,$02,$00,$0E ; $874F
db $0E,$0E,$0E,$07 ; $8753
db $06,$04,$04,$02 ; $8757
db $08,$0F,$31,$C1 ; $875B
db $3D,$4F,$3D,$01 ; $875F
db $01,$02,$10,$0F ; $8763
db $30,$C0,$3C,$4E ; $8767
db $3C,$00,$00,$02 ; $876B
db $18,$A0,$A0,$50 ; $876F
db $40,$60,$80,$C0 ; $8773
db $C0,$03,$00,$03 ; $8777
db $03,$03,$03,$03 ; $877B
db $01,$01,$01,$03 ; $877F
db $08,$02,$0C,$04 ; $8783
db $03,$00,$00,$00 ; $8787
db $03,$03,$10,$00 ; $878B
db $60,$80,$00,$00 ; $878F
db $00,$00,$C0,$03 ; $8793
db $18,$40,$80,$40 ; $8797
db $80,$80,$00,$80 ; $879B
db $00,$04,$08,$07 ; $879F
db $03,$00,$80,$C0 ; $87A3
db $F0,$FE,$FF,$04 ; $87A7
db $10,$E0,$C0,$00 ; $87AB
db $01,$03,$0F,$7F ; $87AF
db $FF ; $87B3
Dibuja una serie de 18 caracteres en la pantalla tomando una dirección VRAM y 8 bytes de datos. Decidí conservar la dirección VRAM destino en formato big-endian en lugar del más común little-endian usado en el Z80.
Esto dibuja el bitmap de una cara en la parte superior izquierda de la pantalla. Creo que esto es algo que agregué después de hacer funcionar el juego debido a la dirección de memoria
Bitmap de cara dibujada en Cubos.
Entonces repite el mismo código para dibujar la misma cara en la parte derecha de la pantalla.
L87B4: LD DE,L87D0 ; Pointer to bitmap data for face.
LD B,$12 ; 18 characters.
L87B9: LD A,(DE) ; Read target VRAM address.
LD H,A
INC DE
LD A,(DE)
LD L,A
INC DE
PUSH BC
LD B,$08 ; 8 bytes.
L87C2: LD A,(DE) ; Read byte.
CALL L0447 ; Copy to VRAM.
INC HL
INC DE
DJNZ L87C2
POP BC
DJNZ L87B9 ; Repeat until all characters are copied.
JP L8884
L87D0: db $00,$E0,$00,$00 ; $87D0
db $00,$00,$03,$03 ; $87D4
db $0F,$0F,$00,$E8 ; $87D8
db $07,$3F,$FF,$FF ; $87DC
db $FF,$FF,$FF,$FF ; $87E0
db $00,$F0,$C0,$F6 ; $87E4
db $FF,$F3,$FF,$FC ; $87E8
db $FF,$F0,$00,$F8 ; $87EC
db $00,$00,$00,$00 ; $87F0
db $60,$C0,$D0,$50 ; $87F4
db $01,$E0,$0F,$0F ; $87F8
db $0F,$0F,$0F,$0F ; $87FC
db $0E,$0E,$01,$E8 ; $8800
db $FF,$FF,$FF,$FF ; $8804
db $C3,$00,$00,$00 ; $8808
db $01,$F0,$F5,$FC ; $880C
db $E5,$CD,$FE,$00 ; $8810
db $00,$00,$01,$F8 ; $8814
db $A0,$50,$A0,$60 ; $8818
db $50,$60,$70,$60 ; $881C
db $02,$E0,$0E,$0E ; $8820
db $0E,$0E,$07,$06 ; $8824
db $04,$04,$02,$E8 ; $8828
db $0F,$31,$C1,$3D ; $882C
db $4F,$3D,$01,$01 ; $8830
db $02,$F0,$0F,$30 ; $8834
db $C0,$3C,$DF,$3C ; $8838
db $00,$00,$02,$F8 ; $883C
db $A0,$A0,$50,$40 ; $8840
db $60,$80,$C0,$C0 ; $8844
db $03,$E0,$03,$03 ; $8848
db $03,$03,$03,$01 ; $884C
db $01,$01,$03,$E8 ; $8850
db $02,$0C,$04,$03 ; $8854
db $00,$00,$00,$03 ; $8858
db $03,$F0,$00,$60 ; $885C
db $80,$00,$00,$00 ; $8860
db $00,$C0,$03,$F8 ; $8864
db $40,$80,$40,$80 ; $8868
db $80,$00,$80,$00 ; $886C
db $04,$E8,$07,$03 ; $8870
db $00,$80,$C0,$F0 ; $8874
db $FE,$FF,$04,$F0 ; $8878
db $E0,$C0,$00,$01 ; $887C
db $03,$0F,$7F,$FF ; $8880
Pude haber ahorrado más de 200 bytes haciendo esto una subrutina y manteniendo un offset para repetir el dibujo. Creo que quería poner una cara diferente en el lado derecho de la pantalla, pero nunca lo hice.
L8884: LD HL,$2000
LD DE,L88A9
LD B,$05
L888C: PUSH BC
LD B,$04
L888F: PUSH BC
LD B,$08
L8892: LD A,(DE)
CALL L0447
INC HL
DJNZ L8892
INC DE
POP BC
DJNZ L888F
LD A,H
INC A
LD H,A
LD A,$00
LD L,A
POP BC
DJNZ L888C
JP L88BD
L88A9: db $1A,$1A,$1A,$1A ; $88A9
db $1A,$16,$18,$1A ; $88AD
db $1A,$16,$18,$1A ; $88B1
db $1A,$16,$18,$1A ; $88B5
db $1A,$16,$18,$1A ; $88B9
Ahora procede a poner color en la cara. Parece que finalmente me di cuenta de como explorar un rectángulo en la pantalla y se puede ver LD B,$05 para dibujar 5 líneas, luego LD B,$04 para dibujar 4 columnas, y finalmente LD B,$08 para repetir el color en un caracter completo de 8x8.
Cualquier codificador experimentado de Z80 se estremecerá al ver 5 líneas de ensamblador que se podían reemplazar con INC H / LD L.0.
Cara de bitmap con color dibujada en Cubos.
Entonces repetí exactamente el mismo código para darle color a la cara de la derecha:
L88BD: LD HL,$20E0 ; Point to the color data (right side)
LD DE,L88A9 ; Color data (reused)
LD B,$05 ; 5 rows.
L88C5: PUSH BC
LD B,$04 ; 4 columns.
L88C8: PUSH BC
LD B,$08 ; 8x8 pixels.
L88CB: LD A,(DE) ; Color data.
CALL L0447 ; Write to VRAM.
INC HL
DJNZ L88CB
INC DE
POP BC
DJNZ L88C8
LD A,$E0 ; Prepare for next row.
LD L,A
POP BC
DJNZ L88C5
JP L8984
Al menos esta vez reutilizé los datos de color en L88A9. El código encadenado continua:
Debido al valor $f1, pude distinguir que LEF13 contenía el código de color (blanco + negro). Los valores en HL y DE lucen extraños, nada como direcciones de VRAM, o direcciones de RAM, pero tenemos valores repetidos, y por supuesto, esto es una subrutina de dibujo de líneas en ROM (H=y1, L=x1, D=y2, E=x2)
Recuerdo vagamente que a principios de 1991 finalmente tuve una rutina de dibujo de líneas en la ROM de la computadora de mi padre.
Dibuja cuatro cuadrados y conecta estos con líneas diagonales. Dentro, dibuja letras para representar las teclas T, Y, G y H. Un pequeño manual visual.
Mapa de teclas dibujado por Cubos.
Solo un pequeño problema: No tengo la ROM para esta computadora, así que para este juego tuve que codificar otro algoritmo de línea Bresenham en Z80.
;
; Draw colored pixel
; D = Y-coordinate.
; E = X-coordinate.
;
L12DA:
call coor2vdp ; Get coordinate and bitmask.
ld c,a ; Save bitmask.
call RDVRM ; Read VRAM.
or c ; Set pixel.
call WRTVRM ; Write VRAM.
set 5,h ; Go to color area.
ld a,(LEF13) ; Get current color.
jp WRTVRM ; Set color for 8 pixels.
;
; Bresenham line drawing
; by Oscar Toledo G.
; Feel free to use it in your own projects.
; Just put my credit somewhere.
;
; H = Y1 coordinate.
; L = X1 coordinate.
; D = Y2 coordinate.
; E = X2 coordinate.
;
L1500:
ld a,d
sub h
ld b,1
jr nc,$+6
ld b,-1
neg
exx
ld b,a ; dy = abs(y2 - y1)
exx
ld a,e
sub l
ld c,1
jr nc,$+6
ld c,-1
neg
exx
ld c,a ; dx = abs(x2 - x1)
exx
exx
ld a,c
cp b ; dx >= dy?
jr c,.7 ; No, jump.
ld l,b
ld h,0
add hl,hl ; 2*dy
ld e,c
ld d,0
sbc hl,de ; -dx
exx
.1:
push bc
push de
push hl
ex de,hl
call L12DA ; Draw a colored pixel on the screen.
pop hl
pop de
pop bc
ld a,h
cp d
jr nz,.2
ld a,l
cp e ; Reached endpoint?
ret z ; Yes, return.
.2: exx
bit 7,h
exx
jr nz,.5
ld a,h
add a,b ; Displace Y-coordinate in Y direction.
ld h,a
exx
ld e,c
ld d,0
sla e
rl d
sbc hl,de ; d -= dx * 2
exx
.5: ld a,l
add a,c ; Displace X-coordinate in X direction.
ld l,a
exx
ld e,b
ld d,0
sla e
rl d
add hl,de ; d += dy * 2
exx
jr .1
.7:
ld l,c
ld h,0
add hl,hl ; 2*dx
ld e,b
ld d,0
sbc hl,de ; -dy
exx
.3:
push bc
push de
push hl
ex de,hl
call L12DA ; Draw a colored pixel on the screen.
pop hl
pop de
pop bc
ld a,h
cp d
jr nz,.4
ld a,l
cp e ; Reached endpoint?
ret z ; Yes, return.
.4: exx
bit 7,h ; d < 0?
exx
jr nz,.6 ; Yes, jump.
ld a,l
add a,c ; Displace X-coordinate in X direction.
ld l,a
exx
ld e,b
ld d,0
sla e
rl d
sbc hl,de ; d -= dy * 2
exx
.6: ld a,h
add a,b ; Displace Y-coordinate in Y direction.
ld h,a
exx
ld e,c
ld d,0
sla e
rl d
add hl,de ; d += dx * 2
exx
jr .3
El código termina cuando la última línea se dibuja, pero de alguna forma decidí insertar más código. Esta vez para mostrar el enemigo del nivel 1.
L8B97: CALL L1500
LD HL,$07D0
LD DE,L8270
CALL L8BA6
JP L8BE2
L8BA6: LD B,$08
L8BA8: LD A,(DE)
CALL L8C06
INC HL
INC DE
DJNZ L8BA8
LD A,L
LD B,$08
SUB B
LD L,A
LD A,H
INC A
LD H,A
LD B,$08
L8BBA: LD A,(DE)
CALL L8C06
INC HL
INC DE
DJNZ L8BBA
LD A,H
DEC A
LD H,A
LD B,$08
L8BC7: LD A,(DE)
CALL L8C06
INC HL
INC DE
DJNZ L8BC7
LD A,L
LD B,$08
SUB B
LD L,A
LD A,H
INC A
LD H,A
LD B,$08
L8BD9: LD A,(DE)
CALL L8C06
INC HL
INC DE
DJNZ L8BD9
RET
L8C06: CALL L0447
PUSH HL
PUSH DE
LD DE,$2000
ADD HL,DE
LD A,$C1
AND $F0
OR $01
CALL L0447
POP DE
POP HL
RET
L8270:
db $03,$0F,$1F,$30 ; $8270
db $37,$69,$6F,$6C ; $8274
db $6B,$37,$30,$DF ; $8278
db $CF,$63,$60,$90 ; $827C
db $C0,$F0,$F8,$0C ; $8280
db $EC,$96,$F6,$36 ; $8284
db $D6,$EC,$0C,$FB ; $8288
db $F3,$C6,$06,$09 ; $828C
El registro HL apunta a la coordenada para dibujar el enemigo, y el registro DE apunta al bitmap del enemigo. El bitmap del enemigo está diseñado para un sprite, así que la subrutina L8BA6 lee el sprite como cuatro caracteres separados para ilustrar (observe los cuatro LD B,$08 separados).
Creo que quería que el primer enemigo luciera como un león, pero en lugar de eso, parece un monstruo con un abrigo... un abrigo muy grande.
Cuando escribe el byte gráfico también pone el color a verde oscuro usando la subrutina L8C06. Esta subrutina lee el color en el registro A ($c1 para verde oscuro con fondo negro) y entonces hace una operación muy loca... para acabar con el mismo valor.
L8BE2: LD HL,$0AD0
LD DE,L85F8
CALL L8BA6
LD HL,$0DD0
LD DE,L8620
CALL L8BA6
LD HL,$10D0
LD DE,L8648
CALL L8BA6
LD HL,$13D0
LD DE,L8670
JP L8BA6
L85F8: db $06
db $0F,$07,$05,$07 ; $85F9
db $03,$01,$0F,$1B ; $85FD
db $1B,$1B,$03,$06 ; $8601
db $06,$06,$0E,$60 ; $8605
db $F0,$E0,$A0,$E0 ; $8609
db $C0,$80,$F0,$D8 ; $860D
db $D8,$D8,$C0,$60 ; $8611
db $60,$60,$70 ; $8615
L8620:
db $0F,$3F
db $09,$39,$0F,$3F ; $8622
db $0B,$3C,$0F,$3F ; $8626
db $0F,$3F,$0F,$3F ; $862A
db $0F,$3F,$F0,$FC ; $862E
db $90,$9C,$F0,$FC ; $8632
db $D0,$3C,$F0,$FC ; $8636
db $F0,$FC,$F0,$FC ; $863A
db $F0,$FC ; $863E
L8648:
db $01,$03
db $05,$07,$0F,$01 ; $864A
db $3F,$77,$77,$73 ; $864E
db $73,$F3,$0F,$06 ; $8652
db $06,$0E,$80,$C0 ; $8656
db $A0,$E0,$F0,$80 ; $865A
db $FC,$EE,$EE,$CE ; $865E
db $CE,$CF,$F0,$60 ; $8662
db $60,$70 ; $8666
L8670:
db $81,$41
db $21,$11,$09,$05 ; $8672
db $03,$FE,$03,$05 ; $8676
db $09,$11,$21,$41 ; $867A
db $81,$00,$02,$04 ; $867E
db $08,$10,$20,$40 ; $8682
db $80,$FF,$80,$40 ; $8686
db $20,$10,$08,$04 ; $868A
db $02,$00 ; $868E
Seguramente pensé que sería agradable mostrar todos los enemigos que pueden aparecer, así que repetí las llamadas a L8BA6 con apuntadores a los gráficos de los otros enemigos.
Los enemigos que aparecen en Cubos.
El enemigo para el segundo nivel es un chef (probablemente mi homenaje a Burgertime), y el tercero es un chip Z80, el cuarto es un verdugo (algo relacionado con la guillotina), y el quinto es una estrella, o tal vez un símbolo del omega, el final de todo, o más simplemente se me acabaron las ideas.
Después de este épico código parchado en cadena, retorna a donde fue llamado L86E0 y pone DE a $1080 y A con $05 y llama L8027.
L8027: LD B,A
L8028: PUSH BC
LD B,$06
L802B: NOP
CALL L80FD
NOP
INC D
NOP
DEC E
DJNZ L802B
LD B,$0C
L8037: PUSH DE
CALL L09B9
POP DE
INC D
DJNZ L8037
POP BC
DJNZ L8028
RET
L80FD: PUSH DE
CALL L09B9
POP DE
DEC E
PUSH DE
CALL L09B9
POP DE
RET
Esta subrutina dibuja 5 escalones yendo abajo y a la izquierda en la coordenada dada por DE (D es la coordenada Y y E es la coordenada X). Cada pixel es dibujado usando L09B9. La línea que va a la izquierda es dibujada como una línea escalonada de dos pixeles cada vez.
Puedo recordar vividamente cuando codifiqué esta subrutina de dibujar pixeles, pero no puedo recordar los detalles exactos, sin embargo para este artículo codifiqué un calculador de coordenada de pixeles (coor2vdp), una subrutina para escribir un pixel, una subrutina para borrar un pixel, y una subrutina para leer un pixel.
;
; Convert an X,Y coordinate to a bitmap VRAM address.
; D = Y-coordinate.
; E = X-coordinate.
;
; HL = Final VRAM address.
; A = Pixel bitmask.
;
coor2vdp:
ld a,d
rrca
rrca
rrca
and $1f
ld h,a
ld a,e
and $f8
ld l,a
ld a,d
and $07
or l
ld l,a
ld a,e
and $07
add a,pixel and 255
ld e,a
adc a,pixel>>8
sub e
ld d,a
ld a,(de)
ret
pixel: db $80,$40,$20,$10,$08,$04,$02,$01
; Set pixel.
L09B9:
call coor2vdp
ld c,a
call RDVRM
or c
call WRTVRM
RET
; Erase pixel.
L09E4: call coor2vdp
cpl
ld c,a
call RDVRM
and c
jp WRTVRM
; Test pixel.
L09FC: call coor2vdp
ld c,a
call RDVRM
and c
RET
Primera 'escalera' dibujada en Cubos.
El código continua en L8043 y hace una llamada a una subrutina similar.
L8043: LD DE,$1674
LD A,$05
CALL L804E
JP L806A
L804E: LD B,A
L804F: PUSH BC
LD B,$06
L8052: NOP
CALL L8109
NOP
INC D
NOP
INC E
DJNZ L8052
LD B,$0C
L805E: PUSH DE
CALL L09B9
POP DE
INC D
DJNZ L805E
POP BC
DJNZ L804F
RET
L8109: PUSH DE
CALL L09B9
POP DE
INC E
PUSH DE
CALL L09B9
POP DE
RET
La subrutina L804E hace exactamente lo mismo que L8027, excepto que la línea escalonada va a la derecha.
Segunda 'escalera' dibujada en Cubos.
L806A: LD B,$0C
L806C: PUSH DE
CALL L09E4
POP DE
DEC D
DJNZ L806C
LD DE,$1080
LD A,$05
CALL L804E
LD DE,$168C
LD A,$05
CALL L8027
NOP
NOP
NOP
JP L8095
Ahora borra la última línea vertical dibujada (12 pixeles). Luego dibuja dos escaleras internas, una yendo a la izquierda y otra yendo a la derecha.
Dos 'escaleras' más dibujadas en Cubos.
Continué dibujando la pirámide usando más código parcheado. Un síntoma de prueba constante y reajustes.
La pirámide empieza a tomar forma, y una vez dibujados varios escalones a la derecha, procedí a dibujar los escalones a la izquierda para completar la forma.
En este punto a la pirámide solo le falta una línea diagonal a cada lado de la parte inferior.
L8157: LD DE,$6A44
LD A,$01 ; Draw one step to the right.
CALL L804E
CALL L808A ; Erase last vertical line.
LD DE,$6ABC
LD A,$01
CALL L8027 ; Draw one step to the left.
CALL L808A ; Erase last vertical line.
Este código termina la pirámide dibujando escalones (una línea diagonal + una línea vertical) pero en cada uno borra la línea vertical para no dejar líneas colgando.
Después de este penoso código, el niño pensó: ¿Por qué no dibujar las letras del título con líneas?
Pone el color a $74 (7 - Turquesa, 4 - Fondo azul) cuando hace esto el VDP crea una línea con color que sobresale, y lo usé para un efecto seudo-3D.
Letras de título para Cubos.
Las líneas muestran el título "CUBOS".
LD A,(LFF0D)
CP $01
JP NZ,L8290
NOP
LD HL,$3800
LD DE,L8250
LD BC,$0060
CALL L0309
JP L8290
L8250: db $03,$05,$07,$03 ; $8250
db $01,$3F,$EF,$EF ; $8254
db $C7,$C7,$03,$0F ; $8258
db $0C,$0C,$0C,$3C ; $825C
db $80,$40,$C0,$80 ; $8260
db $00,$F8,$EE,$EE ; $8264
db $C6,$C6,$80,$E0 ; $8268
db $60,$60,$60,$78 ; $826C
L8270:
db $03,$0F,$1F,$30 ; $8270
db $37,$69,$6F,$6C ; $8274
db $6B,$37,$30,$DF ; $8278
db $CF,$63,$60,$90 ; $827C
db $C0,$F0,$F8,$0C ; $8280
db $EC,$96,$F6,$36 ; $8284
db $D6,$EC,$0C,$FB ; $8288
db $F3,$C6,$06,$09 ; $828C
Ahora si el nivel es 1, carga los sprites completos del juego en VRAM. Sin embargo, el contador en BC es para 3 sprites, y solo hay 2 sprites. ¡Pero no importa!
Ahora pone el modo de 16x16 pixeles para sprites, y pone la posición inicial para el jugador y los dos enemigos. Realiza una pequeña optimización al cargar el VDP, llamando L8918 para escribir en VRAM y también incrementar la dirección del VDP. Los dos enemigos tienen un color seudoaleatorio generado por el registro R del Z80 (el contador de refresco de DRAM). Interesante que aparentemente olvidé que ya había leído el registro R, y dentro de L8C1B copio de nuevo el valor de R en A, y extrae los 4 bits bajos para el color y si el color es 0 ó 1 (transparente o negro) lee de nuevo R hasta tener un color válido.
Cubos después de poner los sprites.
Movimiento del jugador
Ahora empezamos con el bucle principal, y lo primero que se hace es el movimiento del jugador.
Esta es una parte donde mi joven yo decepciona a mi viejo yo. Pude distinguir de inmediato los códigos ASCII para las letras T, Y, G y H, indicando que es el código de movimiento principal, y lo reemplacé con lectura del joystick (incluso en MSX que tiene un teclado), solo para ver que el juego terminaba de inmediato.
Después de un breve análisis, descubrí que el juego realiza unas pocas tareas y retorna a L82E0, así que en realidad procedía a una velocidad muy alta moviendo los enemigos sobre el jugador, y entonces agregué un gran retardo, y de esta forma vi que L02AA era una subrutina decodificadora de teclado que espera la tecla. Así que este juego Cubos es un juego basado en turnos, una vez que tecleas, la computadora responde.
Así que tuve que codificar una rutina lectora de joystick que se bloquea hasta que se hace un movimiento.
; Read keyboard
L02AA:
.0:
ld bc,$0800 ; Delay.
dec bc
ld a,b
or c
jr nz,$-3
xor a ; Read keyboard movement.
call GTSTCK
or a
jr z,.2
ld a,1 ; Read joystick 1 movement.
call GTSTCK
.2:
ld b,a
ld a,(debounce)
or a ; Is it debouncing?
jr z,.1 ; No, jump.
dec a ; Yes, countdown.
ld (debounce),a
ld b,$ff ; No movement accepted.
.1:
ld a,$00
dec b ; 1-Up?
jr nz,$+4
ld a,$59 ; ASCII Y
dec b
dec b ; 3-Right?
jr nz,$+4
ld a,$48 ; ASCII H
dec b
dec b ; 5-Down?
jr nz,$+4
ld a,$47 ; ASCII G
dec b
dec b ; 7-Left?
jr nz,$+4
ld a,$54 ; ASCII T
or a ; Any key pressed?
jr z,.0 ; No, jump and wait.
push af
ld a,15 ; Start debouncing delay.
ld (debounce),a
pop af
RET
Para cada una de las cuatro subrutinas de movimiento lee las coordenadas X,Y del sprite del jugador, las desplaza por el monto correcto de pixeles (una combinación de 17 pixeles a la derecha o izquierda, y 12 pixeles arriba o abajo), y guarda las nuevas coordenadas X,Y en la VRAM. Entonces salta a L8311 y lee las nuevas coordenadas X,Y, llama a L8345 para determinar si marca el nuevo cubo de la pirámide.
L8345: PUSH DE
CALL L09FC
POP DE
PUSH AF
CALL L09B9
POP AF
RET NZ
JP L8371
L8371: LD HL,LFF0F
INC (HL)
RET
Para determinar si debe llenar un nuevo cubo de la pirámide, primero llama L09FC para leer el pixel debajo de la coordenada D,E, y entonces llama a L09B9 para dibujar un pixel en la misma posición. Retorna si el pixel ya estaba marcado o de lo contrario salta a L8371 para incrementar el contador en LFF0F (probablemente un contador de cuantos cubos se han llenado).
L86A8: INC E
CALL L869C
INC E
CALL L869C
INC E
CALL L869C
INC E
CALL L869C
INC D
NOP
CALL L869C
DEC E
CALL L869C
DEC E
CALL L869C
DEC E
CALL L869C
INC D
DEC E
CALL L869C
INC E
CALL L869C
INC E
CALL L869C
INC E
CALL L869C
RET
L869C: LD A,$F1
LD (LEF13),A
PUSH DE
CALL L12DA
POP DE
RET
Dibuja algo similar a una huella en el cubo, pixel por pixel, y la subrutina L869C pone el color para la huella y dibuja un pixel coloreado.
Ahora para el gran momento vergonzoso: ¡Nunca verifica si el cuadro destino del jugador es uno válido! El jugador fácilmente puede caminar en el cielo y ganar sin ser molestado por los enemigos. ¡Huy!
Movimiento de los enemigos
Después de mover el jugador, salta a L82F7 para mover los enemigos:
En una demostración aterradora de código parcheado, realiza tres llamadas encadenadas hasta que alcanza L83A4 y si el jugador ha llenado los 15 cubos de la pirámide entonces ha ganado el nivel actual y salta a L85C1 (después se estudiará). De nuevo, un parche salta a L83EB y complementa el contenido de LFF0E, si es $ff entonces no mueve los enemigos en este turno (haciendo RET Z). Esto significa que el jugador puede moverse dos veces antes de que los enemigos se muevan. Entonces salta de nuevo a L83AF con HL puesto a $1b00 (atributos de sprites en VRAM para el jugador).
Obtiene la coordenada Y para el jugador y la coordenada Y para el enemigo 1 y si el enemigo está debajo del jugador, va arriba (SUB $11), de lo contrario va abajo (ADD A,$11). Entonces actualiza la coordenada Y del enemigo en L83BF, y salta a L83CA.
L83CA obtiene la coordenada X para el jugador y la coordenada X del enemigo 1 y si el enemigo está a la derecha del jugador, se va a la izquierda (SUB $0C), de lo contrario se va a la derecha (ADD A,$0C). Y actualiza la coordenada X del enemigo en L83DD, y salta a L8402.
El resultado de esto es que el enemigo se mueve en diagonal siguiendo al jugador.
Y entonces el código se repite embarazosamente para mover el segundo enemigo.
El final del código salta directamente en L0447 y usa una característica del apuntador de pila donde L0447 contiene la instrucción RET para volver al punto de llamada.
El tercer enemigo
El juego no retorna directamente al punto de llamada en L82F7, en lugar de eso salta a L88E0.
L88E0: LD A,(LFF10)
CP $FE
JP Z,L8926
LD C,$7F
CALL L891D
NOP
NOP
NOP
LD HL,$1B0C
LD A,$08
CALL L0447
INC HL
LD A,$78
CALL L0447
INC HL
LD A,$04
CALL L0447
INC HL
LD A,$0E
JP L890F
L890F: CALL L0447
LD A,$FE
LD (LFF10),A
RET
L891D: CALL L8333
CP $40
RET NC
POP IX
RET
L8333: LD A,R
CP C
RET C
SRA A
CP C
RET C
PUSH BC
LD B,A
L833D: DJNZ L833D
POP BC
JR L8333
Primero checa si la variable FF10 es $fe para saltar a L8926, pero como fue inicializada a cero, entonces pone C a $7f y llamada L891D y en un encadenamiento a L8333 para ver si el número aleatorio es mayor o igual que $40 para crear un tercer enemigo apareciendo el tope de la pirámide (la serpiente en Q*Bert), y si es menor de $40 se traga la dirección de retorno de la pila (POP IX) y retorna sin hacer nada. Esto significa que el tercer enemigo tiene un chance de aparición en el tope de la pirámide de 50% por cada movimiento del jugador.
Como el valor del registro R es comparado contra C (que contiene $7f), casi siempre retorna sin cambios en L8333 con RET C debido a que R es $00-$7f, y el resto del código solo se usa si R es $7f en una sorprendente falta de conocimiento de mi lado de como R funciona.
El enemigo es creado en el tope de la pirámide con el mismo sprite de enemigo (una oportunidad perdida de mostrar otro tipo de enemigo) y color gris fijo, también encadena a L890F para marcar el enemigo como inicializado (poniendo LFF10 a $fe).
Movimiento del tercer enemigo
Si el tercer enemigo está presente (LFF10 contiene $fe), el código salta a L8926.
L8926: LD HL,$1B0C
CALL L0454
ADD A,$11
CALL L8971
LD C,$7F
CALL L8333
CP $3F
JR C,L8945
LD HL,$1B0D
CALL L0454
SUB $0C
JP L894D
L8945: LD HL,$1B0D
CALL L0454
ADD A,$0C
L894D: CALL L0447
LD HL,$1B00
CALL L0454
LD B,A
LD HL,$1B0C
CALL L0454
CP B
RET NZ
LD HL,$1B01
CALL L0454
LD B,A
LD HL,$1B0D
CALL L0454
CP B
RET NZ
JP L8480
L8971: CALL L0447
CP $5D
RET NZ
LD A,$D1
CALL L0447
LD A,$00
LD (LFF10),A
POP IX
RET
La coordenada Y para el tercer enemigo se lee de VRAM $1b0c y se le agrega $11 para hacerlo avanzar verticalmente, y si alcanza $5d (comparación en L8971), se le hace desaparecer poniendo su coordenada Y en $d1 (fuera del rango visual), poniendo LFF10 a $00, y se traga la dirección de retorno (POP IX) y vuelve a donde se le llamó.
Si no ha alcanzado $5d, entonces genera un número aleatorio y si es menor que $3f entonces agrega $0c a la coordenada X, de lo contrario sustrae $0c a la coordenada X, haciendolo oscilar horizontalmente sobre la pirámide.
En L894D hace una comparación de la coordenada Y del jugador contra la coordenada Y del enemigo, y si ambas son iguales entonces hace la comparación de coordenadas X, y en caso de colisión salta a L8480.
Colisión del enemigo
Después de completar los enemigos y volver al punto de llamada en L82F7, salta a L843E.
El código luce como detección de colisión con enemigos.
L843E: LD HL,$1B00
CALL L0454
LD B,A
LD HL,$1B04
CALL L0454
CP B
CALL Z,L845C
LD HL,$1B08
CALL L0454
CP B
CALL Z,L846E
JP L82E0
L845C: LD HL,$1B01
CALL L0454
LD C,A
LD HL,$1B05
CALL L0454
CP C
JP Z,L8480
RET
L846E: LD HL,$1B01
CALL L0454
LD C,A
LD HL,$1B09
CALL L0454
CP C
JP Z,L8480
RET
Lee la coordenada Y del jugador de la VRAM y hace una comparación contra la coordenada Y de los enemigos, si alguno de estas es igual entonces llama a una subrutina extra en L845C o L846E para hacer una comparación de la coordenada X, y si alguna de estas es igual entonces salta a L8480 (jugador derrotado).
Jugador derrotado
Cuando algún enemigo toca al jugador, el juego salta a L8480.
Dependiendo en el número de nivel, selecciona el sprite para los siguientes enemigos que serán ilustrados, reinicia el número de cubos llenados y el turno de enemigos. Cuando el sprite se cambia ocurre un bug que se puede ver brevemente cuando los enemigos actuales se convierten en los nuevos enemigos.
En un obvio pensamiento posterior, salta a L8AA9 para mostrar otro mensaje vectorial:
El jugador termina un nivel en Cubos. Se puede ver el bug de que los enemigos del siguiente nivel son redefinidos demasiado pronto.
El mensaje es GANAS! y hace otro enorme retardo en L8B88, y salta atrás para volver a mostrar la pirámide.
Sin embargo, para el último nivel nunca muestra el mensaje GANAS! porque salta directo a L8690 que reinicia el nivel 1 y salta al inicio del juego (¿porqué hice eso?).
Variables en memoria
Las variables de memoria utilizadas para este juego son admirablemente escasas.
ORG RAM_BASE
LEF13: RB 1 ; Color para dibujar pixeles.
LFF0D: RB 1 ; Número de nivel.
LFF0E: RB 1 ; Turno de los enemigos.
LFF0F: RB 1 ; Número de cubos llenos.
LFF10: RB 1 ; Estatus del tercer enemigo.
; ($00- No creado, $fe- Creado)
debounce: rb 1
STACK: EQU RAM_BASE+1024
Las coordenadas del jugador y los enemigos son preservadas en VRAM.
Interludio
Es bastante claro que todavía no hacía planes para escribir un juego, asi que los parches se acumulaban haciendo muy difícil seguir el código.
Muchas subrutinas largas pudieron ser usadas para simplificar el juego. Tener las coordenadas del jugador y enemigos en RAM hubiera simplificado el juego, y permitido hacer un juego en tiempo real con el característico salto del jugador y los enemigos.
La falta de detección de frontera para el jugador fue un error trágico que pudo ser evitado teniendo una tabla para cada posible coordenada de cubo en la pirámide, sin embargo, estoy muy seguro de que el código parcheado jugó en mi contra haciendo cada vez más difícil ver donde tenía que parchear el código. La solución obvia hubiera sido que cada tecla cargara un registro con el desplazamiento requerido, y una sola rutina estaría a cargo de mover el jugador y ver si la posición destino es válida.
El dibujo vectorial de líneas para las letras pudo beneficiarse de una tabla de origen-destino-destino salvando un montón de llamadas a la subrutina de dibujo de lineas.
Fue también una oportunidad desperdiciada no tener un cuadro especial para un enemigo cayendo de arriba, o diferentes cuadros para las direcciones del jugador, o al menos un cuadro diferente cuando el jugador es tocado por los enemigos. Al menos esta vez todos mis gráficos fueron originales.
Tampoco hay efectos de sonido, probablemente debido a que el juego se detiene cada vez a esperar por un movimiento.
Este iba a ser el epílogo, sin embargo, no estaba satisfecho simplemente con mostrar mi viejo juego defectuoso. ¡Necesitaba corregirlo! Así que aquí vamos.
Dibujando la pirámide
En lugar de dibujar la pirámide usando el código tipo escaleras, preferí usar una subrutina para dibujar un cubo seudo-3D para dibujar cada uno de los quince cubos en la pirámide.
;
; Draw a row of cubes.
;
.2:
push bc
push de
call draw_cube
pop de
ld a,e
add a,$18 ; Move X-coordinate.
ld e,a
pop bc
djnz .2
pop de
ld a,e
sub $0c ; Half to the left.
ld e,a
ld a,d
add a,$11 ; Next row.
ld d,a
pop bc
inc b ; Increase number of cubes.
ld a,b
cp 6 ; Reached six cubes?
jp nz,.1 ; No, continue drawing.
El primer paso es dibujar la pirámide en líneas. La primera línea contiene un cubo, la segunda contiene dos cubos, e iterativamente hasta que dibuja cinco cubos en la quinta línea. El código para dibujar un cubo seudo-3D es este:
draw_cube:
push de ; Drawing schematic:
call draw_left ; 1 1/ \5
push de ; / \
call draw_vertical ; 2 2|\4 /|6
call draw_right ; 3 | \ /8|
pop de ; 3\ I9/7
call draw_right ; 4 \I/
pop de
call draw_right ; 5
push de
call draw_vertical ; 6
call draw_left ; 7
pop de
call draw_left ; 8
jp draw_vertical ; 9
;
; Draw a diagonal line to the left.
;
draw_left:
ld b,$06
.1: call draw_pixel
dec e
call draw_pixel
dec e
inc d
djnz .1
ret
;
; Draw a diagonal line to the right.
;
draw_right:
ld b,$06
.1: call draw_pixel
inc e
call draw_pixel
inc e
inc d
djnz .1
ret
;
; Draw a vertical line.
;
draw_vertical:
ld b,$0b
.1: call draw_pixel
inc d
djnz .1
ret
draw_pixel:
push de
call L09B9
pop de
ret
Sólo con este cambio ahorramos 243 bytes.
El siguiente cambio fue definir el sprite de enemigo en el comienzo del nivel y poner juntos en memoria todos los bitmaps de enemigos, así que cargar el enemigo en la VRAM es un asunto de multiplicar el nivel por 32 y añadir un offset, también agregué un nuevo dibujo de sprite para el enemigo del tope (¿adivina cuál? ¡una víbora! El sprite viene de Viboritas). Esto permitió también eliminar el bug de no mostrar el mensaje ganador en el último nivel. Incluso cuando agregamos un nuevo sprite, el tamaño del juego se redujo debido a la eliminación de código ineficiente.
Codifiqué una lista de posiciones válidas en la pirámide (los quince cubos completos), y reemplacé el código de movimiento con una selección de offsets para moverse, entonces lee las coordenadas actuales del jugador, las desplaza por el offset, y verifica si es un movimiento válido. Si el movimiento es válido entonces actualiza la posición del jugador, y verifica si debe dibujar la huella. Si el movimiento no es válido entonces el jugador no se mueve.
L82E0:
call L02AA ; Read the keyboard.
cp $54
jr nz,$+5
ld de,$eff4 ; y = -17, x = -12
cp $59
jr nz,$+5
ld de,$ef0c ; y = -17, x = +12
cp $47
jr nz,$+5
ld de,$11f4 ; y = +17, x = -12
cp $48
jr nz,$+5
ld de,$110c ; y = +17, x = +12
ld hl,$1b00
call L0454 ; Read Y-coordinate of the player.
add a,d ; Add Y offset.
ld d,a
inc hl
call L0454 ; Read X-coordinate of the player.
add a,e ; Add X offset.
ld e,a
ld hl,valid_positions
ld b,15
.1:
ld a,(hl)
cp d
inc hl
jr nz,.2
ld a,(hl)
cp e
jr z,.3
.2:
inc hl
djnz .1
jp L82F7 ; Invalid movement.
; Valid movement.
.3:
ld hl,$1b00
ld a,d
call L0447 ; Update Y-coordinate of the player.
inc hl
ld a,e
call L0447 ; Update X-coordinate of the player.
ld a,d
add a,$0e
ld d,a
ld a,e
add a,$06
ld e,a
push de
call L8345 ; Check if it filled another pyramid square.
pop de
call L86A8 ; Draw footprint.
L82F7:
CALL L832A
jp L843E
valid_positions:
db $08,$78
db $19,$6c,$19,$84
db $2a,$60,$2a,$78,$2a,$90
db $3b,$54,$3b,$6c,$3b,$84,$3b,$9c
db $4c,$48,$4c,$60,$4c,$78,$4c,$90,$4c,$a8
Cada uno de los cuatro movimientos posibles se pone como dos offsets en el registro DE, y añadido a la posición X,Y del jugador. Ahora hace una comparación contra todas las posiciones válidas sobre la pirámide (la tabla es valid_positions). Si el movimiento es inválido simplemente salta a mover los enemigos, de lo contrario actualiza las coordenadas X,Y del jugador.
Un excelente premio por hacer este cambio es que se eliminó el confuso código replicado para leer las coordenadas X,Y del jugador en cada movimiento ¡También ahorró un montón de bytes!
Finalmente, nunca me gustó la cara que dibuje para las esquinas, así que hice otra y reemplacé completamente el código de dibujo con una sola subrutina para copiar un rectángulo (usada cuatro veces).
;
; Draw faces.
;
ld hl,face_bitmaps
ld de,$0000
call copy_rectangle
ld hl,face_bitmaps
ld de,$00e0
call copy_rectangle
ld hl,face_colors
ld de,$2000
call copy_rectangle
ld hl,face_colors
ld de,$20e0
call copy_rectangle
copy_rectangle:
ld b,4
.1: push bc
push de
ex de,hl
ld b,32
.2: ld a,(de)
call L0447
inc hl
inc de
djnz .2
ex de,hl
pop de
inc d
pop bc
djnz .1
ret
;
; Bitmaps and color for the face.
;
face_bitmaps:
db $ff,$ff,$fc,$fe,$ff,$f9,$fd,$fe
db $f1,$a4,$a9,$52,$54,$55,$3f,$7f
db $2d,$09,$22,$00,$92,$20,$ff,$fc
db $ff,$3f,$2f,$8f,$1f,$4f,$1f,$2f
db $f9,$fc,$fe,$f9,$fa,$f8,$fa,$fa
db $3f,$ff,$3f,$bf,$c7,$81,$1f,$c1
db $ff,$fc,$ff,$ff,$e3,$81,$fc,$83
db $8f,$0f,$af,$1f,$5f,$1f,$5f,$5f
db $fa,$fa,$fc,$fe,$fe,$fe,$fe,$ff
db $b3,$ff,$fe,$fe,$fd,$fd,$fe,$7f
db $e5,$ff,$ff,$ff,$ff,$7f,$ff,$fe
db $5f,$5f,$3f,$7f,$7f,$7f,$7f,$ff
db $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff
db $7c,$7c,$bf,$df,$ef,$f7,$f8,$f8
db $3e,$3e,$fc,$f8,$f0,$e0,$ff,$ff
db $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff
face_colors:
db $a1,$a1,$a1,$a1,$a1,$a1,$a1,$a1
db $a1,$a1,$a1,$a1,$a1,$a1,$b1,$b1
db $a1,$a1,$a1,$f1,$a1,$a1,$91,$91
db $a1,$a1,$a1,$a1,$a1,$a1,$a1,$a1
db $a1,$a1,$a1,$a1,$a1,$a1,$a1,$a1
db $b1,$b1,$b1,$b1,$b1,$b1,$b1,$b1
db $91,$91,$91,$91,$91,$91,$91,$91
db $a1,$a1,$a1,$a1,$a1,$a1,$a1,$a1
db $a1,$a1,$a1,$a1,$a1,$a1,$a1,$a1
db $b1,$b1,$b1,$b1,$b1,$b1,$b1,$b1
db $91,$91,$91,$91,$91,$91,$91,$91
db $a1,$a1,$a1,$a1,$a1,$a1,$a1,$a1
db $a1,$a1,$a1,$a1,$a1,$a1,$a1,$a1
db $b1,$b1,$b1,$b1,$b1,$b1,$b1,$b9
db $91,$91,$98,$98,$98,$98,$81,$81
db $a1,$a1,$a1,$a1,$a1,$a1,$a1,$a1
Tambien removí unos pocos saltos encadenados y la versión actualizada del juego es 547 bytes más corta y más fácil de entender.
Cubos actualizado y corregido (v2).
Epílogo
Mirar el código que escribí en mis primeros años me dio un gran panorama de como solucionaba los problemas de diseño.
Mi entusiasmo juvenil me mantenía enfocado en un problema, y si no podía encontrar una solución, simplemente seguía adelante codificando otras partes del juego.
Pero este mismo entusiasmo evitó que usara un poco de tiempo para hacer un plan del juego. Esta sola cosa pudo salvarme horas de errores en desarrollo, y me hubiera ayudado a hacer juegos más profesionales más pronto.
Veo ahora que al venir del lenguaje BASIC, codificaba como lo hacía en el lenguaje BASIC: Agregaba cosas hasta que funcionaba, excepto que en BASIC puedes insertar líneas de´código e incluso renumerar las líneas de código. Podemos ver BASIC como un lapiz, puedes borrar para insertar cosas o reescribirlas, pero el código máquina es como una pluma, no puedes corregir fácilmente tus errores.
Descargas
cubos.zip (34.3 kb), incluye el binario original, primer desensamblaje, primer port y segundo port mejorado.