Cubos: A 3K game made in 1991

Cubos updated gameplay
As I was happily writing the article for Viboritas, I noticed a game called Cubos sitting along the same disk but I couldn't remember what was it about.
A few days later when I added the missing drawings for Karateka, I noticed I had a full draft of the screen for Cubos. And again, I started remembering things and fixing bugs from more than 30 years ago.

Televideo

The year was 1986, and my father rebuilt a Televideo PC from parts. We had a few floppy disks, and one of the games was J-Bird by Greg Kuperberg. It displayed a bird jumping on a pyramid built of cubes, and you need to fill every cube. In advanced levels, you were required to jump several times over the same cube to get the right color while avoiding balls and snakes.
Of course, I'm describing Q*Bert, but at the time I didn't know it, and it was a novel concept for me. It took me years to discover that J-Bird was a clone of the arcade.
I played a lot of J-Bird and other early PC games that year. I remember being mesmerized by the pseudo-3D look but I had no idea how to get that effect on my games.
However, already I had drawn the following idea for my own cube-jumping game:
Early draft of Cubos
Early draft of Cubos on my notebook.
Early draft of player sprite for Cubos
Early draft of player sprite for Cubos.
You can see my kid's mind hasn't yet figured that the pyramid could be easily constrained to the borders of the squares (i.e. the character grid on the screen), and doing that I could have figured that the whole pyramid could be drawn using predefined characters. My idea was that the cassette man was fighting again the evil disks, and the snake would be a joystick.
J-Bird was too advanced for my knowledge at the time, so the idea stood there for some years until early 1991 when I was age 12.

The game

The binary comes from a disk I used while I worked on one of the homebrew computers built by my father. The disk per se doesn't have an operating system, instead, it showed up a menu to choose games, and it commanded the disk subroutines to retrieve a track of the disk and load it at the address $8000.
I worked directly in machine code using a monitor program, so one of the first steps is disassembling the full game. In order to make it to work on a MSX or Colecovision we'll reuse the translation layer I made for my first Z80 game.

        ;
        ; 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
The disassembled game goes up to 1292 assembler lines. Each label is named after its original address in the game. We'll need to follow the code along several labels because my heavily patched style of programming at the time.
The game uses only 4 bytes of RAM to keep work data, because I took a terrible design decision of keeping the X,Y coordinates of the player and enemies in the VRAM and this takes a lot more code than having these directly in normal RAM.
So here we go with the first piece of code:

L8000:      	CALL L836A
L8003:      	CALL L0099
            	LD HL,$2000
            	CALL L0100
L800C:      	LD A,$F1
            	OUT ($98),A
            	INC HL
            	LD A,H
            	CP $38
            	JR NZ,L800C
            	LD HL,$4701
            	CALL L86E0
            	LD DE,$1080
            	LD A,$05
            	CALL L8027
            	JP L8043

L836A:      	XOR A
            	CALL L83FB
            	JP L8696

L83FB:      	LD (LFF0F),A
            	CALL L85B9
            	RET

L85B9:      	LD (LFF0E),A
            	INC A
            	LD (LFF0D),A
            	RET

L8696:      	CALL L0099
            	JP L0AE8
The call to L836A appears to initialize three variables, two of these to zero, and the other to one, then it ends by calling L0099 and L0AE8, fortunately, I remember these were used to set the high-resolution video mode. Well, at least L0099, because I don't remember the function of L0AE8.
For some reason, I didn't notice that L8003 again calls L0099 to setup the VDP, and it proceeds to clean the color space with white and black. My love for MSX is shown here using the port $98 to write data to the VDP, but instead, it was $90 in my father's computer, and for this game port I'm going to replace this with the label VDP to be able to assemble this game both for Colecovision and MSX.
Next, it puts the border as black color by calling L0100 at 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
It draws a series of eighteen characters on the screen taking a VRAM address and the 8 bytes of data. For some reason, I decided to keep the target VRAM address in big-endian order instead of the more common little-endian order used in Z80.
This draws a bitmap face on the top left of the screen. I think this is something I added after getting the game to work because of the memory address.
Face bitmap drawn by Cubos
Face bitmap drawn by Cubos.
It then repeats the same code to draw the same face on the right side of the screen.

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
I could have saved more than 200 bytes just by making it a subroutine and keeping an offset to repeat the drawing. I think I wanted to put a different face on the right of the screen, but I never made it.

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
Now it proceeds to put color to the face. It looks like finally I figured out how to go over a rectangle of the screen as you can see LD B,$05 to draw five rows, then LD B,$04 to draw four columns, and finally LD B,$08 to repeat the color for a whole 8x8 character.
Any experienced Z80 coders will cringe looking at the whole five assembler lines that could have been replaced with INC H / LD L,0.
Face bitmap with color drawn by Cubos
Face bitmap with color drawn by Cubos.
Then I repeated almost the same code to give color to the face at the right:

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
At least this time I reused the color data at L88A9. The linked code continues:

