Commodore 64 · 1987 · Ocean

Wizball

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.

Sensible Software's shooter, read from its code: how a ball you can only spin gets anywhere, and why the game crashes itself when its clock stops.

01 · One frame, rebuilt from memory

Twelve raster bands a frame

The play screen is several screens stacked. A raster interrupt at $7C73 runs twelve short routines on the way down every frame, and each one points the video chip at a different screen, character set or group of sprites.

Choose a band to light the lines it controls.

Drawn by this page from a snapshot of level 1 in play: the screen at $E000 and colour memory, the character sets at $F000 and $F800, the sprites, and the video chip's registers and sprite pointers as each band left them, read at every interrupt of one frame. It matches the emulator's picture of the same frame in all but 13 of its 104,448 pixels. Twelve of those are stars, which twinkle between the frame shown and the moment memory was read; the thirteenth, on line 239, is not explained.

The list of bands lives in memory as data ($B3E2 in play): a first raster line, then four bytes a band, the routine's address and the line of the next interrupt. The interrupt writes each routine's address into the operand of its own JSR at $7C9D and calls it. Other screens install other lists through $7CB4: the title, the player menu, name entry, "get ready!" and the three Wiztips pages each have their own.

The icon bar at the top and the cauldrons at the bottom sit in the border, where a C64 normally shows nothing. At line 247 the band $8BBB switches the screen to 24 rows: after the line where a 24-row screen starts its bottom border, and before the line where a 25-row screen does, so the border never switches on. The band at line 65 puts 25 rows back. Sprites show in the open border, and that is what the icons and cauldrons are.

Sprite 0 appears three times in one frame. From line 48 it is the beam's upper arc, at two lines above the Wizball it becomes the Wizball, and 20 lines further down it becomes the beam's lower arc. The band $8A52 writes those two raster lines into the list every frame ($B3F6, $B3FA), so the bands follow the ball up and down.

02 · The sound

Martin Galway's tunes, from the stored bytes

Nine tunes and 26 sound effects share the SID's three voices. A music mode ($B8F1) says which voices the music has: none, voice 3 only, or all three; the effects get the rest.

A port of the driver ($45BD-$5524, with the two routines in the tune data that command $D6 calls and the clock at $B8F3), started as the game starts each tune, playing through the site's model of the SID, which leaves out the filter. It was checked against the game's own code run in a 6502 simulator: all 25 registers after every frame, and the order of the writes within each frame, for 3,000 to 22,000 frames of each tune, past every loop point, and for 400 random tunes that use every command of every voice.

The driver ($4553-$5524) keeps each voice's track as a list of notes and commands with a call-and-repeat stack five levels deep. Each voice has a 29-byte instrument (voice 1 at $87, voice 2 at $4680, voice 3 at $A4): four pitch steps or an arpeggio, a two-step pulse sweep, gate and release times. The notes come from a table tuned for PAL at $471A. A command can call machine code stored among the tune data, which the tunes use to set up the filter. The voice 2 and voice 3 code switches a note off and on again within one frame, so the attack restarts at every note; a model that heard only the registers at the end of each frame would lose it.

The two bonus-stage tunes take turns: the first bonus stage after loading plays one, the next plays the other, and $BB0C keeps the turn from one game to the next. The Wiz-lab has its tune only after the second of the two ($AB66); after the first it is silent. Every command byte some voice understands is used by at least one tune, and the notes run from the eighth entry of the table to the ninetieth.

The band $B90C runs the sound once a frame in every display list, and three more bands ($B903, $B8FC, $B8F3) run the music again when its speed says two or four ticks a frame.

03 · The Wizball

A ball that changes speed only when it bounces

The joystick does not move the Wizball. Left and right change its spin, and the spin becomes rolling speed only at the moment the ball touches the ground. Thrust, the first icon, makes the spin drive the speed on every frame.

Hold a button, or the arrow keys once the picture has been clicked. A port of bounce_wizball ($7F62), read_spin_control ($80F8) and spin_to_roll_speed ($818E), drawn with the game's 16 rotation sprites. It was checked against the game in the emulator: over 370 passes of the frame loop, with the stick moved left and right, spin, rotation, speed, height and vertical speed agree on every pass. With thrust they agree for 56 passes, until the real ball hits a wall, which the port leaves out.

