https://nanochess.org/viboritas.html
[logo]
Main page
Intel 8080 emulator
Chess programs
Contests
Store
Retrogaming
FAQ
Links
About me
Ver en espanol
Viboritas: A 2K game made in 1990
Viboritas updated in action
I had completely forgotten about this game, but fortunately I made
backups of my floppy disks for historical record. Recently, I looked
at the floppy disk backup index, and I found things as old as 1989,
and a name caught my attention: Viboritas (Spanish for little
snakes).
This game was coded around 1990, printed, and later saved onto a 5 1/
4" floppy disk, then copied again onto a 3 1/2" floppy disk by 1992
when these became mainstream (the image I found), and finally, it was
back up around 2011. These backups survived by luck several hard
drive crashes till we reach 2024.
I opened the file, extracted the 2K binary, and memories started
coming slowly. I even caught a 34 year-old bug.
The history
1990 homebrew computer for students
After doing my first game in Z80 assembler in 1988, I got a lot of
confidence in my abilities and went to do several other Z80 programs.
However, I lacked access to a proper assembler program, and instead,
I coded directly into machine code using a monitor-style program just
like shown in the picture at the right, you can even see the Z80 code
being typed!
A monitor program was a small application in the ROM of the computer
where you had some basic commands like memory listing, writing to
memory, and executing your program. Some extras were exit points for
your program to show the contents of the Z80 registers and flags.
My father gave again a lecture about computer building in 1990, and I
showcased my Karateka game, but I wanted to do something better. I
was 11 years old, about to become a teen, and I had a great
imagination about impressive games.
My development environment for this game was a homebrew computer for
students with a keyboard, a TV for display, and a set of sheets from
Zilog with the Z80 mnemonics and respective machine code.
The specifications for this homebrew computer were a Zilog Z80 CPU,
2K of EPROM, 2K of RAM, a TMS9118 VDP, and a Commodore keyboard. You
could plug in an expansion board with an AY-3-8910 sound chip that
fed from the Z80 clock. The VDP chip, AY chip, and keyboard were
readily available because of the 1984 crash.
Students working in the 1990 homebrew computer
Students working in the homebrew computer. You can see a young
@nanochess in the front right. Dec/1990.
Into the binary
At the time, I was very inspired by my favorite magazine: Input MSX,
in this case issue 12, containing screenshots of a game called Future
Knight, and I was in awe imagining how great was it by the
screenshots and cover art (later I discovered it is a pretty boring
game). In the same issue, there was a BASIC language game called "El
Castillo Embrujado" that I already had ported to the BASIC language
available in another homebrew computer built by my father.
For this project, I wanted to do a science fiction game in 2K, where
the player would use ladders, avoid enemies, and... I had no more.
But I was inspired by the ton of ladders in Future Knight. I didn't
know what size coding was, but I put the 2K limit in my mind, and the
other objective was that the game should be coded directly into the
student's computer, now most known as coding in the real hardware.
I didn't envision a final for the game, nor a history, or even a
gameplay. After all, I only wanted to have fun developing games, and
of course, showcase my game to the students. Notice that having fun
developing games isn't the same as developing fun games.
I think that this is my 3rd game in machine code for Z80 processors.
The binary came from a 1992 demo disk for students, but it was
originally coded in 1990. I only extracted the 2K of the game from
the 720K disk image.
The binary of the game looks like this:
Part of the original binary of Viboritas
Given the lack of information about it, I will have to
reverse-engineer my own game! The first pass was to fully disassemble
it. It sized up to just over one thousand lines of Z80 assembler code
but more easily read looking like this:
ORG $8000
FNAME "viboritas.bin"
L04CC: EQU $04CC
L0100: EQU $0100
L0169: EQU $0169
L0447: EQU $0447
L8000: CALL L04CC
CALL L801B
CALL L81BA
L8009: CALL L8217
L800C: CALL L8324
CALL L8504
JR NC,L800C
L8014: LD HL,L87FC
INC (HL)
JP L8009
I separated the ROM calls, and I don't remember if I have a copy of
this computer ROM, but I still remember the function of each call
(these are the same as in my first Z80 game). L0100 sets a VDP
address (for VRAM or VDP registers), L0169 reads ahead to send data
to VRAM, L04CC cleans the screen, and L0447 reads the keyboard. A
thing that changed between 1988 and 1990 computers was the port
numbers for the VDP.
I made sure the disassembled listing assembles the same binary as the
original. Then I started separately the adaptation for the MSX
computers and Colecovision consoles, both having the same video
processor, so you can play the game. Just don't expect too much from
a kid.
First steps
For starters, I'll reuse the translation layer I made for my Karateka
game in Z80 assembler. It will help us to play this old game on an
MSX or Colecovision.
;
; Viboritas (little snakes)
;
; by Oscar Toledo G.
; (c) Copyright Oscar Toledo G. 1990-2024
; https://nanochess.org/
;
; Creation date: Oct/1990. I was 11 years old.
; Revision date: Jan/31/2024. Disassembled.
; Revision date: Feb/01/2024. Ported to MSX/Colecovision.
;
COLECO: EQU 1 ; Define this to 0 for MSX, 1 for Colecovision
RAM_BASE: EQU $E000-$7000*COLECO
VDP: EQU $98+$26*COLECO
PSG: EQU $FF ; Colecovision
PSG_ADDR: EQU $A0 ; MSX
PSG_DATA: EQU $A1 ; MSX
KEYSEL: EQU $80
JOYSEL: EQU $C0
JOY1: EQU $FC
JOY2: EQU $FF
if COLECO
fname "viboritas_cv.ROM"
org $8000,$9fff
dw $aa55 ; No BIOS title screen
dw 0
dw 0
dw 0
dw 0
dw START
jp 0 ; RST $08
jp 0 ; RST $10
jp 0 ; RST $18
jp 0 ; RST $20
jp 0 ; RST $28
jp 0 ; RST $30
jp 0 ; RST $38
jp 0 ; No NMI handler
else
fname "viboritas_msx.ROM"
org $4000,$5fff
dw $4241
dw START
dw 0
dw 0
dw 0
dw 0
dw 0
dw 0
WRTPSG: equ $0093
SNSMAT: equ $0141
endif
WRTVDP:
ld a,b
out (VDP+1),a
ld a,c
or $80
out (VDP+1),a
ret
SETWRT:
ld a,l
out (VDP+1),a
ld a,h
or $40
out (VDP+1),a
ret
WRTVRM:
push af
call SETWRT
pop af
out (VDP),a
ret
FILVRM:
push af
call SETWRT
.1: pop af
out (VDP),a
push af
dec bc
ld a,b
or c
jp nz,.1
pop af
ret
; Setup VDP before game
setup_vdp:
LD BC,$0200
CALL WRTVDP
LD BC,$C201 ; No interrupts
CALL WRTVDP
LD BC,$0F02 ; $3C00 for pattern table
CALL WRTVDP
LD BC,$FF03 ; $2000 for color table
CALL WRTVDP
LD BC,$0304 ; $0000 for bitmap table
CALL WRTVDP
LD BC,$3605 ; $1b00 for sprite attribute table
CALL WRTVDP
LD BC,$0706 ; $3800 for sprites bitmaps
CALL WRTVDP
LD BC,$0407 ; Blue border
CALL WRTVDP
IF COLECO
LD HL,($006C) ; MSX BIOS chars
LD DE,-128
ADD HL,DE
ELSE
LD HL,($0004) ; MSX BIOS chars
INC H
ENDIF
PUSH HL
LD DE,$0100
LD BC,$0300
CALL LDIRVM
POP HL
PUSH HL
LD DE,$0900
LD BC,$0300
CALL LDIRVM
POP HL
LD DE,$1100
LD BC,$0300
CALL LDIRVM
LD HL,$2000
LD BC,$1800
LD A,$F4
CALL FILVRM
RET
LDIRVM:
EX DE,HL
.1: LD A,(DE)
CALL WRTVRM
INC DE
INC HL
DEC BC
LD A,B
OR C
JR NZ,.1
RET
GTTRIG:
if COLECO
out (KEYSEL),a
ex (sp),hl
ex (sp),hl
in a,(JOY1)
ld c,a
in a,(JOY2)
and c
ld c,a
out (JOYSEL),a
ex (sp),hl
ex (sp),hl
in a,(JOY1)
and c
ld c,a
in a,(JOY2)
and c
rlca
rlca
ccf
ld a,0
sbc a,a
ret
else
xor a
call $00d8
or a
ret nz
ld a,1
call $00d8
or a
ret nz
ld a,2
call $00d8
or a
ret nz
ld a,3
call $00d8
or a
ret nz
ld a,4
call $00d8
ret
endif
;
; Gets the joystick direction
; 0 - No movement
; 1 - Up
; 2 - Up + right
; 3 - Right
; 4 - Right + down
; 5 - Down
; 6 - Down + left
; 7 - Left
; 8 - Left + Up
;
GTSTCK:
if COLECO
out (JOYSEL),a
ex (sp),hl
ex (sp),hl
in a,(JOY1)
ld b,a
in a,(JOY2)
and b
and $0f
ld c,a
ld b,0
ld hl,joy_map
add hl,bc
ld a,(hl)
ret
joy_map:
db 0,0,0,6,0,0,8,7,0,4,0,5,2,3,1,0
else
xor a
call $00d5
or a
ret nz
ld a,1
call $00d5
or a
ret nz
ld a,2
jp $00d5
endif
; ROM routines I forgot
; Clean screen
L04CC: ; $04cc
LD HL,$3C00
LD BC,$0300
XOR A
JP FILVRM
; Select address or register in VDP
L0100:
LD A,L
OUT (VDP+1),A
LD A,H
ADD A,$40
OUT (VDP+1),A
RET
; Copy string to VDP
L0169: ; $0169
EX (SP),HL
.0: LD A,(HL)
INC HL
OR A
JR Z,.1
PUSH AF
POP AF
OUT (VDP),A
JR .0
.1: EX (SP),HL
RET
;
; Start of the game
;
START: ; 8000
DI ; We don't need interruptions.
LD SP,L87F0
if COLECO
CALL $1FD6 ; Turn off sound.
endif
CALL setup_vdp ; Not in original but needed to setup VDP.
ld hl,$7513
ld (L8780),hl
ld hl,$983f
ld (L8782),hl
ld hl,$c9bf
ld (L8784),hl
We now start analyzing the code, trying to discover how it works.
We'll go forward and backward on the 2K memory map and as I coded
directly in machine code, there are no names for labels except for
their corresponding address in the original binary.
L8000: CALL L04CC ; Clear the screen.
CALL L801B
CALL L81BA
L8009: CALL L8217
L800C: CALL L8324
CALL L8504
JR NC,L800C
L8014: LD HL,L87FC
INC (HL)
JP L8009
The first call is pretty obvious, it simply clears the screen. Notice
the original game assumed the VDP was already initialized, but we
already took care of it with CALL setup_vdp.
The next call L801B apparently does graphics set up.
L801B: LD HL,$0400 ; VRAM bitmap data $80 character.
LD DE,L806A
LD BC,$00C8
CALL L805D
LD HL,$2400 ; VRAM color data $80 character (1st).
LD DE,L8112
LD BC,$001B
CALL L8148
LD HL,$2C00 ; VRAM color data $80 character (2nd).
LD DE,L8112
LD BC,$001B
CALL L8148
LD HL,$3400 ; VRAM color data $80 character (3rd).
LD DE,L8112
LD BC,$001B
CALL L8148
LD HL,$3800 ; VRAM sprite bitmaps.
LD DE,L8404
LD BC,$0100
CALL L805D
LD HL,$4400 ; Obviously a patch.
JP L83FB
L83FB: CALL L0100
LD HL,$41C2
JP L0100
The target address for VRAM is in the register HL, while the source
address is in DE, and the byte count is in BC. That's completely
reversed from the standard Z80 definitions for LDIR, or the MSX
LDIRVM BIOS subroutine.
As I was coding in machine code, any mistake was a pain to correct,
especially if you needed to insert additional instructions! As you
can see the jump to $83FB continues setting register 4 of VDP to
zero, and then proceeds to set up register 1 of VDP for 16x16
sprites.
The VDP in high-resolution mode needs to have separate bitmap
definitions for three 64-pixel-high areas for a total of 192 vertical
rows. Setting the VDP register 4 is a trick for the VDP to repeat the
top bitmap into the other two screen areas. I found this trick months
ago experimenting different values for VDP registers.
So we mark L801B as "Setup graphics".
Then we have the L805D subroutine that simply copies data from the
memory to VRAM.
L805D: CALL L0100
L8060: LD A,(DE)
OUT (VDP),A
INC DE
DEC BC
LD A,B
OR C
JR NZ,L8060
RET
Now I can see why I did it that way as the L0100 subroutine uses HL
as VDP address then it was easier for me to have the source data
pointed by the register DE. I've replaced the original port $B0 (VDP
write) and $c0 (VDP read) with the VDP definition for the current
console (MSX or Colecovision).
It is followed by 200 bytes of bitmaps for the game:
L806A: db $FF,$FF,$FF,$FF ; $806A
db $FF,$FF,$FF,$FF ; $806E
db $E7,$E7,$E7,$E7 ; $8072
db $E7,$E7,$E7,$E7 ; $8076
db $FF,$FF,$00,$FF ; $807A
db $FF,$00,$FF,$FF ; $807E
db $42,$42,$7E,$42 ; $8082
db $42,$7E,$42,$42 ; $8086
db $FE,$82,$BA,$AA ; $808A
db $BA,$82,$FE,$00 ; $808E
db $BA,$BA,$BA,$BA ; $8092
db $BA,$BA,$BA,$BA ; $8096
db $EE,$00,$FF,$FF ; $809A
db $FF,$00,$00,$00 ; $809E
db $42,$42,$7E,$42 ; $80A2
db $42,$7E,$42,$42 ; $80A6
db $EF,$EF,$EF,$00 ; $80AA
db $FE,$FE,$FE,$00 ; $80AE
db $7E,$7E,$7E,$00 ; $80B2
db $6E,$6E,$6E,$00 ; $80B6
db $00,$FF,$FF,$AA ; $80BA
db $44,$00,$00,$00 ; $80BE
db $42,$42,$7E,$42 ; $80C2
db $42,$7E,$42,$42 ; $80C6
db $EE,$EE,$EE,$00 ; $80CA
db $EE,$EE,$EE,$00 ; $80CE
db $40,$30,$0C,$03 ; $80D2
db $0C,$30,$40,$40 ; $80D6
db $00,$FF,$00,$AA ; $80DA
db $55,$00,$FF,$00 ; $80DE
db $81,$81,$C3,$BD ; $80E2
db $81,$81,$C3,$BD ; $80E6
db $81,$58,$37,$47 ; $80EA
db $39,$27,$49,$27 ; $80EE
db $47,$49,$27,$40 ; $80F2
db $28,$15,$12,$27 ; $80F6
db $00,$FE,$FE,$00 ; $80FA
db $EF,$EF,$00,$00 ; $80FE
db $0C,$0C,$18,$18 ; $8102
db $30,$30,$18,$18 ; $8106
db $54,$FE,$54,$FE ; $810A
db $54,$FE,$54,$00 ; $810E
This includes walls, columns, floor, and ladders (4 characters for
each level), for a total of 5 levels, plus a kind of drain cover.
Next is the color table for these bitmaps.
L8112: db $08,$22,$08,$3C ; $8112
db $08,$A1,$08,$F1 ; $8116
db $08,$74,$08,$E1 ; $811A
db $01,$F1,$01,$11 ; $811E
db $03,$E1,$03,$11 ; $8122
db $08,$F1,$08,$6E ; $8126
db $08,$E1,$10,$F1 ; $812A
db $08,$61,$08,$A1 ; $812E
db $03,$F1,$02,$51 ; $8132
db $03,$F1,$08,$E1 ; $8136
db $08,$98,$08,$32 ; $813A
db $01,$11,$06,$6E ; $813E
db $01,$11,$08,$31 ; $8142
db $08,$F1 ; $8146
What is this? This data cannot be copied directly to the VDP, instead
it looks like there are byte counts.
L8148: CALL L0100
LD B,C
L814C: PUSH BC
LD A,(DE)
LD B,A
INC DE
LD A,(DE)
INC DE
L8152: OUT (VDP),A
NOP
DJNZ L8152
POP BC
DJNZ L814C
RET
And that's right, the kid was smart enough to create a decompressor
that reads a count of bytes and a byte to replicate. So 73 bytes
replace 200 bytes of color.
Bitmaps used for the level backgrounds.
Bitmaps used for the level backgrounds along character number. Notice
the order of wall, column, floor, and ladder.
Do you remember I mentioned "El Castillo Embrujado" from Input MSX?
At age 11 I wasn't very confident in my graphical design abilities,
so for my game I reused the sprite graphics for the player and the
snakes. Many years later I discovered these graphics from "El
Castillo Embrujado" were in fact a copy from another game, the famous
Abu-Simbel Profanation for ZX Spectrum.
However, for this article I'll help my younger myself designing
all-new graphics. If you are curious about it you can see the
previous graphics set in the viboritas_orig.asm file.
Comparison of 1990 sprites versus updated sprites.
At the left you can see the 1990 sprites and at the right the updated
sprites.
The new sprite set for Viboritas.
The new sprite set for Viboritas.
;
; Sprites for the player and half of the snakes.
;
L8404:
; $00 - Player going right (frame 1).
DB $00,$01,$05,$03,$07,$03,$07,$1e
DB $37,$67,$77,$74,$03,$0e,$0e,$0f
DB $00,$50,$f0,$f0,$d0,$70,$10,$e0
DB $00,$b8,$b8,$00,$c0,$f8,$7c,$00
; $04 - Player going right (frame 2).
DB $00,$02,$01,$03,$01,$03,$03,$07
DB $07,$06,$06,$07,$03,$03,$03,$03
DB $a8,$f8,$f8,$e8,$b8,$88,$70,$80
DB $c0,$e0,$e0,$00,$c0,$00,$c0,$e0
; $08 - Player going left (frame 1).
DB $00,$0a,$0f,$0f,$0b,$0e,$08,$07
DB $00,$1d,$1d,$00,$03,$07,$1e,$3e
DB $00,$80,$a0,$c0,$e0,$c0,$e0,$78
DB $ec,$e6,$ee,$2e,$c0,$70,$70,$f0
; $0c - Player going right (frame 2).
DB $15,$1f,$1f,$17,$1d,$11,$0e,$01
DB $03,$07,$07,$00,$03,$00,$03,$07
DB $00,$40,$80,$c0,$80,$c0,$c0,$e0
DB $e0,$60,$60,$e0,$c0,$c0,$c0,$c0
; $10 - Player using ladder (frame 1).
DB $0a,$07,$0f,$0f,$07,$07,$03,$0c
DB $1b,$70,$73,$02,$06,$06,$1e,$3e
DB $a0,$c0,$e0,$e0,$ce,$ce,$98,$70
DB $c0,$00,$c0,$60,$38,$3c,$00,$00
; $14 - Player using ladder (frame 2).
DB $05,$03,$07,$07,$73,$73,$19,$0e
DB $03,$00,$03,$06,$1c,$3c,$00,$00
DB $50,$e0,$f0,$f0,$e0,$e0,$c0,$30
DB $d8,$0e,$ce,$40,$60,$60,$78,$7c
; $18 - Snake going left (frame 1).
DB $1b,$2d,$2d,$36,$1f,$7d,$9b,$03
DB $0f,$1f,$3e,$3c,$3c,$3f,$1f,$0f
DB $00,$00,$00,$00,$00,$80,$80,$82
DB $02,$06,$06,$0e,$cc,$ec,$fc,$38
; $1c - Snake going right (frame 2).
DB $00,$0d,$16,$16,$1b,$0f,$1e,$5d
DB $61,$0f,$1f,$1e,$1e,$1f,$0f,$07
DB $00,$80,$80,$80,$00,$80,$c0,$c0
DB $c0,$84,$0c,$cc,$d8,$f8,$b8,$30
Music player
At this moment of the analysis, the following routine isn't called
yet.
L815B: LD HL,L87FA
INC (HL)
LD A,(HL)
CP $08
JR NZ,L8194
LD (HL),$00
DEC HL
INC (HL)
LD A,(HL)
CP $30
JR NZ,L816F
LD (HL),$01
L816F: LD A,(HL)
ADD A,255 AND (L8744-1)
LD L,A
LD H,(L8744-1)>>8
CALL L8197
NOP
LD A,(HL)
OUT ($80),A
INC HL
LD A,$01
OUT ($00),A
LD A,(HL)
OUT ($80),A
LD A,$07
OUT ($00),A
LD A,$B8
OUT ($80),A
LD A,$08
OUT ($00),A
LD A,$0A
OUT ($80),A
L8194: JP L8398
L8197: LD A,(HL)
ADD A,A
ADD A,255 AND (L81A2-2)
LD L,A
LD H,(L81A2-2)>>8
XOR A
OUT ($00),A
RET
L81A2: dw $01ac
dw $0153
dw $011d
dw $00fe
dw $00f0
dw $0140
dw $00d6
dw $00be
dw $00b4
dw $00aa
dw $00a0
dw $00e2
L8744: db $01,$02,$03,$04 ; $8744
db $05,$04,$03,$02 ; $8748
db $01,$02,$03,$04 ; $874C
db $05,$04,$03,$02 ; $8750
db $06,$04,$07,$08 ; $8754
db $09,$08,$07,$04 ; $8758
db $06,$04,$07,$08 ; $875C
db $09,$08,$07,$04 ; $8760
db $03,$0C,$08,$0A ; $8764
db $0B,$0A,$08,$0C ; $8768
db $06,$04,$07,$08 ; $876C
db $09,$08,$07,$04 ; $8770
Ok, it increments a byte at L87FA, and when it reaches the value 8 it
is reset to zero and proceeds to increment L87F9 until it reaches 48
when it is reset to 1. Of course! L87F9 is the index number in the
song table, and L87FA is the counter of note duration.
Then it uses the index to get the note to play from L8744. Here is a
machine code trick that isn't useful when converting to assembler
mnemonics: You know the data is fixed at the address, so there is no
handling for carry to the higher address byte.
Now we have another patch, this time calling L8197 to get the note
frequency to play, and then it writes to the AY-3-8910 sound chip.
The $00 port address sets the AY-3-8910 index register, and the $80
port address sets the AY-3-8910 data register. It even sets the
register 7 of PSG to $38 to disable white noise, and this value can
burn for real some MSX1 computers. Let us replace the sound code:
CALL L8197
if COLECO
LD A,(HL)
INC HL
LD H,(HL)
LD L,A
AND $0F
OR $80
OUT (PSG),A
SRL H
RR L
SRL H
RR L
SRL H
RR L
SRL H
RR L
LD A,L
OUT (PSG),A
LD A,$93
OUT (PSG),A
else
LD E,(HL)
LD A,0
CALL WRTPSG
INC HL
LD E,(HL)
LD A,1
CALL WRTPSG
LD E,$0A
LD A,$08
CALL WRTPSG
endif
L8194: JP L8398
L8197: LD A,(HL)
ADD A,A
ADD A,255 AND (L81A2-2)
LD L,A
LD H,(L81A2-2)>>8
RET
Also at first glance, I thought this routine was written first, but
then I realized it occupies the space of an uncompressed color table!
When I optimized the color table definition to use compression, I
added the song player in the freed space!
This means the first version of my game didn't have any music, and
there's a chance that somewhere in my files exists a printout. And I
just remembered a thing: I coded the game without music, and a
student had a background in music, and he handed me some musical
notes that I implemented terribly because I didn't know anything
about music timing.
The music is a rendition of a boogie-woogie. The music player still
goes on, and there is a very weird patch jumping at L8398. But we'll
see it later, as it looks like the keyboard code.
Initialization
Now we go to the next unexplored code at L81BA:
L81BA: XOR A
LD (L87F3),A
LD (L87F4),A
LD (L87F7),A
LD (L87F8),A
LD A,$F0
LD (L87F5),A
LD A,$01
LD (L87F6),A
LD A,$0F
LD (L8786),A
LD A,$00
LD (L8787),A
LD HL,$0000
LD (L87F9),HL
LD A,$02
LD (L87FB),A
CALL L04CC
LD HL,$3EE9
CALL L0100
CALL L0169
db "(C) OTEK 1990",0
LD HL,$3EAC
CALL L0100
CALL L0169
db "VIDAS:0",0
LD A,$01
JP L82A5
This is looking like an initialization code (let us add a note to
L81BA as game initialization).
We'll discover the function of each variable soon. So far it cleans
again the screen, sets up the VDP to the last row of the screen, and
shows the copyright message. OTEK comes from my father's initials
(Oscar Toledo Esteva) which we used as a kind of company name.
Also, it shows the number of remaining lives (VIDAS in Spanish),
and... another patch jumps to L82A5.
L82A5: LD (L87FC),A
XOR A
LD (L87FE),A
LD (L87FF),A
RET
It is simply some more variable initialization.
Screen drawing
Now another routine L8217 that is called immediately after in L8009:
L8217: LD A,(L87FC)
ADD A,A
ADD A,A
ADD A,$7C
LD (L87FD),A
LD HL,$3C00
CALL L0100
LD B,$A0
L8229: PUSH BC
LD B,$03
L822C: LD A,(L87FD)
OUT (VDP),A
INC HL
DJNZ L822C
LD A,(L87FD)
INC A
OUT (VDP),A
INC HL
POP BC
DJNZ L8229
LD HL,$3C80
LD B,$04
L8243: PUSH BC
PUSH HL
CALL L0100
LD B,$20
L824A: LD A,(L87FD)
ADD A,$02
OUT (VDP),A
INC HL
DJNZ L824A
POP HL
LD BC,$00A0
ADD HL,BC
POP BC
DJNZ L8243
LD HL,$4701
CALL L0100
LD HL,$2000
CALL L8277
LD HL,$2800
CALL L8277
LD HL,$3000
CALL L8277
JP L8288
It starts by getting a variable from L87FC, multiplying by 4, adding
$7c, and saving the result at L87FD. L87FC is initialized to one.
This starts at $80, it sounds like the character number for level
definition. It draws in sequence 160 walls (3 characters) + columns
(one character), then it draws over 4 floors starting at row 4
($3c80), each one 32 characters wide, using the character in L87FD
offset by 2.
It sets the border to black ($4701), and then the base charset
($00-$7f characters) for the three zones of the screen is set to
black (three calls to L8277)
L8277: LD A,H
ADD A,$04
LD B,A
CALL L0100
L827E: LD A,$F1
OUT (VDP),A
INC HL
LD A,H
CP B
JR NZ,L827E
RET
L8288: LD HL,$3E5E
CALL L0100
LD A,$94
OUT (VDP),A
LD HL,$3C80
CALL L82B0
LD HL,$3D20
CALL L82B0
LD HL,$3DC0
CALL L82B0
RET
The L8288 patch adds the "drain" character $94 at the bottom right of
the screen. I still don't understand why I didn't just draw a 2x2
door, but probably I felt like there were space constraints (defining
the graphics and drawing the tiles).
At the end, it calls 3 times the L82B0 subroutine with different
screen rows as the base:
L82B0: LD A,(L87FC)
LD B,A
LD A,$06
SUB B
LD B,A
L82B8: PUSH BC
PUSH HL
NOP
LD D,$00
NOP
CALL L82CB
LD E,A
ADD HL,DE
CALL L830F
POP HL
POP BC
DJNZ L82B8
RET
This L82B0 subroutine subtracts the level number from 6 and calls a
subroutine L82CB to get an offset, and then L830F to do something.
L82CB: PUSH BC
PUSH DE
PUSH HL
LD HL,(L8780)
LD DE,(L8782)
LD BC,(L8784)
ADD HL,HL
ADD HL,HL
ADD HL,BC
ADD HL,DE
LD (L8780),HL
ADD HL,DE
ADD HL,DE
ADD HL,BC
ADD HL,BC
ADD HL,BC
ADD HL,HL
ADD HL,HL
ADD HL,HL
ADD HL,HL
ADD HL,DE
ADD HL,BC
LD (L8782),HL
ADD HL,DE
ADD HL,DE
ADD HL,BC
ADD HL,HL
ADD HL,DE
ADD HL,BC
ADD HL,BC
ADD HL,BC
ADD HL,BC
LD (L8784),HL
LD HL,(L8780)
LD DE,(L8782)
LD BC,(L8784)
ADD HL,DE
ADD HL,BC
LD A,H
ADD A,L
AND $1F
POP HL
POP DE
POP BC
RET
It happens L82CB looks a lot like a random number generator, and only
gets a number between 0 and 31 in the accumulator register.
L830F: LD B,$05
LD A,(L87FD)
ADD A,$03
L8316: PUSH AF
CALL L0100
POP AF
OUT (VDP),A
LD DE,$0020
ADD HL,DE
DJNZ L8316
RET
!Mystery solved! L830F draws a ladder on the screen. Each ladder is 5
rows high, and it is drawn using the level base character offset by
3. In the first level it will draw 5 ladders on each floor, while in
the fifth level, it will draw only one ladder for each floor. That's
an attempt to make more difficult levels.
With all this code analyzed we can mark safely L8217 as the level
drawing code.
The hero and the foes
Now we have the first routine called from the main loop and it is
L8324:
L8324: LD HL,$1B00
LD A,(L8786)
CALL L8390
INC HL
LD A,(L8787)
CALL L8390
INC HL
LD A,(L87FE)
CALL L8390
INC HL
LD A,$0F
CALL L8390
INC HL
LD A,$38
CALL L8390
INC HL
LD DE,L87F3
CALL L86C5
LD A,$0E
CALL L8390
INC HL
LD A,$60
CALL L8390
INC HL
LD DE,L87F5
CALL L86C5
LD A,$0E
CALL L8390
INC HL
LD A,$88
CALL L8390
INC HL
LD DE,L87F7
CALL L86C5
LD A,$0E
CALL L8390
JP L815B
L8390: PUSH AF
CALL L0100
POP AF
OUT (VDP),A
RET
It loads HL with $1b00, pointing to the Sprite Attribute Table. The
VRAM place where sprites are positioned on the screen. And it starts
reading variables and writing to VRAM using L8390 (pretty similar to
WRTVRM of MSX)
L8786 is the Y-coordinate for the player, L8787 is the X-coordinate
for the player, L87FE is the sprite frame for the player. The player
color is white.
It is easily deduced next that the enemies are placed at fixed
vertical positions on the screen ($38, $60, and $88) using a generic
routine L86C5. Notice also that it chain links to the L815B
subroutine for playing the background music, which in turn chain
links to the keyboard decode subroutine (L8398).
L86C5: LD A,(DE)
CALL L8390
INC DE
INC HL
LD A,(DE)
LD B,$18
CP $00
JR NZ,L86D4
LD B,$20
L86D4: LD A,(L8788)
XOR $01
LD (L8788),A
BIT 0,A
LD A,$00
JR Z,L86E4
LD A,$04
L86E4: ADD A,B
CALL L8390
INC HL
PUSH HL
LD HL,$390C
LD DE,L8704
LD BC,$0034
CALL L805D
POP HL
RET
The first byte pointed by DE is used for the X-coordinate of the
enemy. And the next byte signals the movement direction to select the
sprite frame for the enemy. It also switches frames using L8788 to
get alternate movement frames (along with B set to base frame $18 or
$20). Then it does something really weird, copying the memory area
L8704 into VRAM address $390c. Oh, I see, it defines two sprites very
late in the game (the two sprite frames for snakes moving to the
right), it is kind of obvious that I didn't foresee all the required
sprites for the game.
For this updated version of the game, I'll modify slightly the code:
LD HL,$3900 ; Define frame sprites $20 and $24
LD DE,L8700 ; Data for snake going to right.
LD BC,$0040 ; Length of data.
CALL L805D ; Copy to VRAM.
It would not have fit by 8 bytes, or I would have to move a
significant portion of code to make space. In this case my option
could have been moving the portion of code at $8504-$8533 (the
complex enemy movement code), but I didn't have a MOVE command in the
monitor program. I had to copy manually the machine code at the new
position.
;
; Extra sprites for snakes going right.
;
L8700:
DB $00,$01,$01,$01,$00,$01,$03,$03
DB $03,$21,$30,$33,$1b,$1f,$1d,$0c
DB $00,$b0,$68,$68,$d8,$f0,$78,$ba
DB $86,$f0,$f8,$78,$78,$f8,$f0,$e0
DB $00,$00,$00,$00,$00,$01,$01,$41
DB $40,$60,$60,$70,$33,$37,$3f,$1c
DB $d8,$b4,$b4,$6c,$f8,$be,$d9,$c0
DB $f0,$f8,$7c,$3c,$3c,$fc,$f8,$f0
Player movement
The music player chain links to the keyboard code:
L8398: CALL L0447
CP $10
JP Z,L83DD
CP $0F
JP Z,L83B5
CP $37
JP Z,L85B5
CP $38
JP Z,L8567
CP $02
JP Z,L8684
RET
Now this code is looking every moment more like spaghetti code, and
it also needs to be rewritten for portable joystick handling:
L8398: CALL GTSTCK
CP $07 ; Going left?
JP Z,L83DD
CP $03 ; Going right?
JP Z,L83B5
CP $01 ; Going up?
JP Z,L85B5
CP $05 ; Going down?
JP Z,L8567
CALL GTTRIG
CP $ff ; Button pressed?
JP Z,L8684
RET
The first joystick subroutine is L83B5:
L83B5: LD HL,L8787
INC (HL)
INC (HL)
NOP
LD A,(HL)
CP $00
JR NZ,L83C2
LD (HL),$FE
L83C2: LD A,(L87FE)
CP $04
JR NZ,L83CD
LD A,$00
JR L83CF
L83CD: LD A,$04
L83CF: LD (L87FE),A
RET
At this point, we know that L8787 is the X-coordinate of the player
(based on the Sprite Attribute Table writes), and the double
increment makes clear that the player moves to the right. If the
X-coordinate becomes 0, it is rewritten with limit $fe (254 pixels).
It also animates the player switching between sprite frames $00 and
$04.
L83DD: LD HL,L8787
DEC (HL)
DEC (HL)
NOP
LD A,(HL)
CP $FE
JR NZ,L83EA
LD (HL),$00
L83EA: LD A,(L87FE)
CP $0C
JR NZ,L83F5
LD A,$08
JR L83F7
L83F5: LD A,$0C
L83F7: LD (L87FE),A
RET
The next subroutine is the opposite: Moving the player to the left by
two pixels. It also checks for exceeding the left border and sets the
X-coordinate to zero. It animates the player switching between sprite
frames $08 and $0c.
The NOP instruction after the two decrements makes me think that I
considered moving horizontally the player at a speed of three pixels.
Using ladders
The code to allow the player to go up and down over the ladders is
heavily patched, so probably it took me a lot of effort and
experiments.
Let us start with the code to go down (this was coded first because
the player needs to go down from the top floor):
;
; Move the player downward.
;
L8567: LD HL,$1B00
CALL L85A0
CALL L85AE
NOP
LD D,A
INC HL
CALL L85A0
CALL L85FF
RRCA
RRCA
LD E,A
LD A,D
LD L,A
LD H,$00
ADD HL,HL
ADD HL,HL
ADD HL,HL
ADD HL,HL
ADD HL,HL
LD D,$00
ADD HL,DE
LD DE,$3C40
ADD HL,DE
CALL L85A0
LD B,A
LD A,(L87FD)
ADD A,$03
CP B
RET NZ
CALL L85E9
ADD A,$02
L859C: LD (L8786),A
RET
L85A0: LD A,L
OUT (VDP+1),A
LD A,H
OUT (VDP+1),A
NOP
NOP
NOP
NOP
NOP
IN A,(VDP)
RET
L85AE: INC A
AND $F8
RRCA
RRCA
RRCA
RET
L85E9: LD D,$00
ADD HL,DE
LD A,(L87FE)
LD B,$14
CP $10
JR Z,L85F7
LD B,$10
L85F7: LD A,B
LD (L87FE),A
LD A,(L8786)
RET
L85FF: ADD A,$04
AND $F8
RRCA
RET
L8605: ADD A,$03
CP B
RET Z
POP HL
LD A,(L8786)
INC A
AND $F8
JP L8631
L8631: DEC A
NOP
LD (L8786),A
RET
The innocent kid reads from VRAM the coordinates of the player, but
why on Earth? These variables were already available in RAM.
It first reads from VRAM $1b00 the Y-coordinate of the player into
register D converted to a screen row coordinate, and it also reads
the X-coordinate into register E and adjusts it to a screen column
coordinate. It finally takes both numbers to create a pointer to the
VRAM screen.
D = (Y + 1) / 8
E = (X + 4) / 8
HL = D * 32 + E + $3c20
You can see I did LD A,D followed by LD L,A when I could simply do LD
L,D.
It reads the character from VRAM (CALL L85A0), and it checks if the
character is a ladder (the contents of L87FD plus 3). It calls a
patch L85E9 that for some reason adds a value to the content of HL,
animates the player using the ladder (sprite frames $10 and $14), and
gets the Y-coordinate of the player to move it two pixels downward.
;
; Move the player upward.
;
L85B5: LD A,(L8786)
CALL L85AE
LD D,A
INC HL
LD A,(L8787)
CALL L85FF
RRCA
RRCA
LD E,A
LD A,D
LD L,A
LD H,$00
ADD HL,HL
ADD HL,HL
ADD HL,HL
ADD HL,HL
ADD HL,HL
LD D,$00
ADD HL,DE
LD DE,$3C20
ADD HL,DE
CALL L85A0
LD B,A
LD A,(L87FD)
CALL L8605
NOP
CALL L85E9
SUB $02
JP L859C
The code for moving the player upwards is pretty similar, and somehow
I did the right thing in this code using the existing coordinates in
RAM. This means I was reaching my limits, and having 2K of machine
code in the head isn't so easy!
There are a few differences more like the different offset on the
screen ($3c20 versus $3c40), and the fact it calls L8605 to do a
comparison with the ladder character. If it isn't a ladder, it aligns
the player vertically (again using a patch), and using POP HL it
returns to the main loop instead of the original caller. If it is a
ladder, it moves the player two pixels upwards.
Now for the great embarrassing moment: the player can walk over the
air. Because the code for handling left and right never checks if the
player is over a floor. As the floors are always in the same vertical
position, it would be simply a matter of checking if the player is
over one of the valid Y-coordinates, but I can remember vaguely I was
afraid of moving the code again. Lazy kid!
Winning the game
Once the player reaches the grid in the bottom-right of the screen,
the fire button should be pressed to pass the level. I watched in
delight as students forgot to press the button and they were caught
by the snake.
;
; Button press to exit level.
;
L8684: LD A,(L8786)
CP $87
RET NZ
LD A,(L8787)
CP $E8
RET C
CP $F8
RET NC
LD SP,L87F0
LD A,$0F
LD (L8786),A
XOR A
LD (L8787),A
LD A,(L87FC)
CP $05
JP NZ,L8014
LD HL,$3D4A
CALL L0100
CALL L0169
db "HAS GANADO !",0
LD A,$08
OUT ($00),A
XOR A
JP L87B4
L87B4: OUT ($80),A
JP L878A
L878A: LD B,$05 ; Big delay.
L878C: PUSH BC
LD BC,$0000
L8790: DEC BC
LD A,B
OR C
JR NZ,L8790
POP BC
DJNZ L878C
LD SP,L87F0 ; Reset Stack Pointer.
LD A,$0F ; Reset Y-coordinate for the player.
LD (L8786),A
XOR A ; Reset X-coordinate for the player.
LD (L8787),A
LD A,$01 ; Restart at level 1.
LD (L87FC),A
LD HL,L87F9
LD (HL),$00
INC HL
LD (HL),$00
JP L8009
The subroutine first checks for the Y-coordinate to be $87, and the
X-coordinate is between $e8 and $f7 (good tolerance) and if the
conditions are met it resets the stack pointer, sets the player again
at the top-left of the screen, and if the level number isn't 5 then
it jumps to L8014 to increase the level number else it shows a
message "HAS GANADO" (Spanish for you win) on the screen.
It also turns off the music in another tender example of chain
linking because of the heavily patched code.
The sound code should be rewritten as this:
db "HAS GANADO !",0
if COLECO
ld a,$9f
out (PSG),a
else
ld e,$00
ld a,$08
call WRTPSG
endif
JP L87B4
L87B4:
JP L878A
The L878A routine does a big delay so the "HAS GANADO !" message
stays on the screen, and then resets the game, and sends the player
back to level 1.
Enemy movement
The second subroutine called by the main loop of the game is L8504,
it shows heavy patches.
L8504: CALL L850A
JP L83D3
It calls L850A and then L83D3. L83D3 is more akin to a loop to make
the game run slower (I still didn't know the VDP interrupt line, nor
did I have the line connected to the Z80 processor). After setting BC
to $1000, it also updates the current number of lives on the screen.
L83D3: CALL L861E
L83D6: DEC BC
LD A,B
OR C
JR NZ,L83D6
AND A
RET
L861E: LD BC,$1000
LD A,(L87FB)
ADD A,$30
LD HL,$3EB2
PUSH AF
CALL L0100
POP AF
OUT (VDP),A
RET
The subroutine L850A is longer:
L850A: LD HL,L87F3
CALL L8517
LD L,255 AND L87F5
CALL L8517
LD L,255 AND L87F7
L8517: INC HL
LD A,(HL)
OR A
LD B,$03
JR Z,L8520
LD B,$FD
L8520: DEC HL
LD A,(HL)
ADD A,B
LD (HL),A
CP $FF
JR NZ,L852D
INC HL
LD (HL),$01
JR L8533
L852D: OR A
JR NZ,L8533
INC HL
LD (HL),$00
L8533: CALL L855B
CP B
RET C
CP C
RET NC
LD A,L
SUB 255 AND L87F3
RRCA
ADD A,A
ADD A,A
ADD A,$04
LD L,A
LD H,$1B
LD A,L
OUT (VDP+1),A
LD A,H
OUT (VDP+1),A
NOP
NOP
NOP
NOP
IN A,(VDP)
LD B,A
LD A,(L8786)
INC A
CP B
RET NZ
JP L8613
L855B: DEC HL
LD A,L
AND $FE
OR $01
LD L,A
LD B,(HL)
JP L8738
L8738: LD A,(L8787)
LD C,B
DEC B
DEC B
DEC B
INC C
INC C
INC C
INC C
RET
It uses L8517 each time with a pointer to one of the enemies (L87F3,
L87F5, and L87F7). For each enemy, it checks the current direction
and selects an X-displacement (-3 or +3 pixels) in the B register. If
it reaches a certain coordinate it switches the movement direction.
Once this has been done, another patch calls to L855B to make HL
point exactly to the X-coordinate of the enemy (this code is
dependent on the memory address of the enemy coordinates). It reads
the current X-coordinate into the B register and jumps to another
patch in L8738, where it reads the X-coordinate of the player in A,
makes a copy of B in C, subtracting 3 from B, and adds 4 to C.
When it has created a collision width (minimum is B, maximum is C) it
does a comparison of A (player X-coordinate) with B and returns if it
is less than, and a comparison against C and returns if it is equal
or greater than.
As the enemy state doesn't contain its Y-coordinate, it determines
the sprite from the enemy data address, reads the Sprite Attribute
Table from VRAM to get the Y-coordinate and does a comparison with
the player Y-coordinate (L8786), and returns if both aren't equal,
else it jumps to L8613 to kill the player.
There is a bug in this code and the player can die accidentally while
walking. In an amazing example of how bugs can perdure for years, I
couldn't find this accidental kill bug for years until today (Feb/06/
2024) I finally used debuging tools of BlueMSX. It is pretty easy
once found, when a snake is aligned with the player it returns
correctly because the player isn't in the same floor as the snake,
but it loses the value of the register HL because the VRAM read, and
the next snake X-coordinate will be read from ROM creating a fixed
invisible snake in the next floor. It will fail randomly in a
position dependant of the platform. Do you want to correct it? Just
replace my "smart" optimization in L850A to load each time the full
value of HL with the address of the enemy data instead of only the L
register. Case closed, it only took me 34 years.
Let's continue:
L8613: CALL L837A
DEC (HL)
SCF
LD SP,L87F0
JP L8637
L837A: XOR A
OUT ($00),A
LD A,$AE
OUT ($80),A
LD A,$01
OUT ($00),A
LD A,$06
OUT ($80),A
JP L8774
L8774: LD BC,$0000
L8777: DEC BC
LD A,B
OR C
JR NZ,L8777
JP L866F
L866F: LD A,$08
OUT ($00),A
XOR A
OUT ($80),A
LD BC,$0000
L8679: DEC BC
LD A,B
OR C
JR NZ,L8679
LD HL,L87FB
JR L86F8
L86F8: XOR A
LD (L87F9),A
LD (L87FA),A
RET
L8637: LD A,$0F
LD (L8786),A
XOR A
LD (L8787),A
LD A,(L87FB)
CP $FF
JP NZ,L8009
LD HL,$3D4A
CALL L0100
CALL L0169
db "FIN DE JUEGO",0
LD B,$05
L8660: PUSH BC
LD BC,$0000
L8664: DEC BC
LD A,B
OR C
JR NZ,L8664
POP BC
DJNZ L8660
JP L8000
It is pretty embarrassing this chain-linking of code, but let us go
in parts.
The first line of code at L8613 calls L837A, the ultimate purpose is
loading HL with L87FB to point to the number of lives of the player
and decrement it.
But L837A also creates a sound effect (a first!) then jumps to L8774
for a small delay, and then jumps to L866F to turn off the volume,
does another delay, loads HL with a pointer to the number of lives,
and resets the music player's variables.
After it decrements the number of lives, it sets the carry flag but
obviously, I got lost in this path because it is never used. The
stack pointer is reset, the player is set again to the start point in
L8637, and if there are still lives it jumps to L8009 to continue the
game, or else it displays a message "FIN DE JUEGO" (game over in
Spanish), it waits a longer time, and it resets completely the game
jumping to L8000.
We need to patch the L837A and L866F sound routines with this:
L837A:
if COLECO
ld a,$8E
out (PSG),a
ld a,$2a
out (PSG),a
else
ld e,$ae
ld a,$00
call WRTPSG
ld e,$06
ld a,$01
call WRTPSG
endif
JP L8774
L866F:
if COLECO
ld a,$9f
out (PSG),a
else
ld e,$00
ld a,$08
call WRTPSG
endif
LD BC,$0000
L8679: DEC BC
The used variables
The final list of variables inside the code are:
L8780: rb 2 ; Random generator 1.
L8782: rb 2 ; Random generator 2.
L8784: rb 2 ; Random generator 3.
L8786: rb 1 ; Y-coordinate for the player.
L8787: rb 1 ; X-coordinate for the player.
L8788: rb 1 ; Animation bit for snakes.
L87F3: rb 1 ; X-coordinate of enemy 1.
L87F4: rb 1 ; X-direction of enemy 1.
L87F5: rb 1 ; X-coordinate of enemy 2.
L87F6: rb 1 ; X-direction of enemy 2.
L87F7: rb 1 ; X-coordinate of enemy 3.
L87F8: rb 1 ; X-direction of enemy 3.
L87F9: rb 1 ; Note index for music player.
L87FA: rb 1 ; Tick counter for music player.
L87FB: rb 1 ; Current lives.
L87FC: rb 1 ; Current level.
L87FD: rb 1 ; Base character for drawing levels.
L87FE: rb 1 ; Sprite frame for the player.
L87FF: rb 1 ; Not used, yet initialized.
The stack pointer used to be at $87F0 for 2K RAM student computers in
1990. Later moved to $fff0 for 32K RAM (1992).
You can download the ROM for the game ready to be played on a
Colecovision or MSX. I've also included the commented source code.
The only difference between this and my 1990 game is the redesigned
graphics and adjustments to level colors to enhance visibility. The
original colors blended badly on modern emulators (in 1990 I could
adjust contrast in the Sony Trinitron TV).
* Download viboritas.zip (17.24 kb). Source code, ROM for
Colecovision and MSX, and original binary and disassembly.
Epilogue
A lot of bytes could be saved in this game by refactoring some parts
like using an extra byte to preserve the vertical position of
enemies, moving some initialization code out of the main loop (lives
update and snake sprite definition), using data available on RAM
instead of reading VRAM, and compacting the music player code.
On the other side, it reflects my abilities at the time. I could have
almost 2K of machine code on my head, there wasn't a plan ahead
(denoted by the ton of patches). I was still learning how to code a
platform game, and I wasn't too able to draw graphics.
Writing games or other code directly in machine code isn't practical.
Although at first glance you can have everything on your head, you'll
forget completely after a few years, and also unless you have some
paper documentation there aren't any helpful comments!
I would have used an assembler program if these were readily
available, but I had none until I wrote mine some years after. The
software stores in Mexico were scarce, also I never could find
something so esoteric as a Z80 assembler program when the IBM PC was
already the dominant machine.
However, my objective of developing a game in 2K RAM was met.
Students were surprised a real game could work on their computer. I
think I distributed a few copies as printed sheets with the binary
and another few copies in floppies.
I learned as I developed the game, and I didn't make again the
mistake of allowing the player to walk into the void. But still for
many years, I kept coding in machine code and doing spaghetti code
when I needed to insert code, but that is a history for another day.
Related links
* My first game coded in Z80 assembly language.
* Input MSX magazine issue 12.
* Mention of Viboritas in Hackaday!
* The modern version of the course still being taught by my father.
Last modified: Feb/08/2024