L8984:      	LD A,$F1
            	LD (LEF13),A
            	LD HL,$4010
            	LD DE,$4020
            	CALL L1500
            	LD HL,$4010
            	LD DE,$5010
            	CALL L1500
            	LD HL,$5010
            	LD DE,$5020
            	CALL L1500
            	LD HL,$4020
            	LD DE,$5020
            	CALL L1500
            	LD HL,$5810
            	LD DE,$5820
            	CALL L1500
            	LD HL,$5810
            	LD DE,$6810
            	CALL L1500
            	LD HL,$6810
            	LD DE,$6820
            	CALL L1500
            	LD HL,$6820
            	LD DE,$5820
            	CALL L1500
            	LD HL,$4028
            	LD DE,$4038
            	CALL L1500
            	LD HL,$4038
            	LD DE,$5038
            	CALL L1500
            	LD HL,$5028
            	LD DE,$5038
            	CALL L1500
            	LD HL,$4028
            	LD DE,$5028
            	CALL L1500
            	LD HL,$5828
            	LD DE,$5838
            	CALL L1500
            	LD HL,$5838
            	LD DE,$6838
            	CALL L1500
            	LD HL,$6838
            	LD DE,$6828
            	CALL L1500
            	LD HL,$5828
            	LD DE,$6828
            	CALL L1500
            	LD HL,$5020
            	LD DE,$5828
            	CALL L1500
            	LD HL,$5028
            	LD DE,$5820
            	CALL L1500
            	LD HL,$4414
            	LD DE,$441C
            	CALL L1500
            	LD HL,$4418
            	LD DE,$4C18
            	CALL L1500
            	LD HL,$442C
            	LD DE,$4830
            	CALL L1500
            	LD HL,$4434
            	LD DE,$4830
            	CALL L1500
            	LD HL,$4830
            	LD DE,$4C30
            	CALL L1500
            	LD HL,$5C2C
            	LD DE,$642C
            	CALL L1500
            	LD HL,$5C34
            	LD DE,$6434
            	CALL L1500
            	LD HL,$602C
            	LD DE,$6034
            	CALL L1500
            	LD HL,$5C14
            	LD DE,$5C1C
            	CALL L1500
            	LD HL,$5C14
            	LD DE,$6414
            	CALL L1500
            	LD HL,$6414
            	LD DE,$641C
            	CALL L1500
            	LD HL,$641C
            	LD DE,$611C
            	CALL L1500
            	LD HL,$6118
            	LD DE,$611C
            	CALL L1500
            	LD HL,$5C1C
            	LD DE,$5D1C
            	JP L8B97
Because of the value $f1, I can distinguish that LEF13 contains a color code (white + black). The values in HL and DE look strange, nothing like VRAM addresses, or RAM addresses, but we have repeated values, and of course, this is calling a line drawing subroutine in ROM (H=y1, L=x1, D=y2, E=x2)
I remember vaguely that it was in early 1991 when finally I had a line-drawing subroutine in the ROM of my father's computer.
It draws four squares and connects these with diagonal lines. On the inside, it draws letters to represent the keys T, Y, G, and H. A little visual manual.
Key map drawn by Cubos
Key map drawn by Cubos.
Just a tiny caveat: I don't have the ROM for this computer, so for this game I had to code yet-another Bresenham line algorithm in 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
The code ended with the last line drawn, but somehow I decided to insert yet more code. This time to show the enemy for level 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
The register HL points to the coordinate to draw the enemy, and the register DE points to the bitmap for the enemy. The enemy bitmap is designed for a sprite, so the subroutine L8BA6 reads the sprite as four separate characters to display (you can see the four separate LD B,$08).
I think I wanted the first enemy to look like a lion, but instead, it looked like a monster with a coat... a big coat.
When writing the graphic byte it also sets the color to dark green using the L8C06 subroutine. This subroutine loads the color into the A register ($c1 for dark green plus black background) and then does a crazy operation... to get the same value.

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
Surely I thought that it would be nice to show all the enemies that can appear, so I repeated the calls to L8BA6 with the pointers to the graphics of the other enemies.
Enemies appearing in Cubos
Enemies appearing in Cubos.
The enemy for the second level is a chef (probably my homage to Burgertime), the third one is the Z80 chip, the fourth one is an executioner (something related to the guillotine), and the fifth one is a star, maybe a symbol of the omega, the end of everything, or more clearly I was lacking ideas for further enemies.
After all this epic chain patched code, it returns to where it is called L86E0, and sets DE with $1080 and A with $05 and calls 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
This subroutine draws 5 steps going down and to the left at the coordinate given in DE (D is the Y-coordinate, E is the X-coordinate). Each pixel is drawn using L09B9. The line going to left is drawn as a sloped line with two pixels each time.
I can remember vividly when I coded the pixel drawing subroutine, I cannot remember the exact details, but for this article I coded a pixel coordinate calculator (coor2vdp), a write pixel subroutine, an erase pixel subroutine, and a read pixel subroutine.

	;
	; 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