Each frame the stick adds or takes 6 from the spin ($B21C), up to 48 either way, and the spin turns the ball through its 16 rotation frames. When the ball lands, $818E turns the spin into one of 13 speeds: below 4 the ball stops rolling, and each further 8 of spin moves it up one step.

Gravity adds 1/16 of a pixel a frame to the falling speed ($B27C), and the bounce at line 200 sends the ball up at the speed it came down at, so it bounces for ever at the same height: in the emulator, once every 128 frames, rising 126 lines. With anti-grav, the second option of the first icon, $8BEA replaces all this: the stick moves the ball up and down directly and it stops bouncing. Only with both thrust and anti-grav does a spin die away when the stick is let go.

04 · Controls

Nine keys in play, and a joystick chosen by its fire button

The game scans the keyboard itself, twice over: all 64 keys on the title and menu screens, and only nine during play.

The keyboard matrix: rows are the lines the game drives low through $DC00, columns the lines it reads back through $DC01. The small number is the code the scan gives a key. In play only rows 7-5 and columns 7-5 are read.

The play scan's nine keys are RUN/STOP (pause), Q (quit, while paused), C=, /, up-arrow and = (firing volume), comma, @ and colon. They sit in columns 7 to 5 of the matrix, the three lines a joystick in port 1 cannot pull low, so the port-1 stick can never be read as a key.

Neither port belongs to a player. On the "get ready!" screen before every life, $6965 notes which port the fire button was pressed on, and that stick drives the Wiz until the life ends ($B240); the other stick drives the cat. In a one-player game with the catelite, holding fire for ten frames hands the stick to the cat while the Wiz fires by itself.

05 · Landscapes

Eight landscapes in one map

All eight levels are one map of 16 rows by 96 tiles at $0532. Four rows make a landscape's height, and each band of four rows holds two landscapes side by side.

Each tile is 5 × 5 characters from the table at $2642, and each level brings its own 95 glyphs, copied to $FD00 by $77C6 from the block the table $B31C names. Drawn from the game's bytes; the sky's stars are plotted by the game while it runs and are left out here. Checked against the game: for two stopped frames of level 1, every one of the 445 scenery characters on screen is the character this drawing puts there.

The top strip is the whole map, one square a tile, with each level's landscape outlined. Columns 0-44 of a band are one landscape and columns 45-89 another; each opens and closes with the same two tiles. Columns 92-95 hold halves of two pictures, the Wiz-lab and the one shown after the last level: the drawing code ($75DF) jumps four rows down and four columns back after four tiles, so two half-pictures in different bands make one screen.

The view scrolls across one landscape at up to 2 pixels a frame. The game keeps three copies of the landscape screen at $E000, $E400 and $E800: the one on show and two made ready one column to the left and right, a quarter of each rebuilt every frame ($77F7). A coarse step of the scroll is a switch of screen, not a copy.

06 · Colour

Three cauldrons, and a fourth that keeps score

A level is finished when three colours have been made. Each colour has a recipe of red, green and blue units, and the fourth cauldron at the bottom right shows how much of the recipe the three cauldrons hold.

