Commodore 64 · 1984 · Novagen Software

Encounter

This minisite was contributed by air. It’s currently claimed by air who is editing it to reach a Gold tier standard.

A first-person plain of obelisks and saucers, drawn with text where characters change every frame, and the ground is rendered by turning the sky upside down.

01 · One frame, rebuilt from memory

The play screen, drawn from the snapshot

This is a game of spheres bouncing around in apparent real-time 3D. For fans of other Paul Woakes games, we might expect an efficient circle-drawing routine similar to the one rendering the planetfall in his game MERCENARY. It's quite astonishing then that all the circles in Encounter are drawn in text on a standard grid, using custom characters on rows 2-17. Here's a faithfully reconstructed frame rendered with characters, not a screenshot: try clicking cell grid to see how the circle is made.

The picture is character mode from top to bottom. Rows 0 and 1 are the status line in a double-width font. Rows 2 to 17 are the view, shown in multicolour text mode through a character set the game builds every frame. Rows 18 to 24 are the console, drawn once from the same font as the status line. Nothing in the sky or the ground is a pixel: those are background colours changed by a raster interrupt as the beam passes, which is the next section.

The eight sprites are the small things. Sprite 1 is the gun sight, fixed in the middle. Sprite 0 is the radar blip. Sprites 2 to 7 are the horizon hills and the clouds, which scroll against the heading so the horizon turns with the player. The yellow shape at the right edge is the shot from a yellow saucer - a near miss.

02 · The horizon

Seven interrupts and one colour register

The sky, the horizon gradient and the ground cost no pixels. A chain of raster interrupts rewrites the background colour at seven lines of every frame, each stage pointing the interrupt vector at the next.

StageLineRowWrites
$B2FE16above the picturebackground $0B (dark grey) for the status rows
$B31D662sky colour from $BB; character set $4000 or $5000; multicolour on
$B34E1219, topfirst horizon colour from $BC
$B3741259, middlesecond horizon colour from $BD
$B39A1289, bottomthird horizon colour from $BE
$B3C013010ground colour from $AD; character set switched to the reflected copy at +$800
$B3EA19418black; font back to $7800; hi-res; then the per-frame timers, two sound sequencers and the colour flip

The last stage also does the game's per-frame housekeeping ($B415 to $B4BF): it counts frames, steps two sound loops and flips a bit that picks the low or high nibble of each colour pair. Where a pair has two different nibbles the band flickers between two colours at 25 Hz, which is how the game gets shades the palette does not have. The CIA timers are switched off at game start ($9F16), so this chain is the only interrupt while playing.

03 · The reflection

The ground is the sky upside down

After the objects are drawn into rows 2 to 9, $B1EC copies those rows into rows 17 to 10 with every character code inverted, and writes a top-to-bottom reversed copy of the character set into the next 2 KB. The interrupt at line 130 switches to that copy, so the lower half of the view is a mirror image for the cost of a copy loop.

Only the characters allocated that frame are reversed; the four fixed fill characters ($00, $20, $40, $60, textures 00, 55, AA and FF) invert into $FF, $DF, $BF and $9F, which the reversed copy leaves as the same textures. The view is double-buffered: odd frames draw into screen $6400 with characters at $5000 and $5800, even frames into $6000 with $4000 and $4800, and the line-66 interrupt shows whichever pair finished last.

04 · Objects

Obelisks, saucers and missiles are recipes for characters

Every object family has 32 pre-scaled scripts, one per distance step. A script says, row by row, which cells are solid fill and which are edges, and the renderer at $AD76 composes each edge cell into a freshly allocated character from a table of two-pixel masks.

Scripts live at $1E00 to $3EEB; the pointer tables at $9600 (low bytes) and $9700 (high bytes) are indexed by family × 32 + size step. Token colours: purple starts a new screen row, green is a run of fill cells, orange is one edge cell, red ends the variant.

FamilyDrawn asUsed by
0obeliskthe 64 obelisks, objects 32 to 95
1shotplayer shots, enemy shots, the warp spheres
2saucerthe enemy, object 2
3homing missilethe enemy when it is a missile
4, 5, 6explosion fragmentsobjects 24 to 31, cycling through the three kinds
7no scriptthe stargate and the arrival flash, drawn as a rectangle by $BA04