First 'stair' drawn in Cubos
First 'stair' drawn in Cubos.
The code continues at L8043 and does a call to a similar subroutine.

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
The subroutine L804E does exactly the same thing as L8027, except the sloped line is drawn going to the right.
Second 'stair' drawn in Cubos
Second 'stair' drawn in 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
Now it erases the last vertical line drawn (12 pixels). Then draws two internal ladder steps, one going to the left, and one going to the right.
Further two 'stairs' drawn in Cubos
Further two 'stairs' drawn in Cubos.
I continue drawing the pyramid using more patched code. A symptom of retesting and rearrangement.

L8095:      	LD DE,$2274
            	LD A,$05
            	CALL L8151
            	LD DE,$2868
            	LD A,$04
            	CALL L804E
            	CALL L808A
            	LD DE,$3468
            	LD A,$04
            	CALL L804E
            	LD DE,$3A5C
            	LD A,$03
            	CALL L804E
            	CALL L808A
            	LD DE,$465C
            	LD A,$03
            	CALL L804E
            	CALL L808A
            	LD DE,$4C50
            	LD A,$02
            	CALL L804E
            	CALL L808A
            	LD DE,$5850
            	LD A,$02
            	CALL L804E
            	CALL L808A
            	LD DE,$5E44
            	LD A,$01
            	CALL L804E
            	NOP
            	NOP
            	NOP
            	LD DE,$228C
            	LD A,$05
            	CALL L8027
            	CALL L808A
            	LD DE,$2898
            	LD A,$04
            	CALL L8027
            	JP L8115

L8151:      	CALL L804E
            	JP L808A
Current state of the pyramid drawing in Cubos
Current state of the pyramid drawing in Cubos.
The pyramid starts to take shape, and once I have drawn several steps to the right, I proceed to draw steps to the left to complete the shape.

L8115:      	LD DE,$3498
            	LD A,$04
            	CALL L8027
            	CALL L808A
            	LD DE,$3AA4
            	LD A,$03
            	CALL L8027
            	LD DE,$46A4
            	LD A,$03
            	CALL L8027
            	CALL L808A
            	LD DE,$4CB0
            	LD A,$02
            	CALL L8027
            	LD DE,$58B0
            	LD A,$02
            	CALL L8027
            	CALL L808A
            	LD DE,$5EBC
            	LD A,$01
            	CALL L8027
            	JP L8157
At this point, the pyramid is only missing one diagonal line on each bottom side.

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.
This code finishes the pyramid, and it draws steps (one diagonal line + one vertical line) but on each one erases the vertical line to not leave dangling lines.
After this embarrassing code, the kid thought: Why not do the title letters with lines?

            	LD A,$74
            	LD (LEF13),A
            	LD DE,$A840
            	LD HL,$B840
            	CALL L1500
            	LD DE,$A840
            	LD HL,$A850
            	CALL L1500
            	LD DE,$B840
            	LD HL,$B850
            	CALL L1500
            	LD DE,$B058
            	LD HL,$B858
            	CALL L1500
            	LD DE,$B858
            	LD HL,$B868
            	CALL L1500
            	LD DE,$B068
            	LD DE,$B868
            	CALL L1500
            	LD HL,$B070
            	LD DE,$B07F
            	CALL L1500
            	LD HL,$B470
            	LD DE,$B47F
            	CALL L1500
            	LD HL,$B870
            	LD DE,$B87F
            	CALL L1500
            	LD HL,$B070
            	LD DE,$B870
            	CALL L1500
            	LD HL,$B180
            	LD DE,$B380
            	CALL L1500
            	LD HL,$B580
            	LD DE,$B780
            	CALL L1500
            	LD DE,$B068
            	LD HL,$B868
            	CALL L1500
            	LD HL,$B088
            	LD DE,$B098
            	CALL L1500
            	LD DE,$B098
            	LD HL,$B898
            	CALL L1500
            	LD DE,$B898
            	LD HL,$B888
            	CALL L1500
            	LD HL,$B888
            	LD DE,$B088
            	CALL L1500
            	LD DE,$B0A0
            	LD HL,$B0B0
            	CALL L1500
            	LD DE,$B0A0
            	LD HL,$B4A0
            	CALL L1500
            	LD HL,$B4A0
            	LD DE,$B4B0
            	CALL L1500
            	LD HL,$B4B0
            	LD DE,$B8B0
            	CALL L1500
            	LD HL,$B8A0
            	LD DE,$B8B0
            	CALL L1500
