Commodore 64 · 1987 · Ocean

Wizball: the source

This minisite was contributed by air. It’s agent-generated and needs a human editor. Clone https://github.com/gamesexplained/gamesexplained and follow kit/START.md to curate games/c64/wizball with me to Gold.

Every routine, table and variable the game uses, with the names and descriptions given to them. Click an operand to follow it; the counter beside a label lists what refers to it. The reference below the fold is rendered from the game's facts file.

Technical reference: memory map, timing, tables, cheats

Current truth for this game. The workflow lives in kit/skills/; how this understanding developed lives in agent-history.md. Every fact names the routine or table it comes from. Unless marked live, a fact comes from reading the code in the image named under Build. The live tests are listed at the end.

Build

Memory layout

The loaded image fills $0800-$FFFF; the initialisation adds pages 0 to 3. Code is identical in the entry image and in play except for operands the code rewrites; the game builds its screens, the play character set's star and level glyphs, and the title sprites at run time.

ThingWhere
Entry$6389: $01 = $35, then JMP $EC00
Initialisation$EC00-$ECB3, inside what becomes screen memory; ends with JMP $8F61, the title
Outer game cycle$639B-$63C7, JMP $639B: one turn (one life) a pass
Play routine and frame loop$63E0; the loop top is $640E
Raster IRQ handler$7C73
NMI handler$7C61
Display-list installer$7CB4, list address in X (low) and Y (high)
Random numbers$7533: index $7532 + 1, returns code byte $7300 + index exclusive-ORed with CIA 2 timer B's low byte $DD06
Music and sound driver$4553-$5524, command tables $4600, $462A, $4654, tune data $5525-$62D8; effect player $B960-$BA7A, effect table $BACA
Alien slots$0200-$02FB, six of 42 bytes, slot k on sprite k + 2
Alien bullets$0300-$03FF, sixteen of 16 bytes
Shots$0480-$04C7, six of 12 bytes (Wiz 0-1, cat 2, spray 3-5)
Sound-effect slots$0400, $0420, $0440: voices 1, 2, 3
Landscape map$0532-$0B31: 16 rows of 96 tile numbers; row addresses at $0460/$0470
Level glyph blocks$0B42-$2641: nine blocks of $300, addressed by the word table $B31C
Tiles$2642-$3901: 192 tiles of 5 x 5 characters
Bonus-stage script$3902 (copied to $ED02 to run)
Alien types$39DC (20 bonus-stage types) and $3ECF (18 landscape types): word tables of 24-byte records
Sound effects24-byte records from $3C5F
Wiztips pagestable $4199-$41A4, pages $41A5-$4552
Variables and tables$B000-$BFFF (the per-side block $B116-$B12D, the scores $BFDA-$BFE7)
Sprites$C000-$DFFF, blocks 0-127; $D000-$DFFF (blocks $40-$7F) is the RAM under the I/O area, which the video chip reads and the CPU never does
Screens (bank 3)$E000, $E400, $E800: the landscape, three buffers; rows 0-1 of $E000 are the score line. $EC00: the title, menu, Wiztips and get-ready text; in play only its sprite pointers $EFF8-$EFFF are used (the icon bar)
Character sets$F000-$F1FF small font, $F200-$F7F7 large font (both loaded, stored inverted); $F800-$FFF7 the play set: glyph 0 blank, $01-$5A star glyphs drawn at run time, $5B-$7F small blocks, $80-$9F ground textures, $A0-$FE the level's glyphs copied to $FD00 by $77C6, $FF the six hardware-vector bytes

$77C6 copies the first 760 bytes of the level's block to $FD00-$FFF7, stopping before the vectors; in play-start.vsf (level 1) $FD00-$FFF7 equals the entry image's $1142-$1439.

Start-up and anti-tamper

Timing

The display list