A port of the mix at $9E2C: for each of red, green and blue, the smaller of the cauldron and the recipe ($B2C0, three bytes for each of the C64's 16 colours), added up into $B12E. Checked against the game's own code run in a 6502 simulator on 6,400 random cauldron states across the 16 colours: they agree on every one.

The colour sphere's droplets come in the level's own colour: red on levels 1, 4 and 7, green on 2, 5 and 8, blue on 3 and 6 ($B18B). Only the cat can catch them. More units than the recipe needs are wasted, so filling red to 20 on a purple level gives half the mix.

The game does not compare the mix with 20. $9E81 draws the fourth cauldron's sprite a row a frame, and $9E26 copies one byte of its top row into $B874. When that row is drawn full the byte is $AA, and the frame loop leaves for "colour completed". In the emulator, with the red cauldron set to 20 on level 1, that took 22 frames.

Light blue's recipe is the same as purple's, 10 red and 10 blue ($B2C0 + 3 × 14).

Colour completed: the text over the bonus stage's first wave

Level 1's first colour done: "colour completed" over the bonus stage's first formation, the red cauldron full.

07 · The aliens

Eighteen kinds of alien, all worth 50

Aliens come from two tables of templates: 18 for the landscapes at $3ECF and 20 for the bonus stage at $39DC. Six at most are on screen, one in each of six 42-byte slots at $0200.

Each template's frames, drawn from the sprite data at $C000-$DFFF and animated as its frame list says; the points are byte 22 of the template times ten. These are the templates as loaded: for every list it builds, the game rewrites the landscape ones in place, setting each type's shot interval from the level's entry in $A12A, the colour spheres' shots by level ($A027), and sometimes turning a faller into a riser ($A070). The spheres take their colour from the level, drawn here in red.

$9DA1 copies 23 bytes of a template into a slot and then applies its random fields, each a mask, an offset and the byte to change, so two aliens of one kind start in different places. A landscape's aliens come from a list of (column, type) pairs made for it when it opens ($9F9B): groups of 4, 6 or 12 of one type, spawned as the view uncovers their column. Only the highest level open and the two below it get lists. Clearing a list pays 1,000 points and makes a new one.

Every landscape alien is worth 50 points, where the manual says 10 to 500. The ninth alien killed since the last pearl leaves a pearl, and every molecule does ($A9AA = 9; colour spheres do not count). The reviews say eighth.

Kill nothing for long enough and the police come: after $A1B9[level] seconds without a kill a police ship flies in, with a warning tune a second earlier. In the emulator, on level 1, the tune started at frame 2,200 and the ship at 2,250.

08 · Difficulty

What changes from level to level

Four tables set a level's difficulty. Level access is a fifth rule: a level opens only when every level three below it and lower is done.

From $A12A (frames between an alien's shots), $A1B9 (seconds without a kill before the police), $A027 (whether colour spheres shoot) and $B1D1 (the three colours, in the order they replace the greys).

The highest level open is $B1CE = 2 + the number of levels done, counted from level 1 with no gap ($A9C6). The manual says level five opens when level two is done; in the code it also needs level one. Tubes lead up only as far as $B1CE. After all eight levels the game starts again from level 1 with every alien firing every 10 frames.

09 · Secrets, quirks and bugs

Things the manual never told you

Secret: stop the game's clock and it crashes

The initialisation sets CIA 2's timer A to count 19,656 cycles ($4CC7 + 1), exactly one PAL frame, and makes it the NMI. The NMI handler ($7C61) only clears $85. Late in each frame the band $8BBB stores a non-zero value there, and early in the next the band $8A52 ends with a check ($8B04): if $85 is not back to 0, the NMI has not come, and the check pulls a byte off the stack so that the band's return goes astray. Anything that stops the timer or its interrupt sets it off.

AddressBytesInstructionWhat it does
$8B04A7 86LAX $86A and X = $FF, set once by the initialisation
$8B06DF 86 FFDCP $FF86,X$FF86 + $FF wraps round to $0085: decrement it and compare with $FF
$8B09F0 03BEQ $8B0Eit was 0: the NMI came, return normally
$8B0B68PLAotherwise lose a byte of the return address

Both instructions that touch $85 are undocumented opcodes, and the address wraps round the top of memory, so a search of the code for $85 finds only the writes. In the emulator, with CIA 2's interrupts switched off, the check failed on the next frame and the processor ran into screen memory and stopped on a JAM opcode at $EA7D, in the landscape screen.

The title screen has its own version, with an INC $FFB0,X that also wraps round to $85, and a failure path ($9292) that increments a random byte of memory first. One more disguised decrement, at $B589, is LXA #$FF followed by DCP $FF97,X. LXA is an undocumented opcode whose result depends on a constant that varies between 6510 chips; only with the constant $EE does X come out as $EE and the address wrap to $0085. The VICE emulator fixes the constant at $EE with the source comment "needs to be 0xee for wizball". The start-up hides as well: the initialisation reaches CIA 2 only through indexed addresses (STA $DC8E,X with X = $7F is $DD0D) and then overwrites itself with random bytes. The menu and "get ready!" screens compare the hardware vectors with their stored copy every frame ($90F8), flickering the screen if they differ.

Bug: level 5 draws a tile that does not exist

The tile table has 192 entries, but one byte of the map, at $092B, holds tile number $F8 (248). The drawing code does not check, and reads the tile's 25 characters from $2642 + 25 × 248 = $3E7A, the middle of two sound-effect records. Most of those bytes are small numbers, which draw as sky, but two are $FF, and glyph $FF of the play character set is the last eight bytes of memory: two unused bytes and the NMI, reset and IRQ vectors. The result sits just to the right of level 5's first well.

The stray tile beside level 5's first well, enlarged

Left: the emulator's screen on level 5, enlarged. Right: the tile's 5 × 5 bytes from $3E7A, drawn with level 5's character set; a 0 is sky, and the star glyphs $01-$5A show whatever stars the game has plotted into them. Remember's 1997 crack lists "matrix in level 5" among the bugs it fixed, and changes exactly this byte to $00.

Bug: a shield survives quitting the game

The shield timer ($7F06, 40 units, one used every 50 frames) is cleared only when it runs out and in the Wiz-lab. Quitting with Q while paused does not touch it, and nor does starting a new game, so a shield that was running carries into the next game, while that game's icon bar offers the shield as unused. In the emulator: 38 units at the quit, 36 when the new game began, and a collision in the new game cost 8 units and no life. Remember's crack names this as its second fix.

Secret: BOREWIZ, and starting on a later level

On the title or the player menu, the band $AF85 compares each key pressed with the letters B O R E W I Z ($AFCE, in the game's key numbering). After all seven the border turns brown and $BB07 is set: collisions stop ending lives, except during "colour completed" and the bonus stage ($7EF4). The letters only have to come seven in a row, which is why Remember's notes can give it as WIZBOREWIZ; a doubled B breaks it, because a wrong key resets the count without being tried as a new first letter. The same band reads the keys 0 to 5: key n starts the next game on level n + 1, as far as the first level the last game did not finish ($AF7D), with the skipped levels marked done and up to five of thrust, anti-grav, beam, catelite and spray given for free ($B10D). Wiztips calls it the continue game feature.

The spray icon Remember's crack "fixed"

Remember's third fix is "the wrong sprite on the spray icon". Every icon in the bar shows what taking it next would give: after thrust is taken the first icon shows anti-grav, after the beam the second shows double. The spray icon follows the same rule, and after the Wiz takes the spray it shows the cat spray, a cat's face with dots very like the catelite's icon. Remember's version of the handler ($7325 here) swaps the two images, so its icon shows who holds the spray instead.

A hang in the Wiz-lab that no player can reach

The lab keeps up to nine permanent weapons ($B116). When a tenth is taken, $B0B0 frees the first two slots holding the spray and tries again; with fewer than two sprays among the nine it jumps to itself for ever ($B0D2). In the emulator, with nine other weapons put in the list by hand, choosing the spray froze the game there. A player never gets that far: thrust, anti-grav, beam, double, catelite and blazers each turn their icon to the used-up shape once taken, so at most six slots can hold anything but the spray.

An earlier build left in memory

The image carries pieces of an older version of the game. After the initialisation sits a second copy of its last part ($ECB4) with different addresses, never run. $BB0D holds an earlier draft of the third Wiztips page, spelling "permanent" correctly where the real page says "permenant". $6EE4, just after the quit code, would mark the current level's colours done and jump into the level-complete sequence, and the test in front of it for the Q key branches to the next instruction either way: a debugging key, disconnected. Tests for three more keys ($9E5A) branch nowhere too. And the name-entry buffer at $BFF4 still holds "POO BAG", where a team's two sets of initials go; the game fills the buffer with spaces before every entry, so nobody sees it.