A script's first byte gives the screen row the object starts on, baked into each size step, and whether it carries two or four sub-cell variants for horizontal positions that fall between character cells. Fill cells are written straight into the screen as one of the four fixed characters. Edge cells are built: the renderer takes the cell's existing bitmap, applies an AND mask and an OR pattern from the tables at $BC85 and $BD05 for each of eight rows, and stores the result in the next free character code from $05, which walks $80, $A0, $C0, $E0, $01, $21 and so on, skipping the four fixed fills. Sixteen objects in view can use every dynamic character in the set.

Distance to size step goes through $8800, indexed by a logarithmic range code from $B766. The objects are drawn far to near ($B86C) after a selection sort of the view list ($B829), so nearer objects overwrite farther ones.

05 · Controls

One stick, three keys, and a pause that ignores the pause key

InputRead atDoes
stick up$DC00 bit 0forward, unless the object ahead is within $43 units, in which case a bump ($B53F)
stick down$DC00 bit 1reverse: the heading is flipped by 180°, the same move is made, the heading flipped back ($B56C)
stick left / right$DC00 bits 2, 3turn: 2 units on the first pass, then the skill's turn rate every pass, out of 2048 for a circle ($B4F0, $B4D8)
fire$DC00 bit 4a shot from one of three slots, lasting 24 passes (about a second), with an 8-pass lock ($ACB6)
F1$DC01 via column 0abandon the game and return to the title ($A118)
SPACE$DC01 via column 7pause ($A12A)
any stick direction$6Dends the pause ($A145); pressing SPACE again does nothing

The joystick is in control port 2 and is read once per pass of the main loop by $BC2C with the direction nibble kept in $6D and the fire bit in $6E. The same routine replays a recorded stick script from $7400 when the attract demo is running, one record per stick change with a count of passes, so the demo is a real game playing a recording.

On the title screen the keys go through the KERNAL's own scanner. F7 or the fire button starts. F5 cycles the skill through Novice, Advanced and Expert. The number keys 1 to 8 pick a level. The manual says only levels already reached can be picked; in the code the comparison with the highest level reached is still there at $9CE3 but the branch after it has been replaced by two NOPs, so any level is available at once.

06 · The plain

Sixty-four obelisks on a grid

Every object has a 24-bit X and Z position. The obelisks sit on a 32-unit grid and a 16 × 16 map at $8A00 turns any position into the obelisk in that cell, which is how a collision test costs one table read.

The attract demo, the game playing itself from a recorded joystick script at $7400, replayed from its own object table: every position is the 24-bit value held at the top of a pass of the main loop ($A04C), read out of the running game. The plain is a 256 × 256 torus in high-byte units, so the view keeps the player in the middle and wraps. Grid lines are the 16-unit cells of the obelisk map at $8A00, drawn so that each obelisk sits in the middle of its cell. The pale wedge is what the visibility scan ($B77F) lets through: 45° either side of the heading and within $60 units on each axis. A ring marks a shot bouncing off an obelisk.

Shots and enemy shots ask the map every pass ($AC30): if the cell they moved into holds an obelisk and their range code to it is below $4F, their previous position is restored and their heading turned. A head-on hit comes straight back; an off-centre hit is turned four times as far as a mirror would turn it. The player never asks the map. Instead the visibility scan ($B77F) remembers the nearest object of any kind within four units, and the move routine refuses to advance while that object lies within 45° of the direction of travel. Homing missiles use neither test, which is why the manual warns that obelisks do not stop them.

07 · Enemies

One enemy at a time, sixteen kinds of saucer

There is one enemy slot. Between enemies the game waits a random 8 to 39 passes of its main loop, a third of a second to a second and a half, then decides between a saucer and a homing missile. A saucer's type is random within the range the level has unlocked, and each type has a fire script.

Recordings of level 1 with the player standing still, the saucer's type forced at spawn ($A441) or a missile forced through $33. The panel shows the behaviour handler's own counters as the game held them; the strip is the whole recording, coloured by what the enemy was doing, with white ticks where a shot hit the player and the level restarted. Checked against the code, recording by recording: every shot leaves on the bearing to the player plus twice its script's spread byte, every bounce turns the shot the way $AC6E computes it, and every change of course takes its speed from the difficulty tables.