$7CB4 stores a list's address in $68/$69. A list is a first raster line, a zero, and then 4-byte entries: the routine to run in this band (low, high), the raster line of the next interrupt, and a zero. The IRQ at $7C73 reads the entry at index $B1CD, writes the routine's address into the operand of the JSR at $7C9D, sets $D012 to the next line and calls it. An entry whose next line is 0 ends the list: the handler sets the index back to 2 and $D012 to the first line, and the list repeats every frame.

ListInstalled byBands (routine, then next line)
$B3E2$63B5, pointer at $B414$B90C $14, $89BB $30, $8A52 $3F, $B903 $41, $8A25 $46, $8B0F $50, $8B22 $E5, $B8FC $E8, $8BCB $EE, $8B46 $F7, $8BBB $FF, $B8F3
$B416$8F89, pointer at $B450$AF85 $16, $B90C $23, $922C $64, $B903 $6B, $92A2 $93, $9327 $A0, $936A $AD, $B8FC $AF, $93A9 $C0, $93B0 $D0, $93B7 $E0, $93BE $EE, $B8F3 $F0, $93C5
$B452$9020, pointer at $B480$AF85 $16, $B90C $48, $93CC $60, $93D9 $6A, $B903 $78, $93E3 $90, $93ED $A8, $93F7 $B2, $B8FC $E6, $9419 $FF, $B8F3
$B482$6BA4, pointer at $B4B0$B90C $2C, $6DD5 $4E, $6DDC $6A, $B903 $8E, $6DE3 $A6, $6DEB $B2, $B8FC $B6, $6DF2 $CF, $6DF9 $E6, $6E00 $FF, $B8F3
$B4B2$68A3, pointer at $B4C4$B90C $64, $B903 $B2, $B8FC $FF, $B8F3
$9549, $957B, $95A1$9469, from the word table at $B86E$9681, $9703, $970C, $972F, $971A, $9750, $9721, $9728; $9762, $97C2, $97CB, $97D2, $97D7; $97F0, $97FC, $9805, $9812; each also runs $B90C, $B903, $B8FC, $B8F3

$B90C, $B903, $B8FC and $B8F3 run in every list: they are the sound driver's clock (Sound, below). $B416 is the title and high-score page, $B452 the player menu, $B482 name entry, $B4B2 the get-ready screen, and the last three the Wiztips pages.

The play screen (list $B3E2), top to bottom:

From lineBandShows
$14$89BBthe icon bar: seven sprites in the opened top border, pointers $EFF8-$EFFE of screen $EC00 ($D018 = $B0)
$30$8A52rows 0-1 of screen $E000 in the small font ($D018 = $8C): 1UP, HI, 2UP and the scores; copies the VIC shadow $E7C4-$E7F2 into $D000-$D02E; ends with the watchdog check $8B04
$41$8A25the landscape: screen $E000, $E400 or $E800 by $B1FD, characters $F800, multicolour, 38 columns, $D016 = 7 - ($B1F2 AND 7)
rewritten each frame$8B0F, $8B22sprite 0 moved to the Wizball (line Y - 2, list byte $B3F6) and to the beam's lower arc (line Y + 20, $B3FA)
$E8$8BCBblack below the landscape
$EE$8B46the panel: the level digit (sprite 0, block $A0) and the four cauldrons, pointers $F7F8-$F7FF ($D018 = $DC)
$F7$8BBB24 rows after line 247, so the border never closes: the icon bar and the panel show in it

Text

Hardware register census

Every absolute access in the traced code, by register (routine addresses are the accessing instruction).

