1990 · Ocean

Darkman

This minisite awaits a maintainer’s check. A model this site has not proven yet worked on it (claude-sonnet-5-5), so a maintainer will measure its listing on one it has (how the check works).

Ocean's game of the Sam Raimi film, written by Twilight. Each of its six levels is a program of its own, loaded from disk after a screen that asks for fire.

01 · Overview

Credits, then the streets

Once the first level has loaded, the title alternates between two pages: the credits, which name Finlay Munro for the code, Noel Hines for the graphics, and Jonathan Dunn for the sound, and a table of the day's top scores. Fire on the joystick in port 2 starts play.

The Darkman logo above the licence lines, the credits, and the Ocean logo

The credits page of the title screen.

The Darkman logo above a table headed Todays Top Darkmen, with five names and scores

The other page of the title: TODAYS TOP DARKMEN.

The first level is a street scene with a brick wall and shop signs, where Darkman fights gangsters in suits. A panel under the play area holds the score, an energy bar, and a portrait of Darkman. The Maps / levels tab draws all six levels from the bytes of their files.

Darkman in a long coat between two gangsters on a street, above a status panel with a portrait

Level 1, a few seconds into play.

02 · Loading

Six levels, one at a time

A small loader stays in memory at $0400 for the whole game. Before each level, the routine at $0625 puts up a plain text screen and waits for fire. Before level 1 and level 4, the screen says which side of the disk to insert, and before the other levels it reads PRESS FIRE TO LOAD NEXT LEVEL.

Black screen with the words Insert disk side 1, then press fire

The prompt before the first load.

The loop at $0407 then loads the two files of the level whose number is held at $0435, retries after a disk error, adds one to the number, and jumps to $0A20. Each level's first file starts at that address, and its second fills $BF00 onwards, under the chips and the ROMs. Coloured stripes run across the screen while a load is under way. A level ends by jumping back into the loader: levels 1 to 5 jump to $0407 when they are done, and the levels after the first use $0404, which starts the index again at 0, after a game over.

The sound driver is never loaded as a file of its own. Level 1 carries two copies of it, one for the title tunes and one for the sound effects, and copies the second to $B0C8, in a stretch of memory that no later load writes. Every later level calls that copy. The Music tab plays both.

Yellow text on black: Level two. Darkman is discovered before his plan is half grown

The briefing before level 2.

Darkman on a ledge by purple pipes in a brick room, above the status panel

Level 2 in play.

03 · Controls

Walk, jump, crouch, and strike

The joystick in port 2 does everything. Left and right walk, four pixels every four frames. Up jumps, and Darkman rises 26 lines in 32 frames. Down crouches. Fire alone throws a punch, and fire with down throws a low strike that carries him forward ($2979).

The third raster interrupt, $1D43, copies the joystick to $203B once a frame, except while the stun timer after a hit is running. The keyboard is read once a frame by $0E30, which leaves an ASCII key code in $0E2F. The P key gives $50, and the routine at $1777 then toggles the pause flag at $1EF2, so a second press of P resumes play. Two more keys do something only after a secret code has been typed: the Discoveries tab has it.

04 · One frame, rebuilt from memory

A text screen over a bitmap panel, with eight black lines between them