Fire scripts at $8C00: sixteen start offsets, then pairs of (wait in passes after the shot, spread: twice this many 2048ths of a circle added to the bearing, or with bit 7 set a random spread within the low bits) ended by $FF and the offset to loop back to ($A97A). A type whose handler ignores the script still has one. Behaviour handlers from the table at $AAB5: types 0 to 4, 6, 8 and 10 use the default handler at $A5D6; 5 and 11 burst fire ($A563); 9, 12, 14 and 15 fire from the script ($A5C7); 7 and 13 fire permanently ($A70D).

RuleWhere
Type is random in 0 to 2 × level − 1, so level 1 has two types and level 8 all sixteen$A42C
The first two saucers after a level-up are the two newly unlocked types$A42C, $9E
A missile comes when one is pending, when 24 periods of 256 frames pass without a kill or a shot, or one time in four while a saucer flag is set$A322
A saucer gives up and leaves after the same 24 periods$A5F2
Saucer speed and heading are rerolled from a difficulty value that ramps from the skill's base to $3F$A693, $AB70
With a chance set per level ($68C8, hidden in the title screen) the saucer's heading is aligned to the player's bearing$A6E6
Missiles head for the player every pass, weaving up to 45° either side while far away, and whine on voices 2 and 3, pitched by range$A7F0, $A811
An object hits the player when its range is under $80, or between $80 and $BF if its bearing jumped by $57 or more since the last pass (it passed through)$AD26

The console lamps are five cells of colour memory each, recoloured by $AA70. The middle right lamp turns yellow when a saucer arrives and orange when it goes. The lower right lamp turns light blue for 18 passes when a saucer fires. The upper left lamp alternates red and pink while a missile is live.

08 · Scoring and shields

Points times level, and a shield count that lives on the screen

Saucer typePoints× level 1× level 8

The table at $8BB0, four bytes per type in BCD; a missile is worth the 500 at $8BAC. $A8B2 queues the award and $AAC8 pays it level times through the BCD adder at $B8B8.

The number of enemies in a level is drawn when the level starts: a random byte masked to 10 to 19 ($AA8F) into $8BA8, which is also what the E counter shows. The score is four BCD bytes at $8BA0 with a permanent trailing zero, which is why it always ends in 0.

The shield count has no variable. It is the digit on the status line at $6026: the game writes a 4 there at the start, subtracts one character code pair on each death ($ABDC), and adds one, up to 9, when the last enemy of a level dies ($A91D). The check for game over reads the same cell before subtracting, so the game ends on the death that finds the digit already at 0. Four shields are six deaths in the attract demo: four to start, one earned, and one more with S0 showing.

09 · Levels

Eight scene records, one warp, three skills

A level is a 16-byte scene record: sprite flags and five colour pairs. The level index counts down, $70 for level 1 to $00 for level 8, and $80 is the warp. Odd levels have a coloured sky, even levels a black one.

RecordLevelSkyHorizon 1Horizon 2Horizon 3Ground$D022sprites

Records at $8010, copied to $AB to $BA by $B920. A split swatch is a colour pair whose two nibbles differ: the band flickers between them.

ParameterExpertAdvancedNoviceDemo

Eight tables of four bytes at $8F80, one column per skill; the demo plays from the fourth column.

When the last enemy dies the stargate (object 3) is placed at that enemy's X for 240 passes, about ten seconds, and a shield is added at once. Flying into it starts the warp: the scene switches to record 8, speed ramps toward the level's top speed ($A7E8: ), and spheres spawn in the enemy-shot slots every three passes. After about four periods of 256 frames the level number goes up ($A798). Missing the gate returns to play with a new enemy count and no penalty ($A552); hitting a sphere is an ordinary death.

10 · Sound

Two loops and a whine, written straight to the chip

There is no music player. The interrupt steps two pitch loops, one for voice 1 (the engine) and one for voice 3 (the saucer), and the game code writes the rest of the SID registers as things happen.