RegisterAccessWhereWhat
$D000-$D007, $D010write$8A73-$8BD3, $9183-$91BA, $9256-$937D, $94F8-$9519, $89C6-$89EAsprite positions, per band
$D011read and write$681D, $6E71-$6EAF, $7CC8-$7CCD, $8A3A-$8A41, $8BBB-$8BC2, $8C47-$8C5A, $8F43, $EC4Draster bit 8 in the waits; 25 rows at $8A3A and 24 at $8BBB (the open border); blanking at $8C47-$8C5A; vertical scroll always 3
$D012read and writewaits at $6824, $6951, $6E4F-$6EA8, $9088, $EC54; set by the IRQ $7C95, $7CAF, $7CC5; compared at $926C-$973Draster
$D015write$6895, $68EF, $89EF, $8A5A, $8B88, $8F5B, $9052, $909C, $9234, $92C1, $9366, $947E, $94B3sprite enable, per band
$D016write$8A37, $8B61, $8F48the landscape's fine scroll, multicolour, 38 columns ($8A37); 40 columns for the panel ($8B61) and the text screens ($8F48)
$D017, $D01B, $D01C, $D01Dwriteper band and per screensprite expansion, priority, multicolour
$D018write$68B9 ($BC), $89E5 ($B0), $8A2A, $8A54 ($8C), $8B5C ($DC), $8F4D ($BC), $92A9 ($BC), $931D ($BD)$BC: screen $EC00, characters $F000; $B0: sprite pointers from $EC00 (no characters shown); $8C: screen $E000, characters $F000; $8E/$9E/$AE ($8A2A): the landscape buffer, characters $F800; $DC: sprite pointers from $F7F8
$D019, $D01Aread and write$7C74-$7C7B, $7CD2-$7CD5raster interrupt acknowledge and enable
$D020, $D021writemanyborder and background, per band
$D022, $D023write, increment$64EB, $64EE, $652F, $6534, $7D4D-$7D5A, $8A0F-$8A21, $AF5Fmulticolour character colours
$D025-$D02Bwriteper bandsprite colours
$D400-$D406write$47D4, $4DA0-$4DC1, $5128-$524C, $BA3E-$BA77, $B96ASID voice 1
$D407-$D40Dwrite$4EC3-$4EF8, $529D-$53BASID voice 2
$D40E-$D414write$5012-$5043, $5402-$54F7SID voice 3
$D415, $D416write$4D13, $4D23filter cut-off
$D417write$45CA, $49CB, $4C38, $7DC3filter resonance and routing
$D418write$49D2, $4C44, $B916, $B949volume and filter mode
$D3FF,Xwrite$BAB1with X from 1, the SID registers
$D7FF,Ywrite$70F0with Y from 1, colour RAM
$DC00-$DC03read and write$73D4-$745Aboth joystick ports and the keyboard, with the data-direction registers switched around the reads
$DC0Dwrite, read$EC10, $EC13CIA 1 interrupts off
$DD00, $DD02read and write$EC9B-$ECABVIC bank 3
$DD04-$DD0Fwrite$EC2C-$EC6C through $DC86-$DCCD indexedthe NMI timer A and the free-running timer B
$DD06read$753Ctimer B's low byte, exclusive-ORed into every random number
$DD0Dread$7C62NMI acknowledge

LDA $DDDD,X at $495C, $4974, $49A6, $4AAC, $4AE0, $4BC9, $4BE1 and $4C16 is not I/O: the music driver writes the real address into those operands before they run.

Controls

Graphics

Mechanics

Data tables

| Level | Band (rows) | Columns | Glyph block | |---|---|---|---| | 1 | 0 (0-3) | 45-89 | $1142 | | 2 | 0 (0-3) | 0-44 | $0E42 | | 3 | 2 (8-11) | 0-44 | $0B42 | | 4 | 3 (12-15) | 0-44 | $1D42 | | 5 | 2 (8-11) | 45-89 | $1A42 | | 6 | 1 (4-7) | 45-89 | $1742 | | 7 | 1 (4-7) | 0-44 | $1442 | | 8 | 3 (12-15) | 45-89 | $2042 |

Sound