It sets the color to $74 (7 - Cyan foreground, 4 - Blue background) when doing this on the VDP it creates a line bleeding color, but I used it for a pseudo 3D effect.
Title letters for Cubos
Title letters for Cubos.
The lines show the title "CUBOS" (Spanish for cubes).

            	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
Now if the level is 1, it loads the full game sprites bitmaps into VRAM. However, the BC counter is for 3 sprites, and there is only data for 2 sprites. Never mind!

L8290:      	LD HL,$41C2
            	CALL L0100
            	LD HL,$1B00
            	LD A,$08
            	CALL L8918
            	LD A,$78
            	CALL L8918
            	LD A,$00
            	CALL L8918
            	LD A,$06
            	CALL L8918
            	LD A,$4C
            	CALL L8918
            	LD A,$00
            	LD (LFF10),A
            	LD A,$48
            	CALL L8918
            	LD A,$04
            	CALL L8918
            	LD A,R
            	CALL L8C1B
            	LD A,$4C
            	CALL L8918
            	LD A,$A8
            	CALL L8918
            	LD A,$04
            	CALL L8918
            	LD A,R
            	CALL L8C1B
            	LD A,$D1
            	CALL L8918
            	NOP

L8918:      	CALL L0447
            	INC HL
            	RET

L8C1B:      	LD A,R
            	AND $0F
            	CP $01
            	JP Z,L8C1B
            	CP $00
            	JP Z,L8C1B
            	JP L8918
Now it sets the 16x16 pixels mode for sprites, and it puts the initial position for the player and two enemies. It does a small optimization to setup the VDP, calling L8918 to write to VRAM and also to increment the VDP address. The two enemies have a pseudorandom color generated from the R register of the Z80 (the DRAM refresh counter). Very interestingly, I apparently forgotten that I already read the R register, and inside L8C1B it copies against the value of R into A, and it extracts the lower 4-bit for the color; if the color is 0 or 1 (transparent or black) it reads again R until it gets a valid color.
Cubos after sprites are set up
Cubos after sprites are set up.

Player movement

We enter now the main loop, and the first thing handled is the player's movement.

L82E0:      	CALL L02AA
            	CP $54
            	JP Z,L838D
            	CP $59
            	JP Z,L8353
            	CP $47
            	JP Z,L82FD
            	CP $48
            	JP Z,L8376
This is a part where my young self disappointed my oldest self. I could distinguish immediately the ASCII codes for the letters T, Y, G, and H, meaning it is the main movement code, and replaced it with joystick reading (even in MSX where a keyboard exists), only to see that the game finished almost immediately.
After a brief analysis, I discovered the game does a few things and goes back to L82E0, so it played at a very high speed moving the enemies over the player, then I added a big delay, and this way I found that L02AA was a keyboard decoding subroutine that waits for the key. So this Cubos game is a turn-based game. Once you type a key, the computer answers back.
So I had to code this joystick reading routine that blocks until a movement is done.

        ; 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
Now the code for the respective movements:

L838D:      	LD HL,$1B00
            	CALL L0454
            	SUB $11
            	CALL L0447
            	INC HL
            	CALL L0454
            	SUB $0C
            	CALL L0447
            	JP L8311

L8353:      	LD HL,$1B00
            	CALL L0454
            	SUB $11
            	CALL L0447
            	INC HL
            	CALL L0454
            	ADD A,$0C
            	CALL L0447
            	JP L8311

L8376:      	LD HL,$1B00
            	CALL L0454
            	ADD A,$11
            	CALL L0447
            	INC HL
            	CALL L0454
            	ADD A,$0C
            	CALL L0447
            	JP L8311

L82FD:      	LD HL,$1B00
            	CALL L0454
            	ADD A,$11
            	CALL L0447
            	INC HL
            	CALL L0454
            	SUB $0C
            	CALL L0447