Level 1's play screen is a text screen with the street in it, over a bitmap with the panel in it. Four raster interrupts switch the video chip from one to the other every frame. The picture below is drawn by this page from the bytes in memory and the writes the game's own interrupt handlers make to the chip.

    Level 1 in play, drawn from memory. The character set, screen matrix, colour RAM, sprite shapes, sprite pointers, and panel bitmap are the play snapshot's bytes, 4,689 of them. The register values, and the raster line and cycle each was written on, come from running the game's four interrupt handlers on the kit's 6502 simulator for the first whole frame after the snapshot; test_frame.js runs the handlers again on the listing and compares every write. Against the emulator's screenshot of level 1, 2,467 of 104,448 pixels differ. The screenshot was saved at a different moment from the snapshot. The men stand in other places, and the energy bar is longer: in the snapshot Darkman holds 4 of 33 energy points and is drawn white because he has just been hit, and the bar's eight cells have 76 pixels that are not black where the screenshot's have 376. So 2,166 of the differing pixels lie on lines 120 to 162, where the sprites are, and 300 lie in the energy bar. The last pixel, at the right end of line 198, is the light grey dot that the emulator draws where a colour register is written. This drawing leaves out the dot, and puts it 24 pixels to the right of its own write to the border colour. The other 101,981 match, including every pixel of the black strip and of the panel outside its energy bar.

    From the top of the frame to the bottom, the video chip is in four states. The interrupts change them, and the raster lines in the first column are read from the registers of the recorded frame, not from the code.

    Raster linesSet byModeWindowScreen matrixCharacters or bitmap
    0 to 9Interrupt 4 of the frame beforeMulticolour bitmap25 rows$CC00Bitmap at $E000
    10 to 198Interrupt 1Multicolour text24 rows$CC00Characters at $C000
    199 to 206Interrupt 3Invalid mode, drawn black25 rows$CC00None shown
    207 to 311Interrupt 4Multicolour bitmap25 rows$CC00Bitmap at $E000

    Every band is 38 columns wide. The picture starts on line 16, so the first band and the first 6 lines of the second lie above it. The display ends on line 250 in the last band, and the bottom border starts on line 251.

    The strip shows nothing from memory. Interrupt 3 sets extended colour mode in $D011 while multicolour in $D016 is still on, a combination the video chip does not define, and the chip draws black for as long as it lasts. The strip covers the eight lines 199 to 206. Inside it the game changes $D018 from the character set at $C000 to the bitmap at $E000, and interrupt 4 adds bitmap mode near the start of line 206 and removes extended colour mode near its end, as the simulator counts the cycles, so that the first line of the panel is 207. The code does not say what the strip is for.

    The vertical scroll of 7 that interrupt 3 writes puts the first line of each character row on lines 199, 207, 215, and so on, where the play field's rows begin on multiples of 8. The last row of the play field is cut off after seven lines for that reason, and the panel's first row, row 19 of the matrix at $CC00, begins on line 207. The panel's colours are that matrix and colour RAM; its pixels are the bitmap at $E000, eight bytes to each matrix cell.

    The sprites are the part of the screen that is not split. Every write to a sprite register or pointer in the recorded frame falls between lines 11 and 56, apart from the two shared colours on line 198, and the first sprite line is 120. Interrupt 3 changes the two shared sprite colours to black on line 198, below the last sprite line. No sprite is moved after it has been drawn, so the frame does not multiplex them: eight sprites cover the whole screen. Sprites 0 and 1 are Darkman, one above the other, drawn over sprite 2, which is also Darkman and twice as tall. Sprites 3 and 4 are one enemy, a pair stacked in the same way, and sprites 5 and 6 are the other. Sprite 7 is the exit marker, and while an enemy is on the screen the game puts it on line 1, under the top border, where none of it shows.

    05 · The four interrupts

    Four raster interrupts draw the screen and run the game

    Each interrupt writes the line of the next one to $D012 and the next one's address to the vector at $FFFE, so the four run in a ring: on line 10, line 80, line 198, and line 205, and then line 10 again.

    InterruptRaster lineHandlerWrites to the screen registers, in orderSprite writes
    110$1E21$D011=$10 $D016=$D0 $D011=$10 $D018=$30 $D012=$5038
    280$1EF4$D016=$D0 $D022=$0B $D023=$0F $D012=$C60
    3198$1D43$D018=$30 $D011=$5F $D020=$00 $D012=$CD $D018=$382
    4205$1DBF$D011=$7F $D016=$D7 $D020=$00 $D012=$0A $D011=$3F0

    The writes of the recorded frame, without the acknowledgement of the video chip's interrupt in $D019. Sprite writes are those to the sprite positions, enables, expansions, colours, and pointers.

    Interrupt 1, $1E21, sets the text screen and then runs one step of the game: Darkman's move ($265E), the exit marker ($354A), the enemies ($3725), the objects ($3297), the shot ($4140), and the tests for touching an exit or an object. Along the way the handler places all eight sprites, so that every sprite register is set before the first sprite line.

    Interrupt 2, $1EF4, plays the sound ($B176) and reads the keyboard ($0E30). The handler also writes the scroll in $D016 and the background colours in $D022 and $D023, and moves sprite 0 or sprite 1 eight pixels sideways when the flag at $2768 or $2F67 is set ($1F40). In the recorded frame the registers already hold the values it writes, so the screen is the same on both sides of line 80. The code does not say why they are written again.

    Interrupt 3, $1D43, begins the switch to the panel. The handler sets extended colour mode, 25 rows, and the vertical scroll of 7 in $D011, and it blacks the border and the sprites' shared colours. Interrupt 3 also counts down the stun timer ($29F3) and the hit grace counter ($29F4), and reads joystick port 2 into $203B unless the stun timer is still running after a hit.

    Interrupt 4, $1DBF, finishes the switch. A run of 17 NOPs holds back its first write to $D011, so that, in the recording, the write falls early in line 206. The handler then points the vector back at interrupt 1 and, unless the game is paused, erases the shot ($41EF), tests the shot against Darkman ($4636), and scrolls the play field ($2066). All three run after the last line of the play field has been drawn.

    The main loop at $179E is the only code that runs outside the interrupts. Each pass redraws the panel's energy bar ($4449) and the seven figures of the score ($44FD), checks for the space bar ($1A0C) and the pause key ($1777), and then the loop waits for the flag that the NMI at $2030 sets. The loop leaves for $4A8F when $1A1E is set, and for the game-over code at $195C when the flag at $0A11 and the energy at $43E3 are both zero.

    06 · Enemies and pickups

    Seventy objects, eleven slots

    Levels 2, 4, and 6 keep their enemies, pickups, and hazards in one list of objects, 84 in level 2, 73 in level 4, and 70 in level 6. Each frame scan_objects ($0F0F in level 4) walks the list from the last object to the first. An object that comes within the camera's window, from 26 pixels left of the camera to less than 346 pixels right of it and between lines $14 and $D6 below its top, takes the first free one of 11 slots. An object that leaves the window gives its slot back.

    Every slot has a state, and update_objects ($117E) runs the behaviour for that state through a table. The behaviours are routines with names that say what they do: behave_attack_left and behave_attack_right, behave_walk_left and behave_walk_right, behave_vertical_patrol, behave_radial_shot, behave_lobbed_shot, behave_flashing_hazard, behave_energy_pickup and behave_score_pickup. Before a behaviour runs, player_hit_test ($101C) tests the slot against Darkman. Level 6 adds a map switch (behave_map_switch, $13AF) that, on contact, writes zeros over four cells of the level map, and an area trigger ($1F6A) that, once the player passes a map position, writes 3 into the high byte of the height of objects 41 to 44.

    07 · The mask game

    A scan cursor, twelve slices, and a timer

    Levels 1, 3, and 5 end in a game of their own, which the story text calls preparing a disguise. The main loop of level 3 jumps to mask_main ($4286) when the level-done flag is set, level 5 does the same through mask_screen ($29F1), and level 1 reaches it through $4A8F. A raster interrupt chain of three handlers ($5177, $51DB, $5242 in level 3) runs it.

    The player moves a cursor with the joystick in steps of 8 pixels inside a window (cursor_move, $49E9) while a three-digit timer counts down (mask_timer_tick, $535D). Each press of fire captures a 3 by 3 block of characters (fire_capture, $46C5), and match_check ($475F) tests whether the cursor stands on a picture's place. Twelve captures, or an empty timer, end the game, and scan_cursor ($48BA) then walks the cursor along a fixed scan path, sounding when it reaches a slice that matched. mask_report ($445F) prints MASK COMPLETE or INSUFFICIENT DATA TO GENERATE MASK, and a dissolve ($448C) erodes the captured picture in 30 rounds, each ANDing every byte of a 256-byte buffer with one of four random masks. Five objects slide in and out of the picture as the game runs (objects_run, $4BB9).

    The three mask games are the same routines at different addresses.

    08 · The chase

    Level 5 is a script, not a map

    Level 5 has no scrolling map. The player hangs on a rope under a flying machine, sprite 3, and swings with the joystick (player_swing, $126D) while four walls, flying obstacles, and the shots of a chopper cross the screen. The chopper, sprites 0 to 2, follows a script of four-byte entries: a step in X, a step in Y, a number of frames, and a flag that makes it shoot (chopper_step, $1417). The walls move down and up along their own rows (wall_move_down, wall_move_up), and a time bar runs down (time_bar_step, $0B0A). When a collision comes after the bar has run out, finale_step ($0A7B) fills the screen with a closing pattern, row by row, and the level ends in the mask game.