| Tune | What | Mode / speed | Started at | |---|---|---|---| | 0 | police alarm (a second before a police ship, and a filth raid) | 1 / 0 | $A17D, $893D | | 1 | bonus stage, every second one | 2 / 1 | $6573 | | 2 | colour completed | 2 / 0 | $6507 | | 3 | title | 2 / 2 | $8F6A | | 4 | bonus stage, the first after loading and every second one after | 1 / 0 | $6573 | | 5 | get ready | 2 / 2 | $6916 | | 6 | name entry | 2 / 2 | $6BC0 | | 7 | game over | 2 / 0 | $6A91 | | 8 | Wiz-lab, only after tune 1's bonus stage ($AB66) | 2 / 2 | $AB75 |

$6553-$6573 flips $BB0C between 2 and 0 at each bonus stage; the image holds 2, so the first bonus stage after loading plays tune 4, and the turn carries from game to game. Tunes 1, 3, 4, 6 and 8 loop; 0, 2, 5 and 7 end.

Corner cases

Each is what the code does; whether a player can reach it is stated.

Live tests

All on 24 September 2026, VICE 3.10 (vice-mcp 3.13.1), from the snapshots in work/; checkpoints are non-stopping counters unless said.

TestHowResult
Frame loop, IRQ, NMI ratescounters on $640E, $7C73, $7C61 over 100 frames99, 1,195, 99
NMI watchdog$7F to $DD0D in play$8B0B ran once; the CPU stopped on JAM at $EA7D, screen frozen
BOREWIZkeys by matrix on the title; then a poked collision $7F0A = 1$BB07 0 to 7; with it a hit is ignored, without it the life ends
Port 1fire on port 1 at the title and get ready, then left and rightthe game started and the landscape scrolled both ways
Pause and quitRUN/STOP, then Q (matrix)the frame loop stopped; Q reached $8F61, the title
Shields after a quittake_shield's writes poked, Q, new gameshield 38 at the quit, 36 in the new game, 30 later; a poked collision cost 8 units and no life
Continue keys$AF7D = 5 poked, key 4 on the titlethe game started on level 5 with levels 1-4 marked done and four permanent weapons
The stray tilelevel 5, view at tile column 54tile $F8's bytes on screen, two $FF glyphs and a $64 beside the first well
Police shiplevel 1, BOREWIZ, no inputtune 0 at 2,200 frames, the ship at 2,250
Pearls and pointsa poked hit on the police shipcounter 0: explosion, counter 1, score +50; counter 8: pearl, counter 0, score +50
Colour completionred cauldron poked to 20 on level 1$B874 = $AA after 22 frames, then $646F: "colour completed", the bonus stage
Wiz-lab listnine non-spray weapons poked, spray chosen in the labthe lab's icons 1-4 showed 9; the CPU stayed at $B0D2
Level variable$B128 poked on the get-ready screenonly the glyphs change: the view is built from the map before the poke
No input on level 1play-start, 150 sthree Wizballs lost, 50 points each time

Cheats

Pokes that change the game within its own parameters. Each one names the variable it changes and whether it has been tested live. Untested pokes are labelled as candidates.

EffectPokeStatus
Collisions stop ending lives, except during "colour completed" and the bonus stage (the game's own BOREWIZ flag)$BB07 = 7 in play, or type B O R E W I Z on the title or the player menulive: the flag set by typing, and a poked collision ignored with it set
Start any of levels 1-6: the continue keys accept up to this level$AF7D = 5 on the title, then press 0-5live: key 4 started level 5 with levels 1-4 marked done
Complete the current colour: the level's first target is red on level 1red cauldron $B12B = 20 in play (green $B12C, blue $B12D; the recipe for each colour is at $B2C0)live: "colour completed" 22 frames later
A shield for 36 seconds$7F06 = 40 (the cat's copy $89B6)live: counted down one unit per 50 frames; a collision cost 8 units and no life
The next kill leaves a pearl$A9A9 = 8live
Wizballs left for side 1 and side 2, the one in play included (the HUD shows one less, up to 9)$B278, $B279candidate: read during play (2 after a first life lost), not poked
routines tables variables strings branch labels

Loading listing…