L8311:      	DEC HL
            	CALL L0454
            	ADD A,$0E
            	LD D,A
            	INC HL
            	CALL L0454
            	ADD A,$08
            	LD E,A
            	PUSH DE
            	CALL L8345
            	POP DE
            	CALL L86A8
            	JP L82F7
Each of the four movement subroutines reads the player sprite X,Y coordinates, displaces these by the right amount of pixels (a combination of 17 pixels to the left or right, and 12 pixels upward or downward), and saves the new X,Y coordinates in the VRAM. It jumps to L8311 and reads the new X,Y coordinates, calling L8345 to determine if a new pyramid square is filled.

L8345:      	PUSH DE
            	CALL L09FC
            	POP DE
            	PUSH AF
            	CALL L09B9
            	POP AF
            	RET NZ
            	JP L8371

L8371:          LD HL,LFF0F
            	INC (HL)
            	RET
To determine if a new pyramid cube is filled, it calls L09FC to read the pixel under the D,E coordinate, and then it calls L09B9 to draw a pixel on the same position. It returns if the pixel was already filled, else it jumps to L8371 and increases a counter located at LFF0F (probably counting how many cubes are filled).

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
It draws something similar to a footprint in the cube, pixel by pixel, and the L869C subroutine sets the color for the footprint and draws a colored pixel.
Now for the great embarrassing moment: It never checks if the target square of the player is a valid one! The player can easily walk over the sky and win without being bothered by the enemies. Sigh!

Enemy movement

After moving the player, it jumps to L82F7 to move the enemies:

L82F7:      	CALL L890A
            	JP L843E

L890A:      	CALL L832A
            	LD A,E
            	RET

L832A:      	CALL L83A4
            	LD E,A
            	NOP
            	NOP
            	JP L88E0

L83A4:      	LD A,(LFF0F)
            	CP $0F
            	JP Z,L85C1
            	JP L83EB

L83EB:      	LD A,(LFF0E)
            	CPL
            	LD (LFF0E),A
            	CP $FF
            	RET Z
            	LD HL,$1B00
            	JP L83AF

L83AF:      	CALL L0454
            	LD B,A
            	LD HL,$1B04
            	CALL L0454
            	CP B
            	JP C,L83C5
            	SUB $11
L83BF:      	CALL L0447
            	JP L83CA

L83C5:      	ADD A,$11
            	JP L83BF

L83CA:      	LD HL,$1B01
            	CALL L0454
            	LD B,A
            	LD HL,$1B05
            	CALL L0454
            	CP B
            	JP C,L83E3
            	SUB $0C
L83DD:      	CALL L0447
            	JP L83E8

L83E3:      	ADD A,$0C
            	JP L83DD

L83E8:      	JP L8402
In a terrifying demonstration of patched code, it does three chain-linked calls until it reaches L83A4 and if the player has filled 15 cubes of the pyramid then it has won the current level and jumps to L85C1 (later to be seen). Again, a patch jumps to L83EB and complements the contents of LFF0E, if it is $ff then it doesn't move the enemies in this turn (doing RET Z). This means the player can move two times before the enemies move. It jumps back to L83AF with HL set to $1b00 (VRAM sprite attribute data for player)
It gets the Y-coordinate for the player and the Y-coordinate for the enemy 1, and if the enemy is below the player, it goes upwards (SUB $11), else it goes downwards (ADD A,$11). It updates the enemy Y-coordinate in L83BF, and then jumps to L83CA.
L83CA gets the X-coordinate for the player and the X-coordinate for the enemy 1, and if the enemy is at the right of the player, it goes to the left (SUB $0C), else it goes to the right (ADD A,$0C). It updates the enemy X-coordinate in L83DD, and jumps to L8402.
The final result is that the code moves the enemy in a diagonal following the player.
And then the code is repeated shamelessly to move the second enemy.

L8402:      	LD HL,$1B00
            	CALL L0454
            	LD B,A
            	LD HL,$1B08
            	CALL L0454
            	CP B
            	JP C,L841B
            	SUB $11
L8415:      	CALL L0447
            	JP L8420

L841B:      	ADD A,$11
            	JP L8415

L8420:      	LD HL,$1B01
            	CALL L0454
            	LD B,A
            	LD HL,$1B09
            	CALL L0454
            	CP B
            	JP C,L8439
            	SUB $0C
L8433:      	JP L0447

L8439:      	ADD A,$0C
            	JP L8433
The code ending jumps directly to L0447 and it uses a feature of the stack pointer where L0447 contains the RET instruction to get back to the calling point.

