A phased plan for bringing Jeremy Smith's 1986 BBC Micro Thrust to the PC3 in MMBasic, built on the frame budget and sprite pipeline proven by Bas/chuckie.bas, the new PLAY BBC SOUND engine, and Kieran Connell's annotated disassembly.
Below: 129 scanlines of level 5's cave, decoded from the real RLE terrain data in the disassembly.
Thrust is a small game with one big idea. The physics is a page of arithmetic on about a dozen variables. The terrain is two numbers per scanline. There are at most 32 objects and 32 particles in the world at once. What made it a technical showpiece in 1986 — XOR plotting, collision detection by self-modifying code, a 256-byte circular terrain window, Q7.8 fixed point — was all workaround for 32K of RAM and a 2 MHz CPU, and none of it needs to survive the port.
What does need to survive is the feel, and the feel is a table of constants. The thrust divisors, the two drag ratios, the tick gating, the rotation rate limit, the 32-entry elliptical angle tables: copy those exactly and the ship flies like the original whether the arithmetic underneath is fixed point or MMBasic floats. Per the house rule that interpreting dominates and the arithmetic is free, floats cost us nothing, so there is no argument for reproducing the fixed point.
Can MMBasic redraw a screenful of cave wall inside a 33 ms frame? That has been answered before writing a line of BASIC — see the measurement. Because the terrain is stored as long runs at a constant X, a typical frame merges down to 13 to 56 rectangle fills, and the worst frame in all six levels is 158. Not 240 scanline draws. The design is comfortable.
| Fact | Value | Source |
|---|---|---|
| Frame budget | 33.01 ms, 30.29 fps sustained | Measured running Chuckie Egg on this board |
| Screen | MODE 2, 320 × 240, 16 colours (RGB121) | Same mode Chuckie Egg uses |
| Sprites | MAXBLITBUF = 64 buffers, 5 layers, SPRITE SHOW SAFE | SPRITE_User_Manual.md |
| Scrolling | SPRITE SCROLL x, y [, fill] moves the background and layer 0 | SPRITE_User_Manual.md |
| Double buffering | FRAMEBUFFER CREATE / WRITE F / COPY F, N | Proven in Chuckie Egg |
| Sound | PLAY BBC SOUND / PLAY BBC ENVELOPE, four channels, 8-note queues | V6.03.02b6, proven on this board |
| Program memory | 144K; Chuckie Egg occupies 53K of it | MEMORY on the PC3 |
| RAM | 6 MB PSRAM, essentially unbounded for this game | MEMORY on the PC3 |
The reference point that matters: Chuckie Egg holds 30 fps with nine moving entities, a full-screen framebuffer copy every frame, and a HUD redraw. Thrust asks for more drawing and less logic.
Kieran Connell's disassembly ships three technical references alongside the annotated 6502. They are unusually good — the physics one in particular reads like a design document rather than a transcription. Everything below is from them.
All forces act on the midpoint, the centre of mass of the ship-and-pod system; ship and pod are derived by offsetting from it along the tether vector in opposite directions. With no pod, the midpoint is the ship. The tether is a damped pendulum driven purely by the tangential component of thrust — there is no gravity torque term, which is correct, because gravity accelerates both masses equally.
32 angles (11.25°). Rotation on 3 frames in 4. Forces on 6 ticks in 16. Thrust ÷16, or ÷32 with the pod. X drag 63/64, Y drag 255/256, tether damping 63/64.
For every scanline in the world the game stores the X of the left wall and the X of the right wall; the cave is the gap between. Both are RLE delta-compressed as (count, signed increment) pairs, decoded forwards and backwards so the 256-byte window can be extended at whichever edge the camera moves toward.
Six levels. The largest is 1262 scanlines deep. All six terrains together are under 250 bytes of source data.
Every sprite is plotted with EOR, so erasing is redrawing. Collision falls out for free: after XOR-erasing a pixel the result should be zero, and anything non-zero means something else drew over it. The test site is three bytes of self-modifying code patched between AND mask,X and JMP.
17 ship shapes cover 32 headings; the other 15 are horizontal mirrors. Plus a shield sprite.
Four OSWORD &08 envelopes and nine OSWORD &07 parameter blocks, sitting in the source as raw bytes. The explosion is the good one: a tone on channel 1 and noise pitch 7 on channel 0, which clocks the noise from channel 1's pitch so the bang is tuned rather than a hiss.
Lines 10786-10840 of thrust.6502.
The three reference documents are the best thing about that repository and they are what made this port cheap, but they are notes rather than a specification, and two claims in ship-physics.md do not match the 6502 beside them. Both were caught by reading the assembly when the behaviour looked odd, and both would have been silent bugs.
AND #$01 / EOR #$FF / ADC #$01, which sign-extends the integer byte that the rotates have shifted the sign out of, and the accumulate is ADC. The table already points the way the ship flies: heading 0 is up and its Y entry is −2.5. Negating it drives the ship backwards.ship_input_rotate is exactly AND #$03 / BEQ return, so three frames in four — but the frame is 33.3 Hz, not 50, giving 25 steps a second and a full turn in 1.28 seconds.draw_player_timed_to_vsync waits for the centisecond clock to reach 3, so the frame is 30 ms and the game runs at 33.3 Hz. Believing the notes made this port 1.5× too fast in every axis at once, which is hard to spot precisely because it is uniform: nothing looks wrong, the game is just unplayable.The terrain is drawn as two solid masses with a gap. Drawn naively — one horizontal line per scanline per wall — a 240-row screen costs 240 draws a frame, which would be marginal. But the RLE the original uses is mostly long runs at a constant X, so consecutive scanlines share a wall position and collapse into one rectangle. Decoding all six levels and sliding a 120-scanline window down each of them gives the real cost. The top two thirds of every level is open sky at a constant X — one fill, and the player never flies up there — so "typical" below is the median taken over the cave alone:
| Level | Scanlines deep | Cave starts | Fills, typical | Fills, mean | Worst frame | Objects |
|---|---|---|---|---|---|---|
| 0 | 692 | 426 | 13 | 20 | 43 | 4 |
| 1 | 833 | 430 | 49 | 45 | 106 | 5 |
| 2 | 929 | 440 | 18 | 19 | 47 | 13 |
| 3 | 1229 | 415 | 14 | 31 | 96 | 12 |
| 4 | 1174 | 420 | 36 | 35 | 80 | 19 |
| 5 | 1262 | 382 | 56 | 63 | 158 | 17 |
So a typical frame is 13 to 56 BOX calls and the pathological frame — a long diagonal on level 5, where every scanline genuinely does differ — is 158. That is the number to size the budget against, and it is comfortably inside 33 ms if a BOX costs anything like what Chuckie Egg's drawing did. Spike 1 confirms it on the board before phase 3 starts.
Two consequences worth noticing. First, the whole bidirectional RLE decoder — four decoder triples, a circular window, forward and reverse step functions — exists only to fit 256 bytes. We decode each level once at load time into two arrays and throw all of it away. Second, because we hold the entire wall profile, terrain collision becomes a handful of array lookups instead of a side effect of XOR plotting.
The BBC's terrain unit is a byte column (4 pixels) wide and, at its half-resolution rendering, 2 pixel rows tall, on a display whose pixels are about 1.07× wider than tall. Keep the game's arithmetic in those units and map x_px = 4 × world_x, y_px = 2 × world_y and the original geometry comes back within 7%. This matters more than it sounds: the angle-to-force tables are deliberately elliptical (Y range ~5.0 against X ~2.5) to compensate for MODE 1's pixel aspect. Take those tables verbatim and the compensation is already correct. Substituting SIN/COS would throw it away.
The BBC play area is 288 × 146 pixels — a 2.1:1 letterbox once pixel aspect is applied. Our 320 × 240 square-pixel screen is 1.33:1, so a full-screen playfield would show roughly half as much again vertically and make the game easier. Recommendation: 320 × 176 playfield with a 64-row panel beneath it for fuel, score, lives and the shield indicator. That is 80 × 88 world units against the BBC's 72 × 73 — 11% wider and 20% taller. Tunable, and worth playing both before fixing.
This section said 50 Hz until the game was playable enough to feel wrong. It is the most consequential correction in the port.
draw_player_timed_to_vsync does not wait for a vsync. It spins on OSWORD 1 until the BBC's centisecond clock reads 3 and then zeroes it, so a frame is 30 ms and the game runs at 33.3 Hz. level_tick_counter advances once per pass of tick_loop, and that counter is what gates the rotation, the six-in-sixteen force update and the tether torque — so every rate in the game is 33.3 Hz based, not 50.
So there is no accumulator and no second clock: pace to 30 ms, take one simulation step per frame, and every cadence falls out at the rate it had in 1986. The tick gating pattern survives because it is untouched.
Taking the disassembly's "50fps" at its word made the whole game half again too fast — gravity, thrust, turn rate, bullet speed, the pendulum and the countdown all 1.5× — which is exactly the sort of thing that reads as "this port is too hard" rather than as a bug.
SPRITE SCROLL is tempting but wrong here. It helps only when the exposed edge is cheap to fill, and Thrust's camera moves diagonally, so a 1-pixel horizontal scroll exposes a full-height strip that needs the same per-scanline work as a complete redraw. Redraw the visible window each frame from the decoded wall arrays, merging runs into BOX calls, into the framebuffer, then one FRAMEBUFFER COPY. This is the same pattern Chuckie Egg already runs at 30 fps, and the measurement above says it fits.
Generate all 17 shapes at build time from the disassembly's sprite data — its single-stream format, where $81-$FE encodes "end of row" and "first pixel of the next row" in one byte, is straightforward to decode in Python. Headings 17-31 are the mirror of 32−a, which the SPRITE SHOW rotation parameter gives us for free. Same gen_sprites.py pipeline as Chuckie Egg.
Up to 32 objects per level (the table above shows 4 to 19 in practice) as sprite buffers, well inside MAXBLITBUF. The 32 particle slots — player bullets, hostile bullets, stars, debris — are single pixels; draw them straight into the framebuffer with PIXEL rather than paying sprite bookkeeping for them.
We cannot get collision free from XOR, and we do not want to. Ship against terrain becomes six or eight hull points tested against wallL() and wallR() at the ship's own scanlines — a handful of comparisons, and more robust than the original, which could only notice a collision on the frame after it happened. Bullets and objects use the sprite engine's collision or plain AABB.
shield_tractor_pressed is one key doing one job. Holding it burns a unit of fuel a frame on two frames in four, draws the circle in place of the ship, and runs the engine — and that is the whole of it. There is no path in the 6502 where it stops plot_ship_collision_detected reaching destroy_player_ship, and sheild_tractor_flag is read in exactly one place, to choose the sprite. The bubble is the tractor field made visible, not armour; the name in the disassembly is the author’s reading of the picture. So it is implemented as what it is, which means it costs fuel and gives you a wider outline to catch the rock with.
FILL PATTERN 15 is exactly that stripe. It would even land world-locked, because our terrain scanline is two pixels and the camera moves in whole scanlines. But the patterned rectangle fill is a per-pixel DrawPixel loop over 72,000 pixels a frame, and the rectangle and polygon paths clamp the pattern index to 7 while the circle path allows all 32 — so it would need a firmware change as well as a frame-time one. Left solid.| Original technique | Why it existed | What we do |
|---|---|---|
| EOR plotting for everything | Erase without storing background | Framebuffer and a full redraw |
| Self-modifying collision test | Free collision during the erase pass | Explicit point tests against the wall arrays |
| 256-byte circular terrain window, four decoder triples, reverse RLE decoding | The wall arrays had to fit two pages | Decode the whole level once at load |
| Q7.8 / Q7.16 fixed point | No FPU, and 6502 multiply is expensive | MMBasic floats; same constants, same order of operations |
| CRTC reprogrammed to 72 columns | Save 2K of screen RAM | Plain MODE 2 |
| Half-resolution striped terrain | Halve the plot cost | Solid fills — cheaper for us, since merged runs get taller. Offer striping as a cosmetic toggle if the look is missed. |
As with Chuckie Egg, the OSWORD &07 blocks are already in PLAY BBC SOUND's parameter order — channel word, amplitude, pitch, duration — so they transcribe rather than translate. Amplitudes 1-4 select the four envelopes; negative amplitudes are plain volumes.
| Block | Bytes | MMBasic | Notes |
|---|---|---|---|
sound_data_own_gun | 12 00 01 00 50 00 02 00 | PLAY BBC SOUND &H12, 1, 80, 2 | Channel 2, flushed, envelope 1 |
sound_data_explosion_1 | 11 00 02 00 96 00 64 00 | PLAY BBC SOUND &H11, 2, 150, 100 | Tone that also sets the noise pitch |
sound_data_explosion_2 | 10 00 03 00 07 00 64 00 | PLAY BBC SOUND &H10, 3, 7, 100 | Pitch 7 = white noise clocked by channel 1 |
sound_data_hostile_gun | 13 00 04 00 1E 00 14 00 | PLAY BBC SOUND &H13, 4, 30, 20 | Channel 3, envelope 4 |
sound_data_collect_1 | 02 00 F1 FF BE 00 01 00 | PLAY BBC SOUND 2, -15, 190, 1 | Queued, not flushed |
sound_data_collect_2 | 02 00 00 00 BE 00 02 00 | PLAY BBC SOUND 2, 0, 190, 2 | Amplitude 0 — the rest after it |
sound_data_engine | 10 00 F6 FF 05 00 03 00 | PLAY BBC SOUND &H10, -10, 5, 3 | Retriggered while thrusting |
sound_data_countdown | 02 00 F1 FF 96 00 01 00 | PLAY BBC SOUND 2, -15, 150, 1 | The ten seconds after the reactor goes |
sound_data_enter_orbit | 12 00 03 00 B9 00 01 00 | PLAY BBC SOUND &H12, 3, 185, 1 | Envelope 3 on a tone channel, not the noise |
The envelopes decode the same way. Envelope 1, the gun, is PLAY BBC ENVELOPE 1, 2, -5, -3, -5, 2, 3, 50, 126, -7, -7, -12, 126, 0; envelope 3, the explosion, is PLAY BBC ENVELOPE 3, 4, 0, 0, 0, 1, 1, 1, 126, -4, -2, -4, 126, 110; envelope 4, the hostile gun, is PLAY BBC ENVELOPE 4, 1, -1, -1, -1, 18, 18, 18, 50, -12, -12, -12, 110, 70.
FRAMEBUFFER COPY. Standalone that is 82 to 111 fps. The striped half-resolution fallback is not needed.gen_terrain.py decodes the six RLE levels to wall arrays; gen_sprites.py pulls the 17 ship headings, the pod, the shield and the nine object shapes; gen_objects.py reads the per-level object tables; gen_sound.py emits the envelopes and blocks as PLAY BBC. All four write into out/, and all but the last draw what they extracted.
The check that turned out to be worth having is not a screenshot comparison but an arithmetic one. gen_objects.py reports what fraction of each object stands inside rock. Across all six levels every one of the 33 limpet guns comes out at exactly 0.25 buried, every one of the 19 fuel cells at 0.30, every pod and door switch at 0.00 and every reactor at 0.10. Those figures are identical level to level because the designer placed objects against the wall, so they can only come out clean if the terrain decoder and the object tables agree to the column. They did not at first: the right wall starts at $FF rather than 0 and segment 0 of the RLE is never read, and getting that wrong put every cave 255 scanlines below its own objects.
Wall arrays in, camera position in, run-merged fills out. Bas/thrust.bas decodes a level's RLE at load, draws the cave, and flies the camera about with the arrow keys; N and P change level and S writes a screenshot.
Drawing the cave beats drawing the walls. The plan assumed two fills per run, one for each wall. Filling the whole playfield with rock and then punching the cave out of it in black is strictly cheaper, because the two walls rarely change on the same scanline and a run only breaks when either does: the worst frame drops from 149 boxes to 112, and the typical frame by a quarter. It also replaces "clear, then draw two walls" with "one fill, then one box per run".
| Level | Frames swept | Boxes avg | Boxes max | ms avg | ms max | fps |
|---|---|---|---|---|---|---|
| 0 | 267 | 12 | 30 | 9.0 | 12 | 111 |
| 1 | 404 | 24 | 62 | 10.5 | 16 | 94 |
| 2 | 490 | 11 | 44 | 9.1 | 13 | 109 |
| 3 | 815 | 24 | 81 | 10.5 | 19 | 94 |
| 4 | 755 | 26 | 63 | 10.8 | 16 | 92 |
| 5 | 881 | 19 | 93 | 12.2 | 20 | 82 |
Measured by the Benchmark routine, which is now on the B key so it can be re-run after any change. It flies the camera down every scanline of every level, drawing exactly as the game will, and prints the table above on the board itself.
The force pipeline is in the original's order — rotate, integrate, derive position, compute next frame's force — with its constants: gravity per level, the angle tables verbatim, thrust over 16, X drag 63/64 and Y drag 255/256, the six active ticks in sixteen, rotation on three frames in four. The simulation runs at a fixed 50 Hz off an accumulator under the 30 Hz draw, so the irregular tick pattern survives. Collision is eight hull points against the wall arrays.
Checked by driving SimStep from the board's command prompt rather than by eye: 48 steps of full thrust from rest in open sky, at each of the four cardinal headings.
| Heading | Δy | Δx | Expected |
|---|---|---|---|
| 0, up | −58.37 | 0.00 | lifts against gravity |
| 8, right | +8.34 | +30.93 | free-fall Y, thrust X |
| 24, left | +8.34 | −30.93 | exact mirror of right |
| 16, down | +75.05 | 0.00 | +66.7 either side of free fall, symmetric with up |
| no thrust | +8.34 | 0.00 | gravity alone, no fuel spent |
Up and down are symmetric to the second decimal about free fall, left and right are exact mirrors, and fuel came off at 18 units in 48 steps — six in sixteen, as the tick gate says. The vertical response is 2.16× the horizontal, which is the table's 2:1 ellipse plus X drag being four times stronger: that asymmetry is the feel of Thrust, and it comes out of the data without being asked for.
The figures above were taken before the frame rate was corrected. They are still right per step — the per-step physics did not change — but each of those 48 steps now takes 30 ms rather than 20, so the ship accelerates over distance at 4/9 of what it did and tops out at 2/3 the speed. A full turn takes 1.28 seconds, not 0.85.
The two thresholds are not a near and a nearer. $75 starts the beam and $84 attaches, and $84 is the larger of the two: you hold the tractor key, fly down until the beam appears, and then pull away, and the pod lifts off its stand as the line goes taut. Both numbers straddle the tether's own length, which is what makes that feel like lifting rather than snapping. The distance metric is the smaller axis plus three times the larger, in pixels.
The tether is a circle of 20 pixels' radius, which is not visible in the tables at all. The delta is four times the angle table, so five columns by ten scanlines — and a column is four pixels against a scanline's two, so both come to twenty. attach_pod_to_ship scales its search vector by 4 in X and 2 in Y for exactly that reason, and once you see it the seven-pass binary search is just ATAN2 on a circle.
Measured on the board: beam on at 8 columns with the key held, attached on pulling out to 12, separation 40.0 pixels exactly, midpoint dead between the two.
| 48 steps, pod attached | tether angle | angular vel | What it shows |
|---|---|---|---|
| coasting | 0.000 | 0.00000 | gravity moves the midpoint and does not twist the tether |
| thrust along the tether | 0.000 | 0.00000 | no torque on axis; Δy −25.02 against −58.37 without the pod, so thrust is exactly halved |
| thrust across it | 9.410 | 0.28150 | a hundred degrees of swing in a second |
| then 160 steps coasting | 15.975 | 0.28150 | the swing does not decay |
That last row is correct and worth knowing. The damping term sits inside the thrust branch in the 6502 — the no-thrust path jumps past both the torque and the damping — so a swinging pod keeps swinging until you thrust again. Physically that is right: both masses are in free fall together, so there is no restoring force and the tether simply rotates. It is also why a careless grab has the pod whirling round the ship, which is the game.
Crashing while carrying the pod respawns with it attached hanging straight below, as level_reset_with_pod_flag does.
Every object is drawn in its level's own palette — the nine shapes are four-coloured and two of those colours change per level, so they are reloaded at each LoadLevel. Guns fire on gun_param: bits 2-4 are the angle the gun points and bits 0-1 pick a spread mask of 1, 3, 7 or 15, so a wide-mask gun sprays and a narrow one aims. Bullets take the angle table's own velocity, unscaled, which is five pixels a step. The reactor takes fifty hits and then the planet goes and the ten seconds start.
| Level | objects | particles | sim ms | draw ms | total | fps |
|---|---|---|---|---|---|---|
| 0 | 4 | 6 | 4.82 | 14.82 | 19.64 | 51 |
| 0 | 4 | 32 | 12.57 | 17.92 | 30.49 | 33 |
| 2 | 13 | 6 | 5.75 | 15.42 | 21.17 | 47 |
| 2 | 13 | 32 | 11.56 | 16.90 | 28.46 | 35 |
| 4 | 19 | 6 | 6.24 | 17.38 | 23.61 | 42 |
| 4 | 19 | 32 | 12.25 | 18.69 | 30.94 | 32 |
| 5 | 17 | 6 | 6.09 | 18.82 | 24.91 | 40 |
| 5 | 17 | 32 | 11.98 | 20.18 | 32.16 | 31 |
Re-measured after the frame rate was corrected. The budget is a 30 ms frame now rather than 33, but the numbers improved anyway, because the simulation runs once a frame instead of 1.65 times.
Six particles is a busy moment and thirty-two is the instant after something large explodes. Play sits in the first of those and has five to ten milliseconds in hand against the 30 ms frame; only level 5 with a completely full pool goes over, by two. The cost is the pool and not the objects — level 0 with four objects costs as much as level 4 with nineteen — which is what an MMBasic array access costs when you make five hundred of them a step. Halving the pool would buy the worst case back, and the debris bursts have already been trimmed from twelve pieces to eight.
You start a game with an empty tank, which reads as a bug in the disassembly until you see where the ship is put down. add_fuel is only ever reached from the tractor beam on a fuel cell, and the level reset table starts every level at the same spot — which, once the sprite-origin offset is applied, is exactly above the first fuel cell, to the column. You fall onto it, hold the beam, and fly off with a full tank. That the two independent corrections (plot origin to centre, and the fuel cell's own centre) agree to the column is the strongest confirmation that either is right.
Refuelling is a box, not a range, and that was a bug here until play found it. I had assumed it reused the tractor beam's distance; collecting_fuel_flag is set by a test that the cell is 1 to 5 columns to the right of the ship and 0 to 27 scanlines below it. Both subtractions are unsigned, which is what makes it one-sided: you have to be above the cell and almost on top of it. And a cell is a fixed ration rather than a tap — obj_tractor_counter counts frames on the beam and at 26 the cell is consumed, scoring 30, so one cell is always 286 units of fuel however you approach it.
All nine are wired: the gun, the hostile gun, both halves of the explosion, the pickup, the engine retriggered on every active thrust tick, the countdown, and entering orbit. The envelope 2 correction described above is the only thing that is not the original's bytes.
Title screen, four ships, a mission bonus of 400 × (level + 5) plus 2000 for the planet, gun probability climbing from 2 in 256 to a cap of 35 from mission three, a high score, and game over.
The two modifiers turn over on a two-bit counter and it is worth reading start_new_level closely to see it. At the end of each set of six it inverts reverse gravity — and only when that switch leaves gravity normal again does it invert the invisible landscape. So the rotation is normal, reversed, invisible, both, coming round after twenty-four missions.
Reverse gravity is not a mirror of the pull it replaces. initialise_level_pointers does EOR #$FF on the fraction with the integer byte set to −1, which comes to -(FRAC + 1) / 256 — a shade stronger. On level 0 that is −0.023437 against +0.019531, and the ship is put down pointing at angle 16 instead of 0.
Checked by playing to mission 8 with the N shortcut: level 1, reverse gravity on and invisible still off, gravity −0.031250 (exactly -(7 + 1) / 256 for that cave), gun probability 8 after six increments, and 20000 of bonus — which is 2000 + 2400 + 2800 + 3200 + 3600 + 4000 + 2000, the documented progression to the digit.
| Risk | Likelihood | Mitigation |
|---|---|---|
| Terrain redraw does not fit the frame | Closed | Measured on the board in phase 2: 20 ms in the worst frame of the worst level, 9 to 12 ms typically, copy included. Thirteen milliseconds of the budget are left for everything else, which is what S2 has to fit into. |
| 64 moving things against Chuckie Egg's nine | Confirmed, contained | Measured in phase 5. A busy frame — every object alive, the pod on the tether, six particles — runs at 35 to 45 fps. A completely full 32-slot pool runs at 26 to 29, so the frame stretches by a few milliseconds for the fraction of a second after an explosion. The cost is the pool itself and not the objects: level 0, with four objects, costs as much as level 4 with nineteen. If it ever needs to go, the pool is what shrinks. |
| The flight model does not feel like Thrust | Medium | The likeliest cause would be substituting trigonometry for the elliptical angle tables, or rescaling the tick gating to 30 Hz. Both are explicitly ruled out above. Phase 3 exists to catch it early. |
| Letterbox versus full screen changes difficulty | Certain | It is a design decision, not a bug. Spike S3 makes it deliberately. |
| Sprite data transcription errors | Low | Render every extracted shape to PNG and eyeball it, as with Chuckie Egg's hens. |
Bas/thrust_tools/
thrust.6502 the disassembly - fetched, NOT vendored
thrustdata.py shared reader, colours, geometry
gen_terrain.py six levels of RLE; --stats and --png
gen_objects.py object tables; --png, and the buried-fraction check
gen_sprites.py 17 ship headings, pod, shield, nine object shapes
gen_sound.py 4 envelopes + 9 blocks -> PLAY BBC
out/ generated .bas fragments and verification images
thrust_code.bas the game, without generated data (phase 2 on)
build.py glue, writing ../thrust.bas (phase 2 on)
One correction carried over from Chuckie Egg: build.py will write straight to Bas/thrust.bas. A build left sitting in the tools directory is a build nobody runs.
docs/ship-physics.md, docs/landscape-drawing.md and docs/sprite-drawing.md. Disassembly by Kieran HJ Connell, 2016; original notes by jms2; BeebDis by Phill Harvey-Smith; verified against BeebAsm by Rich Talbot-Watkins.docs/Elite_Port_Plan.html, Bas/chuckie_tools/, docs/PLAY_BBC_User_Manual.md — our own precedents.