A design review for porting Peter Irvin and Jeremy Smith's 1988 BBC Micro Exile to the PC3 in MMBasic: what the original actually is, measured against what the PicoMite gives us, the decisions that shape the port, and a phased plan built on the Chuckie Egg and Thrust precedents.
Right: the landing site at the start of the game, 30 by 12 squares. Not a screenshot: every pixel was produced by running the reverse-engineered world generator, the palette logic and the sprite sheet, then scaling 2:1 for square pixels.
Thrust was a small game with one big idea, and the port was cheap because everything that made it hard on a 6502 could be deleted. Exile is the opposite shape. Its rendering tricks — the circular screen buffer, the self-modifying sprite plotter, the raster interrupt that paints the water — are all deletable too, and the PicoMite's tile engine draws its world in one call. But the thing that made Exile a masterpiece is the thing that cannot be deleted: a physics kernel that treats every one of sixteen live objects the same way, a creature model with moods, stimuli, line of sight and memory, and a world that is a pure function of its coordinates. That is where the port's effort goes, and it is where the risk is.
The good news is that the whole of it is written down. The level7 disassembly annotates every routine and every table; Tom Seddon made it buildable; Jon Saffron transcribed the landscape generator into C#; and the 2026 bbcexile.com rebuild has re-implemented it bit-identically in TypeScript, validating whole frames against a 6502 interpreter. There is no guessing to do about how Exile works. There is a great deal of transcription to do.
Three measurements were made for this review, before any BASIC was written, because each one changes the design:
TILEMAP DRAW.Each is argued in the architecture section; the recommendation is listed first.
TILEMAP. Done TILEMAP CREATE reads its map only from DATA statements, and 65,536 values will not fit in program memory. TILEMAP LOAD file$, id, flashSlot, tileW, tileH, tilesPerRow now reads a text file of width, height and tile numbers instead; a 256 × 256 map loads in 0.5 s on the PC3. Built and tested the same day (Testfiles/TilemapLoadTest.bas, and Bas/breakout.bas now loads its brick field from breakout.map).PLAY BBC ENVELOPE cannot express. No new primitive is needed: the game steps its own envelope tables and hands each 20 ms step to PLAY BBC SOUND as a flushed 50 ms note, so the sound stays self-limiting. The teleport sound was played that way against an exact-frequency reference (Testfiles/ExileTeleportSound.bas) and the quarter-semitone rounding, at most 12 cents, was not audible. Measured on the PC3, PLAY BBC SOUND is also the cheaper engine: 17 µs a call like PLAY SOUND, but 8.5% of the interpreter with four channels sounding against 19%.BLIT FLASH a frame with the key colour transparent and no RAM buffers. Verified on the PC3: a screenshot of sixty sprites and two of them in all four orientations matches the Python reference with 0 pixels differing.get_tile validated square-for-square against the C# generator, shipped as a 128 KB file. Generating it on the board in BASIC would take minutes.| Fact | Value | Source |
|---|---|---|
| Frame budget | 40 ms at Exile's own 25 Hz; Thrust and Chuckie Egg hold 33 ms | Measured on this board |
| Screen | MODE 2, 320 × 240, 16 colours (RGB121); the eight BBC colours are all in the palette | Same mode as the other ports |
| Tile engine | TILEMAP: 4 maps, 2 bytes per cell, tiles to 256 px, viewport draw in one C call with sub-tile scroll and a transparent colour, SET for live changes, attribute collision | TILEMAP_User_Manual.md (RP2350 only, RGB121 only) |
| Tileset storage | 3 flash image slots, each the size of program memory (144 KB on the PC3): 287 tiles of 32 × 32 per slot | MAXFLASHSLOTS, MAX_PROG_SIZE in configuration.h |
| Sprites | 64 RAM buffers with mirror on SPRITE SHOW; BLIT FLASH from a slot with transparency but no mirror | SPRITE and BLIT manuals |
| Double buffering | FRAMEBUFFER CREATE / WRITE F / COPY F, N; the copy costs 0.1 ms, or waits for the 60 Hz frame with ,B | Proven in Chuckie Egg, Thrust, Elite |
| Input | KEYDOWN, up to six simultaneous keys | Established; Exile needs four at once at most |
| Sound | PLAY BBC SOUND / ENVELOPE on four channels; PLAY WAV for samples | V6.03.02b6 |
| Program memory | 144 KB; Chuckie Egg is 53 KB, Thrust 66 KB of source | MEMORY on the PC3 |
| RAM | PSRAM, 6 MB free: the 128 KB world map and every table are trivial | MEMORY on the PC3 |
| Interpreter speed | Thrust: 500 array accesses a step cost 7 to 9 ms; a full particle pool and 19 objects simulate in 17 to 20 ms | Thrust phase 5 table |
The number to keep in mind is the last one. Exile ticks sixteen objects and thirty-two particles, each with more logic than a Thrust limpet gun, and the frame it has to fit is only seven milliseconds longer. The renderer will be cheap; the interpreter is the budget.
Exile shipped as about 26 KB of 6502 and data. The level7 disassembly of the standard version runs to 8,528 instructions (8,796 for the enhanced version) against 4,937 for Thrust, so the game is 1.7 times Thrust's code, and considerably more than that in behaviour, because Exile's code is table-driven and self-modifying where Thrust's is plain.
The planet is 256 × 256 squares and is never stored. get_tile(x, y) is about 500 bytes of code, 54 bytes of tables and 1,024 bytes of hand-drawn data, and it answers for any square on demand. A seven-instruction hash of the coordinates is the only entropy; from it the layers are stacked: sky above row &4E, rock texture, the surface, the ragged bedrock floor, roughly 8-square chambers, one-wide vertical shafts, sloping passages whose direction is a self-modified add or subtract, horizontal corridors, and finally furniture — nests, bushes, columns, pipes. The 1,024 mapped bytes are addressed by a second hash that admits exactly 1,024 of the 65,536 coordinates, with no collisions: the crashed ship, the equipment rooms and Triax's base read back through it.
Placeholders &00–&08 mean "ask the object system": a 254-entry tertiary table keyed on x alone resolves doors, switches, transporters, nests and their live state.
A custom 6845 mode 128 pixels wide, 256 tall in the enhanced version (16 KB at &4000) and 128 tall in the standard one, with 2:1 pixels so a 16 × 32 tile is square on a monitor. The 16 KB is a circular buffer and scrolling is a nudge of the start-address register, redrawing only the exposed strip. Every graphic in the game comes from one 128 × 81 two-bit sprite sheet of 2,592 bytes holding 125 sprites, drawn through a one-byte palette (top nibble picks the colour for logical 3, bottom nibble indexes a sixteen-entry table for 1 and 2) and flipped either way at plot time by self-modifying the loop direction.
Colours 8–15 are programmed identical to 0–7 and serve as a per-pixel priority flag: objects are plotted masked into 0–7 and skip any byte where a priority pixel already sits.
Sixteen primary slots are simulated each tick; thirty-two secondary slots remember position, type and a nibble of energy for things that went off screen; the tertiary table holds things fixed to a place. The player, a boulder, a bullet and Triax go through the same update: copy to zero page, look up weight and sprite extent, integrate velocity into position, collide with water and tiles, resolve support and wedging, run the type's behaviour, then apply accelerations to velocities with gravity folded in as a carry. Weight is three bits of the type flags and decides buoyancy, wind and what you can lift.
Positions are a square plus an 8-bit fraction; velocities are signed 8-bit fractions per tick, capped at ±64 — a quarter of a square.
Around forty per-type update routines share a walking kernel with seven walking types (player, frogmen, imps, green slime, robots, worms) parameterised by five tables: steepest walkable slope, acceleration, weight shift, turn probability and jump probability. On top of that sits a mood in four states driven by ten stimulus types, a target with a "directness" that says how straight the path is, an avoid flag, a remembered target square, and line of sight that the water surface blocks. Nests spawn birds; feeding an imp yields a gift; whistles summon.
Ten object-type ranges set maximum energy (player &7F, flying enemies &FD, projectiles &3F) and four explosion types decide what happens at zero.
The vsync interrupt steps, for each of four channels, a volume envelope and a frequency envelope, each a list of stages (a delta applied N times) with loops, then writes the 76489 directly: frequency = 4 MHz ÷ (32 × value) through a four-range remap so that lower numbers mean lower pitches. Forty-nine call sites pass a four-byte block: two envelope-and-start bytes for volume, two for frequency. The enhanced version adds seven 4-bit speech samples played from sideways RAM: "Welcome to the land of the Exile", "Ow!", "Ooh", "Destroy!", "Radio die".
Thirty-two particle slots and eleven particle types (plasma, jetpack, explosion, fireball, trail, engine, aim, mushroom spore, flask, water, wind), each a row of eleven bytes of lifetime, speed, colour, flags and randomness. Four independent waterlines by x-range, drawn by a mid-frame raster interrupt that recolours colour 0 cyan for one line and blue below, with "desired" levels that let Triax's lab flood. A random-tile event system that runs wind, water, mushrooms and nest spawning on whichever square it picks. An earthquake that shudders the CRTC. Thirty-nine key actions. A save that dumps the whole state to disc.
| Thing | Count | Where |
|---|---|---|
| World | 256 × 256 squares of 16 × 32 pixels | landscape generation, &1715–&19A6 |
| Mapped squares | 1,024 | 1,024 bytes at &4FEC, a perfect bijection |
| Tertiary objects | 254 entries, matched by 439 squares | x-keyed tables at &05EF |
| Object types | 101 | &00 player to &64 invisible inert |
| Live objects | 16 primary + 32 secondary | physics engine, &1A0B–&1E18 |
| Sprites | 125 in 2,592 bytes | 128 × 81 sheet at &53EC |
| Tile types | 64 | &00–&0F handled, &10–&3F scenery |
| Particles | 32 slots, 11 types | &0206 |
| Sounds | 49 call sites, ~200 bytes of envelopes | &1320–&149C, &2DB9 |
| Key actions | 39 | &11F6 |
| Tick rate | 25 Hz ceiling | two vsyncs in consider_setting_crtc_start_address |
| Instructions | 8,528 (8,796 enhanced) | counted in the level7 listing; Thrust is 4,937 |
Jon Saffron's ExileWorldGenerator is a line-by-line C# transcription of the 6502 landscape generator, the object-data overrides and the palette logic, with the sprite sheet and its tables. Compiled without its Windows Forms shell and driven from a small console harness (kept in Bas/exile_tools/census/), it walks all 65,536 squares and reports what a port has to store and draw. The two images in this document are rendered by the same code; the lower part of the header image is the caves the player first drops into.
| Squares | Count | Share |
|---|---|---|
| Rock and slopes | 38,341 | 58.5% |
| Open (of which windy) | 26,213 (1,308) | 40.0% |
| Features: nests, bushes, pipes, mushrooms, columns | 802 | 1.2% |
| Spaceship structure | 180 | 0.3% |
| Hand-mapped | 1,024 | 1.6% |
| Carrying a tertiary object | 439 | 0.7% |
Rock is earth, stone, their slopes, quarters and halves; open includes the placeholder types that resolve to doors, switches and transporters.
A TILEMAP tile index has to stand for one exact set of pixels, and Exile recolours the same sprite by position: stone changes palette every 16 rows, earth every 32, bushes pick one of four schemes from a position hash, mushrooms are red on floors and blue on ceilings. So the question is how many distinct (sprite, flip, palette) combinations the planet actually uses, and the answer is not the worst case of 64 types × 4 orientations × 34 palettes but 413.
| Measure | Value | Consequence |
|---|---|---|
| Distinct (type, orientation, palette) | 436 | Upper bound on tile indices |
| Distinct after merging identical sprites | 413 | The tileset |
| … of which empty | 15 | Tile index 0 |
| Variants covering 99% of squares | 155 | The long tail is set pieces |
| Distinct palette bytes | 34 | Colours 1–3 through the pair table |
| Storage at 32 × 32 × 4 bpp | 401 tiles, 205 KB | Two slots of 287; the third is free for objects |
Two tilemaps share one map: indices 1–287 live in slot 1 and the rest in slot 2, each map holding zero where the other has the tile, and both are drawn every frame. TILEMAP DRAW skips zero cells, so the second draw costs only the tiles it owns.
The plotter's rule is that an object pixel never lands on a priority pixel. Taking every one of the 413 variants through its palette and counting pixels by group gives 100,773 priority pixels against 1,611 plain ones. 392 variants are all priority, 3 are all plain, 3 mixed, 15 blank. The six with plain pixels are the water-stone tile in Triax's lab and a few spaceship fittings.
So the original's picture is reproduced almost exactly by ordering rather than by masking: fill the background, draw the objects, then draw the tilemap with colour 0 transparent so that objects show through the black of the tunnels and are covered by rock, bushes and pipes. The six exceptions can be drawn before the objects from a third, tiny tilemap, or ignored; either way there is no per-pixel test and no second copy of the tileset. This is the measurement that makes the renderer cheap.
A square is 16 × 32 BBC pixels, a position is a square number and a 256th of a square, so there are 16 fractions to a pixel across and 8 down. Keep all of that as integers in MMBasic — one 64-bit integer per axis holding square × 256 + fraction — and the physics transcribes line for line. The screen mapping is applied only when drawing: one BBC pixel is 2 × 1 of ours, so a square is 32 × 32 pixels and world pixel (x, y) lands at (2x − camX, y − camY). Do not be tempted to rescale the constants; the same lesson as Thrust's elliptical angle tables applies to Exile's "very approximate" angle-to-vector routines, which are 256-step tables with known errors that the game was balanced against. Copy the tables, not the trigonometry.
The enhanced BBC view is 8 squares wide and 8 tall; the standard version showed 8 × 4. At 32 × 32 a full 320 × 240 would show 10 × 7½, and it was the first recommendation, on the grounds that the BBC had no status display. The decision went the other way once the cost of the view size was understood: Exile spawns from a nest and promotes a turret when its tile is plotted, and demotes objects 4 or 12 squares off screen, so the visible area governs how many of the sixteen primary slots are alive, and each live creature is the most expensive thing in the tick. A 256 × 240 view plots 60 squares, the enhanced version's 64 near enough; 320 wide would plot 75. The 64-pixel panel beside it carries what the BBC could not afford, energy, the weapon and its charge, the pockets, and it is drawn once into the framebuffer and never cleared, so it costs nothing per frame. worldview.bas has the layout.
The main loop increments a frame counter, updates every object, runs the events, and goes round again; the only pacing is that replotting the player waits until two vsyncs have passed. So the tick was 25 Hz on a quiet screen and slower on a busy one. Gravity is +1 per tick, inertia is −1 every sixteen ticks on a counter offset per object slot so that not everything settles on the same frame, water drag is ×⅞ every four ticks, the wind tiles rotate once in 64 ticks. None of that survives rescaling. Run one tick per frame at 40 ms, and if the interpreter cannot keep up on a busy screen the game slows down exactly as the original did.
The generator is a pure function, so the whole map can be produced on the PC and shipped as a file: 65,536 cells of tile-variant index, 128 KB as 16-bit values, plus a parallel byte per square of tile type for the game logic (obstruction profile, handler, wind, water). A Python port of get_tile is a day's work and is checked square-for-square against the C# generator, which is itself checked against the game. Generating on the board is out: the routine is a few hundred BASIC statements per square, and 65,536 squares would take minutes.
Getting the file into a TILEMAP was the one thing the firmware could not do when this review was drafted: TILEMAP CREATE reads exactly cols × rows values from DATA, and 65,536 DATA values are more than program memory. That is now closed. TILEMAP LOAD file$, id, flashSlot, tileW, tileH, tilesPerRow reads a plain text file — width, height, then the tile numbers, with comments and any mix of separators — through the same allocation as CREATE, and a 256 × 256 map takes 476 ms on the PC3. So gen_world.py writes one .map file per tilemap slot and the game loads them at start. The windowed-map fallback that this section used to describe is no longer needed.
Doors opening, nests emptying, switches thrown and mushrooms picked are single-cell changes and go through TILEMAP SET in either scheme. Tile collision uses the game's own obstruction profiles, eight heights per tile type, looked up from the per-square type byte; the tile engine's attribute collision is a convenience for the quick tests, not the physics.
BLIT FLASH D5An object is drawn as a sprite in the palette its type table gives it, flipped horizontally to face left and vertically when hanging or swimming inverted. The 112 non-tile sprites total 10,842 BBC pixels; rendered at 2 × 1 in every palette an object type uses and in all four flips, the sheet is about 90 KB, inside one slot. Each object then costs one BLIT FLASH a frame with colour 0 transparent, sixteen a frame at most, no buffer bookkeeping. The alternative, a least-recently-used cache of the 64 RAM sprite buffers keyed by (sprite, palette) with mirroring from SPRITE SHOW's rotation argument, halves the storage and adds a cache; it is the fallback if the sheet does not fit.
Water is not tiles. Four waterlines by x-range (starting at columns 0, &54, &74 and &A0, at rows &CE, &DF, &C1, &C1) with desired levels that make Triax's lab flood and drain. On the BBC a raster interrupt turns colour 0 cyan for one scanline and blue beneath. For us it is two BOX fills into the background before the objects — black above, blue below, a cyan line at the surface — and the tiles' transparent black lets it show through exactly where the original showed it. Wind is particles and an acceleration on objects in windy squares; both come straight from the tables.
Thirty-two slots, eleven types, each new particle seeded from its type's row of lifetime, speed, colour, flags and randomness, optionally inheriting the parent object's velocity or acceleration. Double-height ones are two pixels; the "plotted on foreground" flag means drawn after the tiles. PIXEL and a two-pixel BOX into the framebuffer, as Thrust does.
Exile keeps one table per field, sixteen entries each, and so should we, because that is the layout MATH works on. MATH C_ADD, C_SUB, C_MUL and C_DIV combine two integer arrays element by element, MATH ADD and SCALE apply a constant, MATH SHIFT divides or multiplies by powers of two, MATH SET clears, and C_AND, C_OR and C_XOR work on flag arrays; all of them also accept structure member arrays with a stride, so an object table declared as a structure array is not ruled out, only slower. What they cannot do is anything conditional per element: the ±64 velocity cap, the inertia on the one object whose turn it is, support and collision. So the integration step is seven MATH calls, including the cap through MATH CLAMP a(), lo, hi, b(), which was added to the firmware for this (14 µs for sixteen elements, integer or float, structure members included), and the particles are four calls and a respawn loop. Measured, integration falls from 2.8 ms in a loop to 0.23 ms, and the particles from 2.8 to 0.7. Only the one inertia object per tick and the respawns stay as per-element code.
Phase 3 measured a faithful player kernel at 9.8 ms a tick in BASIC and 0.1 ms as a CSUB, the same transcription in C and the same 2,384-tick check. The physics and the behaviours of all sixteen slots go into that CSUB, called once a tick with the state array, the packed tables and the 64 KB world array (the arithmetic is 8-bit and the tables are bytes, so the translation is mechanical). BASIC keeps what it is good at: reading keys, drawing the frame with TILEMAP and BLIT, stepping the sound envelopes, and the game's events. Two build rules learned: no jump tables (a switch wants a libgcc helper the blob cannot have, so armcfgen.py now compiles with -fno-jump-tables), and no writable statics, so the state lives in the argument array.
The port's largest single body of work. The walking kernel and its seven walking types, the mood machine, the stimulus table, target selection and directness, line of sight and the remembered target all transcribe as data plus one shared routine; that is most of what makes the creatures feel alive. Then the per-type behaviours, about forty routines of ten to eighty instructions each, become SUBs dispatched with SELECT CASE on type. OPTION CACHE SUB and OPTION TRACECACHE, which paid for themselves in Elite, apply here.
The BBC's ENVELOPE has three pitch sections and a four-phase amplitude curve; Exile's envelopes are arbitrary lists of stages with nested loops, stepping every vsync, on frequency and volume independently. So the game does what the original interrupt did: it steps the envelope tables itself, two vsync steps a frame, and issues each step as PLAY BBC SOUND &H10 + ch, -loudness, pitch, 1, a flushed 50 ms note that replaces the last one and falls silent by itself if the program stops writing. The frequency value goes through the original's four-range table to a chip period, then to the nearest BBC pitch unit; that rounding is at most half a unit, 12 cents, and on the teleport sound, the worst case with its 120-cycle warble, it could not be heard against an exact-frequency reference. The 208-byte envelope table and the 49 four-byte sound blocks transcribe as data, as Thrust's did. The seven speech samples are extracted to WAV by Tom Seddon's script and go through PLAY WAV.
Thirty-nine actions on KEYDOWN; Exile's worst case is thrust, aim, fire and the booster together, inside the six-key limit. The BBC save is a dump of the state, so ours is the object lists, the tertiary state bytes, the waterlines, the player's kit and the teleport memories to a file. Keep the original's four-byte random number generator so that a bug seen on the board can be reproduced on the PC.
Thrust's 4,937 instructions became 1,245 lines of BASIC plus generated data. Exile's 8,528 are branchier and table-heavier, so expect three to four thousand lines of code with almost no DATA, because the map, the tiles and the sprites live in files and flash. Loaded with crunching that is within the 144 KB, but it will be the largest program yet on the board, and CHAIN with VAR SAVE is the fallback if it is not.
Everything in this table is from the level7 listing or the bbcexile physics chapter and is what the BASIC must reproduce. It is the equivalent of Thrust's table of divisors: get these right and the game feels like Exile.
| Quantity | Value | Notes |
|---|---|---|
| Position | square + 8-bit fraction per axis | 16 fractions per pixel across, 8 down |
| Velocity | signed 8-bit fractions per tick, limited to ±&40 | a quarter square a tick; objects at the cap stop accelerating |
| Gravity | +1 to vertical velocity per tick | folded in as the carry from CPX #2 |
| Inertia | each velocity moves 1 toward zero every 16 ticks | on a per-object counter: frame_counter + slot |
| Water drag | both velocities × ⅞ every 4 ticks | |
| Buoyancy | up to 4 iterations, each a quarter of the height submerged; each surviving one lifts vertical velocity by 1 or 2 by weight | shorter and lighter objects float more |
| Weight | 3 bits of the type flags; 7 is static | player 3, changed by what is held |
| Jetpack | ±1 acceleration per tick per direction key; doubled with the booster | 1 energy per 2 or 8 ticks |
| Jump | upward velocity (10 − weight) × 2, or (16 − weight) × 2 with the booster | only within 5 ticks of standing on walkable ground |
| Walking | max slope &32 (70°) for the player, &20 (45°) slimes and robots, &80 for frogmen, imps and worms; acceleration 6, 8, 16, 3, 4, 8 by type | plus turn and jump probabilities |
| Tile collision | 8-byte obstruction profile per tile type, one height per two-pixel column | the tables at &0100 |
| Bounce | reflect, softened by ⅛ of the impact angle; speed capped at &20, less 2, × ⅞ | |
| Impact damage | (angle from perpendicular ÷ 4) + speed − &40, halved | only when positive |
| Surface wind | centred on the crash site, none within &1E of it, doubling at &32 and &3C; none below the surface | weight-scaled |
| Cavern wind | variable-wind squares rotate through a circle every 64 ticks | two western caverns blow constantly |
| Angles | 256 to the circle, clockwise from right | approximate vector routines; copy the tables |
| Demotion | off screen by 4 squares if fast or unsupported, 12 otherwise; tertiary return at 1 | type flags &20 / &50 / &60 / &70 |
| Energy | player &7F; ranges &07 to &FF by type; four explosion types at zero | near death the player is teleported |
| Original technique | Why it existed | What we do |
|---|---|---|
| Generating the world on the fly | 65,536 squares would not fit in 32 KB | Generate once on the PC, ship 128 KB, load into TILEMAP |
| Circular screen buffer and start-address scrolling | Redraw only the exposed strip on a 2 MHz CPU | TILEMAP DRAW of the viewport every frame |
| Self-modifying sprite plotter, flips by loop direction, palette through the address field | No lookup tables, no RAM | Pre-rendered variants in flash |
| Per-pixel priority flag in colours 8–15 | Objects behind scenery without a depth sort | Draw tiles after objects; measured to be equivalent for 98.4% of pixels |
| Raster interrupt for water | Water without a water tile | Two box fills behind transparent tiles |
| 2-pixel and 8-scanline scroll steps | Byte and character-row granularity of the buffer | Pixel scrolling |
| Novella word check and the demo mode that hangs | Copy protection | Nothing |
| Disc save through the supervisor | Where a save could go | A file on the SD card |
Bas/exile/worldview.bas scrolls the planet at about 2 ms a frame at the surface, and screenshots at the ship and at Triax's lab, where the slot-2 tiles are, match the Python reference renders pixel for pixel. Two flash slots behind two tilemaps had not been tried before; they work.Testfiles/ExilePhysicsBench.bas runs sixteen objects through the kernel's shape (integrate, eight-column obstruction profiles against a loaded 256 × 256 map, support and bounce, walker, flyer and bullet behaviours, gravity, the cap and the staggered inertia), thirty-two particles and the frame's drawing. A full tick is 35.6 ms of 40 at 315 MHz (the clock that gives 640 × 480 at 75 Hz, three refreshes to a tick) and 30.0 ms at 378 MHz (60 Hz, two and two-fifths refreshes to a tick): integrate 2.8 / 2.3, tile collision 15.7 / 13.3, behaviours 4.6 / 3.9, particles 2.8 / 2.3, draw 11.8 / 9.8. Everything scales with the clock, so the choice between the two is the display cadence, not the budget: at 75 Hz a 25 Hz tick lands on every third refresh exactly, at 60 Hz it alternates between two and three. OPTION TRACECACHE halves the integrate loop and changes nothing else, because the tick is calls and array traffic, not tight arithmetic. Per primitive on this board: an assignment 6 µs, an array read 13 to 23 µs, a TILEMAP(TILE) lookup 12 µs, a sub call 19 µs, a function call 27 µs; a transparent TILEMAP DRAW of the screen 3.0 ms against 0.36 ms opaque; a 32 × 32 BLIT FLASH 107 µs. Three reductions follow from those numbers and take the estimate to about 20 ms: test each object's box once with TILEMAP(COLLISION) against FULL and SLOPE attributes and run the column scan only on slope tiles (the scan is 60 statements an object today); draw the second slot's map only where it has tiles, since an empty region costs 0.02 ms; and do the integration and particle arithmetic across all slots with MATH C_ADD and the new MATH CLAMP, which measured 0.23 ms against 2.8 and 0.7 against 2.8. The reserve, if the real behaviours come in heavier than the stand-ins, is the collision kernel as a CSUB, which would take its 15.7 ms to well under one.The plan said a Python port of get_tile. What was built is better: the level7 listing prints the machine-code bytes of every routine, so exile6502.py loads it back into a 64 KB image and runs the game's own landscape generator, tertiary lookup and palette code on a 250-line 6502 interpreter, exact by construction, self-modifying code included. gen_world.py sweeps all 65,536 squares that way in 4.5 seconds (8.7 million instructions) and agrees with the C# census on every square; it writes the whole-planet map, the two per-slot maps, the type bytes the physics needs and the variant table. gen_tiles.py renders the 399 variants from the sprite sheet at &53EC through their palettes into the two slot BMPs, and its landing-site render is identical to the census generator's. gen_tables.py cuts 51 tables out of the listing under their own labels, obstruction profiles to sound envelopes, into a BASIC DATA fragment, and dumps all 255 tables it finds to JSON. gen_objects.py builds the slot-3 object sheet. One listing typo was found on the way, a line at &4B91 that belongs at &4B9A, and the loader corrects it. gen_objects.py renders the object sheet and its coordinate table. gen_tables.py pulls the particle types, walking types, type flags, obstruction profiles, angle tables and the object type and sprite tables out of the listing. gen_sound.py transcribes the 49 sound blocks and the envelope table, and extracts the samples.
The check worth having: the census already says how many squares every tile variant should cover. If the Python generator's totals do not match the C# ones to the square, something has moved.
Bas/exile/worldview.bas: both tilesets into flash, both maps loaded, the four waterlines as blue fills under transparent tiles, arrow keys and jump keys, numbered screenshots. Verified pixel-exact against the Python renders at two places. The 256-wide view and the panel are in it. Still to do here: a door flipped with TILEMAP SET.
The physics update in the original's order with its constants, tile collision through the obstruction profiles, support and wedging, walking on slopes, jumping, the jetpack and booster, water, wind, and the energy cost of all of it. Checked more strictly than Thrust was: the game itself is the reference. exilegame.py runs the original's main loop one tick at a time on the 6502 interpreter with the keys driven from Python, and gen_traces.py records the player's slot after every tick of eighteen scenarios (standing, falling, walking both ways, jumping, the jetpack with and without the booster, a long flight, lying down, turning, dropping into the western caves' water, a cave ceiling and wall, an earth slope and a stone one): 2,384 ticks. exilephys.py is update_object for the player transcribed at the 8-bit level, carries included, and matches every one of them. Bas/exile/exilephys.bas is that Python translated routine for routine into MMBasic, and on the PC3 (gen_phystest.py, run_phystest.py) it also matches every one of the 2,384 ticks.
What the checking turned up: the second sound of a scream in the level7 listing is printed as JSR &14fa, the middle of the save-game encryption, which ends by wiping memory; the game and the comment say &13fa, and the loader now corrects it. The jetpack drains one unit or two each time, depending on a carry the particle code leaves behind (two is the common case and what the port will do). Climbing a slope steeper than 63° takes its direction from a zero-page temporary that the last walking creature left, an original quirk the port keeps by sharing the variable. The water level breathes: its fraction rises two a tick. And the player's own damage on hitting a wall depends, by one, on the carry the collision code happens to leave.
The cost, and what followed from it. The player's kernel alone, faithful, takes 9.8 ms a tick on the PC3 in BASIC, a quarter of the tick for one object, and the statement count, not the arithmetic, is what costs (S2's 35.6 ms was measured on a kernel of the right shape but a fraction of the branches). OPTION TRACECACHE does not help: with everything cached but one sub the kernel is slower (11.5 ms), because it is subs with locals called once a tick, and with that sub (CollTiles) cached the result is wrong, which is a firmware finding in its own right (bisect_cache.py). So the object update went to C: csub/exilephys.c is the same transcription again, built into a CSUB by armcfgen.py (10,988 bytes of code), called once a tick with the state array, the world and the packed tables, and checked by the same feed files: all 2,384 ticks match, at 0.1 ms a tick, a hundred times the BASIC. Decision D8 is taken: physics and behaviours in the CSUB, BASIC for input, drawing, sound and events.
Primary, secondary and tertiary lists with promotion and demotion at the right distances; picking up, holding, pocketing, dropping and throwing with the weight rules; the six weapons and energy transfer; keys, doors, switches, transporters and their destination table; boulders, grenades, flasks, mushrooms; the transporter beam; teleport memory and the near-death teleport.
Done so far. The CSUB is now the whole-scene kernel (csub/exile.c, ExileTick): update_objects for all sixteen slots, with the collision pass between objects and its velocity transfer, held objects (pick up, carry, drop), removal by distance and demotion to the secondary list, the teleport countdown, the per-type dispatch, and the first behaviours: the inert objects, the giant block and the collectables. The oracle records whole scenes (gen_traces2.py: every slot every tick, the water level, the screen position, and every random number the code drew keyed by the address that drew it, so the kernel asks for the ones it models and skips the rest). Twenty-five scenes match the game tick for tick and slot for slot, 3,336 ticks: the eighteen player scenes and seven with boulders, a piano and a key (resting, pushed, dropped on the player, rolling down a slope, a heap, picked up and carried). 0.14 ms a tick with four objects. Not yet: objects created when their tile is drawn (the ship's turret appears the moment its tile scrolls into view), promotion from the secondary list, the events, and the creatures.
The walking kernel and its tables, moods and stimuli, targets and line of sight; then birds and nests, imps and their gifts, frogmen, slimes, robots and turrets, wasps and piranhas, the hovering balls, Triax and the clawed robots. Each behaviour is verified against the listing's own notes, which for most of them say in a sentence what the creature is trying to do.
The title and the landing, the events system, Triax's lab with its maggot machine and doors and flooding, the earthquake, the destinator, and saving and restoring. Then the enhanced version's extras: the speech, and the viewpoint-object debug feature, which is worth keeping as a development tool.
| Risk | Likelihood | Mitigation |
|---|---|---|
| The tileset does not fit | Closed | Counted: 401 tiles, two slots, one to spare. |
| The priority scheme needs a second tileset | Closed | Counted: 6 variants have any non-priority pixel. Draw tiles last. |
| The interpreter cannot tick sixteen creatures in 40 ms | Measured | S2: 35.6 ms of 40 as first written, with collision at 15.7 ms and the draw at 11.8, for a kernel of the right shape. The faithful kernel costs 9.8 ms a tick for the player alone in BASIC and 0.1 ms as the CSUB it now is (D8), checked against the same 2,384 ticks. Sixteen objects are then under 2 ms; the draw and the particles stay where they were measured. |
Loading the map into TILEMAP | Closed | TILEMAP LOAD built and tested on the board; 65,536 cells in 476 ms. |
| The sound does not sound like Exile | Closed | The engine's own envelopes run in BASIC and drive PLAY BBC SOUND step by step; the teleport sound passed a listening test against an exact reference. |
| The generator port is subtly wrong | Low | 65,536-square comparison against the C# transcription, which the census has already exercised end to end. |
| Program memory | Medium | Three to four thousand lines, crunched on load. CHAIN the title and save screens out if needed. |
| A behaviour that depends on the 2-pixel scroll or the CRTC (the earthquake shudder, the teleport shrink) | Low | Both are cosmetic and have obvious framebuffer equivalents. |
Bas/exile_tools/
census/ this review's harness: Program.cs, README.md, the two images (done)
exile-disassembly.txt the level7 listing - fetched, NOT vendored
exile6502.py loads the listing into a 64 KB image; a 6502 that runs its routines (done)
gen_world.py the planet from the game's own code; maps, type bytes, variants; --check (done)
gen_tiles.py 399 variants -> two slot BMPs, contact sheets, previews (done)
render_window.py a 320 x 240 reference view for checking the board's screenshots (done)
gen_tables.py 51 tables -> tables.bas DATA under labels; every table -> tables.json (done)
gen_objects.py object sheet for slot 3: 213 sprite-palette pairs x 4 orientations (done)
test_objects.py objview.bas + a reference picture; 0 pixels differ on the board (done)
../exile/worldview.bas phase 2: the planet on the PC3, scrolling, screenshots (done)
gen_objects.py object sheet BMP + coordinate table; --png contact sheet
gen_tables.py particles, walking types, type flags, obstruction profiles, angles, sounds
gen_sound.py 49 sound blocks, the envelope table, samples -> WAV
out/ generated .bas fragments, BMPs, verification images
exile_code.bas the game without generated data (phase 2 on)
build.py glue, writing ../exile.bas (phase 2 on)
The listing and the disc image stay out of the tree for the same reason Thrust's did. The C# generator is MIT-licensed and could be vendored, but the census README shows how to fetch and build it in three commands, which is enough.
samples.py for the speech and a sprite viewer.docs/Thrust_Port_Plan.html, docs/Elite_Port_Plan.html, docs/TILEMAP_User_Manual.md, docs/PLAY_BBC_User_Manual.md — our own precedents and the engine facts.