The third enemy

The game doesn't return directly to the calling point at L82F7, instead it jumps to 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
It first checks if the LFF10 variable is $fe to jump to L8926, but as it is initialized to zero, it sets C to $7f and calls L891D and further chain-linking to L8333 to check if a random number is greater than or equal to $40 to create a third enemy appearing at the top of the pyramid (the snake in Q*Bert), if it is less than $40 it pops the return address from the stack (POP IX) and returns without doing anything. This means the third enemy has a 50% chance of appearing at the top of the pyramid on each player's move.
As the value of the R register is compared against C containing $7f, it almost always returns unchanged at L8333 with RET C because R is $00-$7f, and the remaining of the code is only used if R is $7f in an amazing lack of understanding on my side of how R works.
The enemy is created at the top of the pyramid with the same sprite frame for enemies (a lost opportunity to show another type of enemy) and fixed gray color, it also chain-links to L890F to mark the enemy as initialized (setting LFF10 to $fe).

Third enemy movement

If the third enemy is present (LFF10 contains $fe), the code jumps to 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
The Y-coordinate for the third enemy is read from VRAM $1b0c and added $11 to make it to advance vertically, and if it reaches $5d (comparison at L8971), it is made to disappear by setting the Y-coordinate to $d1 (outside of the visual range), setting LFF10 to $00, and pops the return address (POP IX) and returns to the caller.
If it hasn't reached $5d, then it generates a random number and if it is less than $3f then it adds $0c to the X-coordinate, else it subtracts $0c to the X-coordinate, making it to oscillate horizontally in the pyramid.
In L894D it does a comparison of the Y-coordinate of the player against the Y-coordinate of the enemy, and if both are the same then it does the comparison of X-coordinates, and in case of collision it jumps to L8480.

Enemy collision

After completing the enemies and returning to the calling point at L82F7, it jumps to L843E.
This code is looking like the collision detection with enemies.

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
It reads the Y-coordinate of the player from VRAM and does a comparison against the Y-coordinates of enemies, if any of these is the same then it calls a further subroutine at L845C and L846E to do a comparison of the respective X-coordinate, and if any of these is the same it jumps to L8480 (player defeated).

Player defeated

When any enemy gets over the player, the game jumps to L8480.

L8480:      	LD A,$F6
            	LD (LEF13),A
            	LD HL,$8020
            	LD DE,$9020
            	CALL L1500
            	LD HL,$8020
            	LD DE,$8030
            	CALL L1500
            	LD HL,$8820
            	LD DE,$8830
            	CALL L1500
            	LD HL,$8030
            	LD DE,$8830
            	CALL L1500
            	LD HL,$8438
            	LD DE,$8638
            	CALL L1500
            	LD HL,$8838
            	LD DE,$9038
            	CALL L1500
            	LD HL,$8840
            	LD DE,$8850
            	CALL L1500
            	LD HL,$8840
            	LD DE,$9040
            	CALL L1500
            	LD HL,$9040
            	LD DE,$9050
            	CALL L1500
            	LD HL,$8C40
            	LD DE,$8C50
            	CALL L1500
            	LD HL,$8858
            	LD DE,$9058
            	CALL L1500
            	LD HL,$8858
            	LD DE,$8868
            	CALL L1500
            	LD HL,$8868
            	LD DE,$8C68
            	CALL L1500
            	LD HL,$8C58
            	LD DE,$8C68
            	CALL L1500
            	LD HL,$8C58
            	LD DE,$9068
            	CALL L1500
            	LD DE,$8870
            	LD HL,$887C
            	CALL L1500
            	LD DE,$9070
            	LD HL,$907C
            	CALL L1500
            	LD DE,$8870
            	LD HL,$9070
            	CALL L1500
            	LD HL,$8A80
            	LD DE,$8E80
            	CALL L1500
            	LD HL,$887C
            	LD DE,$8A80
            	CALL L1500
            	LD DE,$8E80
            	LD HL,$907C
            	CALL L1500
            	LD DE,$8888
            	LD HL,$9088
            	CALL L1500
            	LD DE,$8888
            	LD HL,$8898
            	CALL L1500
            	LD DE,$8C88
            	LD HL,$8C98
            	CALL L1500
            	LD DE,$9088
            	LD HL,$9098
            	CALL L1500
            	LD HL,$88A0
            	LD DE,$88B0
            	CALL L1500
            	LD HL,$8CA0
            	LD DE,$8CB0
            	CALL L1500
            	LD HL,$90A0
            	LD DE,$90B0
            	CALL L1500
            	LD HL,$88A0
            	LD DE,$8CA0
            	CALL L1500
            	LD HL,$8CB0
            	LD DE,$90B0
            	CALL L1500
            	LD HL,$84C0
            	LD DE,$8BC0
            	CALL L1500
            	LD HL,$8DC0
            	LD DE,$90C0
            	CALL L1500
            	LD B,$05