The loops at $80C8 (voice 1, noise) and $80A0 (voice 3, triangle) hold the high byte of the SID frequency, stepped once per frame by $B415 and $B444. Played here through Web Audio at the PAL rate: frequency = byte × 256 × 985248 / 224, about 15 Hz per unit.

The bump against an obelisk is a burst of noise on voice 1 at $1414 ($B549). The missile whine puts a range-derived pitch on voices 2 and 3 ($A811). The warp has its own rising tone ($A4CD). Volume is set to 15 with the filter mode at $9D9B and left there.

11 · The title screen

Data hidden in plain sight

The title screen is a complete 1000-byte screen stored at $6800. Five of its rows are painted in the background colour, and those rows hold the level-key table, a colour table, sprite colours, three variables, seven stub routines and the skill names.

Drawn from $6800 with the per-row colour table at $6918, which itself sits in row 7. With the reveal on, rows coloured like the background are shown in white.

RowHolds
5$68C8 the chance a saucer aligns to the player, $68C9 its default, $68CB the highest level reached
7$6918 25 row colours; $6931 the matrix codes of keys 8 to 1; $6939 seven sprite colours
13$6A08 seven copies of JMP $6A1D and the RTS they land on; the game calls six of them, on every pass in one case, and all do nothing
20, 21EXPERT, ADVANCED and NOVICE in the wide font, one of which F5 copies into row 22

12 · Quirks

Things a pilot would not know

It refuses to run on an NTSC machine

$9DEC reads the raster line through the odd address $CFF9,X with X = 25, which lands on $D012, and spins until the line reads 0. It then looks at the previous non-zero value. On PAL that is $37 (line 311 wrapping to 0) and the game continues. On NTSC it is $06, and the code jumps through a pointer that by then holds $DBE8, an address in colour memory. Tested with the emulator set to NTSC: the title screen is drawn and the processor parks at $DBE8 for good.

Seven bytes that should not be executed

Between the game-start setup and the level start, $9FF9 holds A9 4B 53 54 41 3A 2A. A checkpoint counted one pass through them on every F7. The processor reads them as LDA #$4B, then the undocumented SRE ($54),Y, EOR ($3A,X) and ROL A, none of which matters to what follows. As text they read KSTA:*. Whether this is a patch or original is unknown.

RESET restarts the game

$8000 holds the vector $9C3E twice and $8004 the text CBM80, the signature the KERNAL looks for in a cartridge. A hardware reset or RESTORE therefore lands in the game's own init instead of BASIC.

Any level, any time

The number keys were meant to reopen only levels already reached this session. The comparison is still in the title loop; the branch that acted on it is two NOPs. Pressing 5 on a fresh boot starts level 5.

The pause key cannot unpause

SPACE enters a loop that watches F1 and the joystick. It never looks at SPACE again; any stick direction resumes play.

The shield count is a screen cell

No variable holds the shields. The routines that add and subtract them edit the character code of the digit on the status line.

Calls into the title screen

Six routines, including the main loop's wait for the raster, call addresses inside row 13 of the title screen, where seven JMP $6A1D lead to an RTS. Something once lived there. What it was, whether a loader hook or a protection check, is unknown.

13 · Where it lives

Thirty-one kilobytes, most of it tables

RangeBytesWhat
$1400 – $17BF960per-object working tables: view list, bearings, velocities, previous positions
$1E00 – $3EEB8428224 shape scripts, moved here from $3E00 by the init
$4000 – $5FFF8192two view character buffers with their reflected copies, rebuilt every frame
$6000 – $67FF2048the two play screens; four sprite frames in the rows of the second that are never drawn
$6800 – $6FFF2048title and game-over screens
$7000 – $7FFF4096sprite frames, then the character set
$8000 – $9BFF7168RESET vectors, scene records, sound loops, object arrays, maths tables, obelisk map, zero-page image, score, fire scripts, skill tables, shape profile rows and pointers
$9C00 – $BDFD8702the game: init and title, main loop and enemy states, renderer, interrupt chain, movement, input

Addresses are from the contributor's own program file, loaded and left at the title screen and then in level 1. The symbol map, the verified facts and the feature checklist sit beside this page.