L85A7:      	PUSH BC
            	LD BC,$0000
L85AB:      	DEC BC
            	LD A,B
            	OR C
            	JR NZ,L85AB
            	POP BC
            	DJNZ L85A7
            	CALL L0109
            	JP L8000
Player defeated in Cubos
Player defeated in Cubos.
It draws the letters "PIERDES!" (Spanish for You lost!) using vectors (or for the same meaning, tons of lines) in white with red background, again for the pseudo-3D effect.
It has a lengthy delay (L85A7) and then it jumps back to the start of the game at L8000.

Level completion

When all pyramid cubes are filled, it increases the level number at LFF0D.

L85C1:          LD HL,LFF0D
            	INC (HL)
            	LD A,(HL)
            	CP $02
            	JP Z,L85DF
            	CP $03
            	JP Z,L8618
            	CP $04
            	JP Z,L8640
            	CP $05
            	JP Z,L8668
            	CP $06
            	JP Z,L8690
L85DF:          LD DE,L85F8
L85E2:      	LD HL,$3820
            	LD BC,$0020
            	CALL L0309
            	XOR A
            	LD (LFF0F),A
            	LD (LFF0E),A
            	JP L8AA9

L8618:          LD DE,L8620
            	JP L85E2

L8640:          LD DE,L8648
            	JP L85E2

L8668:          LD DE,L8670
            	JP L85E2

L8690:      	LD A,$01
            	LD (HL),A
            	JP L8000
Depending on the level number, it selects the sprite for the next enemies to be displayed, resets the number of filled cubes, and the turn for enemies. When the sprite is changed a bug happens because you can see briefly how the current enemies convert into the new enemies.
In an obvious afterthought, it jumps to L8AA9 to show another vectorial message:

L8AA9:      	LD A,$FD
            	LD (LEF13),A
            	LD HL,$8038
            	LD DE,$9038
            	CALL L1500
            	LD HL,$8038
            	LD DE,$8048
            	CALL L1500
            	LD HL,$8048
            	LD DE,$8348
            	CALL L1500
            	LD HL,$9038
            	LD DE,$9048
            	CALL L1500
            	LD DE,$8948
            	LD HL,$9048
            	CALL L1500
            	LD HL,$8940
            	LD DE,$8948
            	CALL L1500
            	LD HL,$8850
            	LD DE,$8860
            	CALL L1500
            	LD HL,$8860
            	LD DE,$9060
            	CALL L1500
            	LD HL,$8850
            	LD DE,$9050
            	CALL L1500
            	LD HL,$8C50
            	LD DE,$8C60
            	CALL L1500
            	LD HL,$8868
            	LD DE,$9068
            	CALL L1500
            	LD HL,$8878
            	LD DE,$9078
            	CALL L1500
            	LD HL,$8868
            	LD DE,$9078
            	CALL L1500
            	LD HL,$8880
            	LD DE,$8890
            	CALL L1500
            	LD HL,$8880
            	LD DE,$9080
            	CALL L1500
            	LD HL,$8890
            	LD DE,$9090
            	CALL L1500
            	LD HL,$8C80
            	LD DE,$8C90
            	CALL L1500
            	LD HL,$8898
            	LD DE,$88A8
            	CALL L1500
            	LD HL,$8898
            	LD DE,$8C98
            	CALL L1500
            	LD HL,$8C98
            	LD DE,$8CA8
            	CALL L1500
            	LD HL,$8CA8
            	LD DE,$90A8
            	CALL L1500
            	LD HL,$9098
            	LD DE,$90A8
            	CALL L1500
            	LD HL,$84B8
            	LD DE,$8CB8
            	CALL L1500
            	LD HL,$8EB8
            	LD DE,$90B8
            	CALL L1500
            	LD B,$05
L8B88:      	PUSH BC
            	LD BC,$0000
L8B8C:      	DEC BC
            	LD A,B
            	OR C
            	JR NZ,L8B8C
            	POP BC
            	DJNZ L8B88
            	JP L8003
Player completes level in Cubos
Player completes level in Cubos. You can see the bug where the enemies for the next level are redefined too soon.
The message is GANAS! (Spanish for You won!) It does another big delay at L8B88, and jumps back to re-display the pyramid.
However, for the last level it never shows the GANAS! message, because it jumps directly to L8690 where it sets the level number to 1 and jumps to the game start! (facepalm)

Memory variables

The memory variables used for this game are admirably sparse.

        ORG RAM_BASE

LEF13:      RB 1	; Color for drawing pixels.
LFF0D:      RB 1	; Level number.
LFF0E:      RB 1	; Turn for enemies.
LFF0F:      RB 1	; Number of filled cubes.
LFF10:      RB 1	; Status of the third enemy.
			; ($00- Not created, $fe- Created)

debounce:       rb 1

STACK:  EQU RAM_BASE+1024
The coordinates for the player and the enemies are preserved in VRAM.

Interlude

It is pretty clear that I still didn't have a plan for writing the game, so the patches accumulated, making it very difficult to try to follow the code.
Many long subroutines could have been reused to simplify the game. Having the player and enemies coordinates in RAM could have simplified the code, and allowed to do a real-time game including the characteristic jumping of the player and enemies.
The lack of frontier detection for the player was a tragic mistake that could have been solved simply by having a table of each available cube coordinate in the pyramid, however, I'm pretty sure the patched code worked against me making each time harder to see where I should patch the code. The obvious solution would have been that each key loaded a register with the required displacement, and a single routine would have been in charge of moving the player and checking if the target position was valid.
The vectorial line drawing for letters could have benefited from an origin-target-target table saving a ton of calls to the line drawing subroutine.
It was a missed chance to not have a special frame for the enemy falling from the top, or different frames per the directions of the player, or a different frame for when the player is touched by the enemies. At least this time all the graphics were my originals.
Also there is no sound effects, probably because the game stops each time to wait for movement.
This was going to be an epilogue. However, I wasn't satisfied by just getting out my old buggy game, I needed to correct it! So here we go.

Drawing the pyramid

Instead of drawing the pyramid using the stair-like code, I preferred to use a single pseudo-3D cube subroutine to draw every one of the fifteen cubes in the pyramid.

        ;
        ; 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.
The first step is drawing the pyramid in rows. The first row contains one cube, the second row contains two cubes, and iteratively until it draws five cubes in the fifth row. The code for drawing a pseudo-3D cube is this one:

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
Just with this change, we saved 243 bytes.
My next change was to define the enemy sprite at the start of the level and put together in memory all the enemy sprites bitmaps so loading the enemy into the VRAM is a matter of multiplying the level by 32 and adding an offset, added also a new sprite drawing for the enemy falling from the top (guess what? A snake! The sprite comes from Viboritas). This also removes the bug of not showing the winning message in the last level. Even though we added a new sprite, the game size was reduced more because of the removal of inefficient code.
I coded a list of valid positions over the pyramid (the whole fifteen cubes), and replaced the movement code with a selection of offsets for movement, then it reads the current coordinates of the player, displaces these by the offset, and checks if it is a valid movement. If the movement is valid then it updates the player position, and checks if it should draw the footprint. If the movement isn't valid then the player isn't moved.

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

Each of the four possible movements is coded as two offsets in the register DE, and added to the current X,Y position of the player. Now it does a comparison against each one of the valid positions over the pyramid (the table is valid_positions). If the movement is invalid, it simply jumps to move the enemies. If the movement is valid, it updates the X,Y coordinates of the player.
As bonus points for doing this change, I could remove the confusing replicated movement code as each one read the X,Y coordinates of the player! Also saved a ton of bytes.
Finally, I never really liked the face I draw for the corners, so I made another and replaced completely the drawing code with a single subroutine for copying a rectangle (used four times).

    ;
	; 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
I removed also a few chain-linked jumps, and the updated version of the game is 547 bytes shorter and easier to understand.
Cubos updated and corrected (v2)
Cubos updated and corrected (v2).

Epilogue

Having a peek into code written in my early years gave me a great view of how I solved the design problems.
My kid's enthusiasm kept me focused in a problem, and if I couldn't find a solution, I simply went ahead coding other parts of the game.
But this same enthusiasm prevented me from using some time to make a plan for the game. This single thing could have saved me tons of hours of mistakes in development, and it would have helped me to write more professional games sooner.
I see now that coming from BASIC language, I coded like in BASIC language: Added things until it worked, except in BASIC you could insert lines of code, and renumber source code lines. We can see BASIC like a pencil, you can erase it in order to insert things or rewrite them, but machine code is like a pen, you cannot correct easily your mistakes.

Downloads

Related links

Last modified: Feb/22/2024