Port plan · PicoMite MMBasic · September 2026

Elite on the PicoComputer 3

A phased plan for bringing the 1984 BBC Micro Elite (cassette version) to the PC3 in MMBasic, built on the firmware's Draw3D engine, the twelve ship meshes already proven in Bas/3ddemo.bas, and the annotated source at bbcelite.com.

Rendering below: the real Cobra Mk III mesh from 3ddemo.bas, drawn with the same backface rule Draw3D uses.

Recommendation

Wireframe with backface culling is both the cheapest mode and the authentic one

The original game never did hidden-line removal. Its rule was "draw an edge if either face it belongs to faces the viewer", and that is exactly what Draw3D produces when faces are created without a fill colour. Filled faces cost about the same (the triangle fill runs in C). True hidden-line mode (depthmode=2) is the expensive one: it rasterises a float depth buffer over the ship's screen box every frame, which for a close ship is more RAM than the whole MMBasic heap.

Default: authentic wireframe

Draw3D CREATE with no fill array, Draw3D WRITE n,x,y,z,0,0 into a framebuffer, one FRAMEBUFFER COPY per frame. Overlapping ships simply overdraw, as on the BBC. Works on every RP2350 variant.

Option: solid ships

Same meshes with a fill array. Costs roughly one triangle fill per face on top of the edges. Ships must be drawn far-to-near (sort by z in BASIC, trivial) because the engine has no inter-object depth. Offer it as an in-game toggle; it is the Archimedes look, not the BBC look.

Reserve: hidden-line

Use only where one large object fills the screen and the frame rate does not matter: the title-screen ship and the docking-bay view. Not for flight.

Ships beyond visibility range

As in the original, a ship further than its blueprint's visibility distance is a single PIXEL. This is the main frame-rate lever: only ships close enough to have shape cost a Draw3D call.

Decisions taken · 9 September 2026
  1. MODE 2 (320x240, 16 colours). Runs on every PC3 variant; display plus F buffer is 76800 bytes of the framebuffer pool.
  2. MAX3D raised from 8 to 12 in configuration.h. The only array sized by it is the 13-entry pointer table, so the cost is 16 bytes of BSS. Needs a rebuild and flash before Phase 0 can create more than 8 objects.
  3. Draw3D CLOSE ALL off-by-one explained below; fixed in graphics/Draw3D.c (loop now 1..MAX3D), uncommitted alongside the MAX3D change.
  4. PSRAM is present but nothing depends on it. The budget below fits the plain heap. Hidden-line mode is the only feature that needs PSRAM, and it stays optional (title screen only, skipped when MM.INFO(PSRAM SIZE) is 0).
  5. Single program, with every table external and the docked and flight halves kept separable so a CHAIN split stays a mechanical fallback if the tokenised size cap is reached.

The CLOSE ALL bug

Objects are numbered 1 to MAX3D and live in struct3d[MAX3D + 1]; index 0 is never used. closeall3d() (Draw3D.c line 303) loops for (i = 0; i < MAX3D; i++), so it frees indices 0 to MAX3D - 1 and never visits the last object. HIDE ALL a few lines later loops 1 .. MAX3D correctly. closeall3d() is also what CloseAllFiles() calls when a program ends, so a program that finishes (or errors) with the last object open leaves its pointer dangling after the heap is reinitialised: the next run's Draw3D CREATE of that number fails with "Object already exists" and any SHOW of it reads freed memory. With MAX3D at 12 the victim becomes object 12, which Elite will use. Fixed: the loop is now for (i = 1; i <= MAX3D; i++). LIGHT, SET FLAGS and RESET now raise the same "object does not exist" error as SHOW instead of dereferencing a missing object, and SET FLAGS rejects a face number outside 0..nf-1 (a negative face used to pass the upper-bound test and write before the flags array).

Ground truth

What the PC3 gives us

The PicoComputer 3 is an RP2350B machine. Everything below is from the firmware source and the PC3 manual, not from the BBC's constraints.

ItemPC3 / PicoMite factWhat it means for Elite
CPURP2350B, 252 MHz default, 315 or 378 MHz via OPTION CPUSPEED (tied to the HDMI pixel clock)Run the game at 378 MHz. Both the interpreter and the SPI/HSTX paths scale.
RAMMMBasic heap 152 KB (HDMIUSB), 144 KB (HDMIWEB), 180 KB (HDMIBTH). 8 MB PSRAM if OPTION PSRAM PIN is set; GetMemory spills to PSRAM for requests over half the heap.Ship objects (about 5 KB each) and all game arrays fit the heap comfortably. PSRAM is the safety valve for tables and the z-buffer.
Framebuffer pool153600 B on HDMIUSB; 96000 B on HDMIWEB/HDMIBTH (cut-down build)MODE 2 + F = 76800 B fits everywhere. MODE 5 + F = 153600 B fits HDMIUSB only.
DisplayHDMI 640x480 timing; MODE 2 = 320x240 x 16 colours (palette via MAP), MODE 5 = 320x240 x RGB332, both pixel-doubled by hardware. FRAMEBUFFER COPY F,N,B copies on core 1.Same 320x240 canvas Chuckie Egg used. Elite's 256x192 space view plus 56-row dashboard maps to 320x184 + 56 with the same proportions.
KeyboardUSB host. KEYDOWN(0) = keys held, KEYDOWN(1..6) = their codes; INKEY$ for typed characters. Arrow codes 128..131, F1..F12 = 145..156.Held-key flight controls work (Chuckie's ReadKeys is the template). Function-key screens as on the BBC.
GamepadDEVICE(GAMEPAD n, LX|LY|RX|RY|B|H|T) for USB and BLE padsOptional analogue pitch/roll; map later, not in the core.
AudioI2S DAC (PCM5102). PLAY SOUND 4 channels (S/Q/T/N/O waveforms), PLAY MODFILE, PLAY MODSAMPLE, PLAY SAMPLEBBC-style beeps from PLAY SOUND (the sfxdata.bas idiom); verify the sound generators run on the I2S path early in Phase 0.
StorageA: flash drive and SD card, VAR SAVE (16 KB flash area), RUN "file",args with MM.CMDLINE$, CHAIN (clears variables), LIBRARYCommander saves as a file. Data files (meshes, tokens, tables) on A: or SD.
Program sizeMAX_PROG_SIZE = HEAP_MEMORY_SIZE (tokenised). Chuckie Egg is 53 KB of source, 1664 lines.Elite will be 3 to 4 times Chuckie. Keep every table out of the program text from day one.
Maths in CMATH Q_CREATE / Q_MULT / Q_INVERT / Q_ROTATE / Q_VECTOR / Q_EULER, V_CROSS / V_NORMALISE / V_ROTATE, M_MULT, DOTPRODUCT, SCALE, INTERPOLATEOrientation and universe rotation stay in C. The interpreter only sequences them.

The keyboard, sound and display facts above are for the PC3 (USB host, HDMI). They do not hold for the PicoCalc, whose I2C keyboard cannot report held keys.

Ground truth

What Draw3D actually does

From graphics/Draw3D.c (1349 lines, compiled -Os, runs from flash). The published 3D manual is out of date in several places; the list below is what the parser and renderer do today, and every item matters for the port.

Model and camera

Visibility and drawing

Cost and memory

Design choice

Rendering modes compared

ModePer-ship costMemoryLookFidelity to BBC EliteVerdict
Wireframe, backface culled (depthmode 0, no fill)Normals + sort + one DrawLine per perimeter edge of visible facesObject onlyWhite lines on black, ships read as solid because rear faces are culledExact: same edge rule as LL9Default
Solid (fill index per face)Above plus one triangle fill per face (two per quad)Object onlyFlat-shaded ships, one colour per face groupArchimedes/Amiga look; needs far-to-near draw order across shipsToggle
Hidden-line (depthmode 2)Above plus per-pixel depth raster of every visible face and depth-tested edges4 bytes per bbox pixel, cached, up to 300 KB for a full-screen objectClean wireframe with correct occlusion, including concave partsBetter than the originalTitle and hangar only
Dot (PIXEL)One projection in BASICNoneSingle pixelExact: SHPPTBeyond visibility distance

Elite's ships are mostly convex, which is why the original looked right without occlusion. The exceptions (the Cobra's engine recesses, the Coriolis docking slot, the Python's rear) are handled in the original by winding and normals, and the converter carries those normals across, so mode 0 gives the same picture the BBC gave.

Per-face visibility distances in the blueprints (faces that are "always drawn when far") and per-vertex level-of-detail have no equivalent in Draw3D. Neither is visible at 320x240: the dot threshold covers the far case and the near case is drawn fully.

Architecture

Shape of the program

Frame loop

Plain DO loop, no SETTICK. Each iteration: read keys (INKEY$ then KEYDOWN), advance the simulation by the measured frame time, draw into F, FRAMEBUFFER COPY F,N,B. TIMER gives the delta. The BBC ran 5 to 12 frames a second and moved everything per frame; we scale the per-frame deltas by (our frame time / BBC frame time) so speed and turn rates feel the same at 25 fps as at 8.

Universe state

Twelve slots as parallel arrays: type, position (3 floats, Elite units), orientation quaternion (5 floats), speed, accel, pitch and roll counters, energy, AI flags, missiles, target, explosion timer, and the Draw3D object number (0 when the ship is a dot or absent). Slot 0 is the planet, slot 1 the sun or station, as in FRIN.

Orientation in C

Player pitch and roll rotate the universe: build r with MATH Q_CREATE, then for each slot MATH Q_ROTATE the position and MATH Q_MULT the ship's quaternion. A ship's own pitch and roll are a Q_MULT in its own frame; its nose vector is Q_ROTATE(q, [0,0,1]). Renormalise the quaternion every 16 frames, the same cadence as Elite's TIDY.

Data pipeline

Python tools in Bas/elite_tools/ (the chuckie_tools pattern): parse the beebasm source into mesh, token, market and ship-stat files; generate reference test vectors for the galaxy; assemble the final .bas. Nothing hand-copied from the assembler listing.

Memory budget (HDMIUSB, MODE 2)

ConsumerKBNotes
Framebuffer pool: MODE 2 display + F75Separate pool, not heap. Add L (37.5 KB) only on HDMIUSB.
12 Draw3D objects~60Heap. Created on spawn, closed on kill. Coriolis is the largest (16 v, 15 f).
Universe arrays (12 slots)~4Heap.
Mesh source arrays (12 blueprints)~12Loaded once from file; copied into each CREATE.
Text tokens, names, market tables~8Pre-expanded strings; spill to PSRAM if present.
Stardust, scanner, temp strings, locals~6
Heap total~90of 152 KB (HDMIUSB) or 144 KB (HDMIWEB). Hidden-line z-buffer is extra and PSRAM-only.

Screen layout (320x240)

Space view 320x184 (Elite's 256x192 at 1.25x width, trimmed 8 rows), dashboard 320x56 below it. The dashboard is line art on the BBC and stays line art here: drawn once into a background copy and blitted, with the moving indicators redrawn each frame. The 3D scanner ellipse, its ship sticks and the compass live in the dashboard. Text uses an 8x8 DEFINEFONT of the BBC MOS character set so the docked screens keep the original 40-column feel at 320 pixels.

Files

Bas/elite/
  elite.bas            the game (single program to start)
  data/ships.dat       12 meshes: header stats, vertices, faces (winding fixed)
  data/tokens.dat      expanded text tokens, name digraphs, system descriptions
  data/market.dat      17 commodities: base price, econ factor, base qty, mask, unit
  data/dash.bmp        dashboard bezel (optional; line art otherwise)
  elite.cmd            commander save (A: or SD)
Bas/elite_tools/
  blueprints.py        beebasm SHIP_* blocks -> data/ships.bas + ships.json (+ 3ddemo cross-check)
  meshcheck.py         scores meshview.bas auto output: engine verdicts vs stored normals
  pc3.py               COM3 console driver: probe, cmd, run (AUTOSAVE), grab (XMODEM screen capture)
  tokens.py            TKN1/QQ16 tables -> tokens.dat
  galaxy_ref.py        reference seeds/names/system data -> tests/galaxy_ref.txt
  build.py             assemble elite.bas from parts + DATA
Bas/elite/tests/
  bench.bas            Phase 0 frame-time harness
  meshview.bas         spin any ship in the three render modes; AUTOMODE runs the winding check
  sheet.bas            contact sheets of all ships for screen capture (captures/*.png)
  galaxy_test.bas      compares 256 systems x 8 galaxies against galaxy_ref.txt
Translation table

Elite mechanism to MMBasic mechanism

Elite (cassette source)MMBasic on the PC3Notes
Ship blueprints (XX21): vertices, edges with two face ids, face normals, visibility distancesships.dat with face polygons; Draw3D CREATE per live shipFaces are rebuilt from edge adjacency; winding set so the computed normal agrees with the stored one. 3ddemo.bas already has all 12 and is the cross-check.
INWK / K% ship data block, FRIN slots, NOSH = 12Parallel arrays, 12 slotsShuffle-down on KILLSHP as in the original; close the Draw3D object first.
Orientation vectors (sidev, roofv, nosev), TIDYUnit quaternion + MATH Q_*; normalise every 16 framesQuaternion is smaller and the engine wants one anyway.
MVEIT: apply player pitch/roll to all ships, move along nose, apply ship's own pitch/roll, dampOne SUB per slot per frame using Q_ROTATE/Q_MULTCounters and damping tables copied from the source.
LL9 ship drawing, SHPPT dotDraw3D WRITE when z ≤ visibility distance, else PIXELEngine culls and projects. Skip when z below near threshold or when the projected centre is far off screen.
Explosion cloud (DOEXP)BASIC particle burst from the ship's projected centre, sized by distance and ageEngine does not expose projected vertices; the original's per-vertex clusters are approximated.
Planet (PLANET, PL9): circle plus crater (cassette); meridians are the disc versionCIRCLE outline; crater as one ellipse of LINE segments from the rotated axis vectors16 to 64 segments depending on radius. Meridians can be added later from the disc source.
Sun (SUN): filled disc with fuzzy edgePer-scanline LINE with random edge jitter, as the originalRadius clamped; cache the last drawing to skip when unchanged.
Stardust (STARS front/side)18 particles, PIXEL/short LINEFront view: radial with speed; side views: horizontal drift; rear: inward.
Space views and axis flippingFlip x or z of every position and quaternion before drawing; camera unchangedFour views, F1 to F4 as on the BBC.
Dashboard (DIALS), 3D scanner, compassLine art into the dashboard strip each frameScanner: ellipse, stick per ship, dot colour by type.
Key logger (DOKEY)KEYDOWN(1..6) latched into flags, INKEY$ for one-shot keysChuckie's ReadKeys pattern; BBC letters kept, views and screens on F1 to F10 (key table in B4).
Galaxy and system seeds, twisting, names, system data, marketPure integer BASIC, bit-exactVerified against the Python reference over all 2048 systems.
Text tokens (TT27, QQ18)Pre-expanded strings in tokens.dat; only names and system descriptions generated at runtimeRemoves the recursive tokeniser entirely.
Tactics (TACTICS), aggression, missiles, ECM, fleeing, docking trafficOne SUB per ship, run every 8th frame per slot as in the original's schedulingBehaviour tables from the source.
Docking checks, station safe zone, docking computerAngle and speed checks in the station's frame; docking computer = timed auto-dockCassette version has no docking animation.
Sound (NOISE, BEEP, EXNO)PLAY SOUND ch, B, Q|N|T, f, v envelopes on a tickLaser, missile launch, ECM, explosion, hyperspace, beep, boop.
Commander save (SVE/LOD)Text file on A: or SDNo competition code.
Sequence

Phases

Each phase ends with something that runs on the PC3 and a measurable acceptance test. Sizes are relative to Chuckie Egg (about 1700 lines including data).

0Spike

Feasibility and numbers

Size: small · Output: a table of measurements and the five decisions above, settled
  • bench.bas: MODE 2 + F at 378 MHz; create 1, 4, 8 Cobras; measure ms per frame for wireframe, solid and hidden-line, plus the cost of CREATE/CLOSE, FRAMEBUFFER COPY, and a synthetic MVEIT-sized BASIC loop per ship.
  • Key handling: KEYDOWN reports up to six held keys; pick the flight keys so that pitch, roll, speed and fire do not ghost on a typical keyboard matrix (test with keytest.bas). PLAY SOUND on I2S is established.
  • Check OPTION PSRAM PIN and MM.INFO(PSRAM SIZE).
  • Decide the frame budget: target 25 fps with 6 drawn ships; floor is 15 fps with 8.

The harness, using the Cobra mesh from 3ddemo.bas (its DATA block is elided here):

' bench.bas  -  Phase 0 frame-time harness.  Run at OPTION CPUSPEED 378000.
OPTION EXPLICIT
MODE 2 : FRAMEBUFFER CREATE : FRAMEBUFFER WRITE F
Draw3D CAMERA 1, 320                       ' Elite's field of view on 320 px
DIM FLOAT v(2, 27), q(4)                   ' v(coord, vertex); q = w,x,y,z,m
DIM INTEGER fc(16), f(59), col(1), ec(16), fl(16)
DIM INTEGER n, ships, i, mode
DIM FLOAT t0
' ... READ the 28 vertices into v(0..2, i), the 17 face counts into fc(),
' ... the 60 face-vertex indices into f(); col(0)=RGB(WHITE): col(1)=RGB(GRAY)
' ... ec() all 0 (edge colour index), fl() all 1 (fill colour index)
q(4) = 1                                   ' m must be 1: the engine scales by m^2
FOR mode = 0 TO 2                          ' 0 wireframe, 1 solid, 2 hidden-line
  FOR ships = 1 TO 8
    FOR n = 1 TO ships
      IF mode = 1 THEN
        Draw3D CREATE n, 28, 17, 1, v(), fc(), f(), col(), ec(), fl()
      ELSE
        Draw3D CREATE n, 28, 17, 1, v(), fc(), f(), col(), ec()    ' 9 args, no fill = wireframe
      ENDIF
    NEXT n
    t0 = TIMER
    FOR i = 1 TO 100
      CLS
      FOR n = 1 TO ships
        MATH Q_EULER RAD(i + n * 40), RAD(i * 2), 0, q()
        Draw3D ROTATE q(), n
        Draw3D WRITE n, (n - 4) * 60, 0, 900, 0, (mode = 2) * -2
      NEXT n
      FRAMEBUFFER COPY F, N, B
    NEXT i
    PRINT "mode"; mode; ships; " ships:"; (TIMER - t0) / 100; " ms/frame"
    FOR n = 1 TO ships : Draw3D CLOSE n : NEXT n
  NEXT ships
NEXT mode

Add a second run with a synthetic per-ship update (three MATH Q_ROTATE, one Q_MULT, twenty scalar statements) to stand in for MVEIT, and a third with the ships at z = 2500 to see how much of the cost is fill and line length rather than per-face overhead.

Accept when the numbers exist and the firmware changes (if any) are agreed.

Results, 9 September 2026 (PC3 on COM3, HDMIWEB V6.03.02b3, 378 MHz, PSRAM, MODE 2 + F)

Full listing in Bas/elite/tests/bench_results.txt; programs bench.bas, bench2.bas, keytest.bas alongside. Per-ship figures exclude the clear and the copy.

Per Cobra, msz = 300z = 500z = 900z = 2500
Wireframe (mode 0)1.020.730.500.35
Solid (mode 1)2.031.511.140.84
Hidden-line (mode 2)21.156.951.250.48
Fixed costmsNote
CLS of the framebuffer0.10
FRAMEBUFFER COPY F,N0.0838400 bytes
FRAMEBUFFER COPY F,N,B16.55Paces to the 60 Hz frame: this is the game's vsync
Draw3D CREATE + CLOSE0.19Spawn and kill per frame cost nothing
Synthetic MVEIT update, 8 ships2.073 Q_ROTATE, 1 Q_MULT, ~20 scalar statements each

Budget decided: one 60 Hz frame (16.5 ms) per iteration, paced by the background copy. Eight wireframe ships at z = 900 plus the MVEIT-sized update come to about 6 ms, leaving 10 ms for the dashboard, scanner, stardust, planet and tactics. Solid ships fit 60 fps with few ships close and 30 fps otherwise. Hidden-line at z = 300 took 21 ms per ship and a 164 KB z-buffer that only existed because PSRAM is fitted; it stays title-screen only.

Closed by Peter: KEYDOWN works as documented, up to six simultaneous keys like any USB keyboard, subject to the keyboard's own matrix (which keys ghost depends on their positions), and PLAY SOUND on the I2S path is proven. Neither is a risk. The one remaining item is flashing the MAX3D = 12 build, which the PC3 does not yet run.

1Data

Blueprint pipeline and mesh viewer

Size: small · Depends on: 0
  • blueprints.py: parse elite-source.asm SHIP_* blocks; build face polygons from edge adjacency; orient winding against the stored normals; emit header stats (speed, energy, laser, missiles, bounty, canisters, visibility distance, targetable area, explosion count) and the mesh.
  • Cross-check every mesh against 3ddemo.bas (same vertex count, same face count, same edge set).
  • meshview.bas: cycle ships, toggle the three modes, show the face count and frame time. This is where a wrong winding shows up as a missing or ghost face.

Accept when all 12 ships display correctly in wireframe and solid at three distances, with no face flagged by hand.

Results, 9 September 2026

elite_tools/blueprints.py reads the cassette elite-source.asm and writes Bas/elite/data/ships.bas. Every blueprint edge is drawn: boundary loops chained from the edge list, single-face detail edges as extra polygons, and bare lines (the Python's four fin lines, the Thargoid's two vents, the Cobra's nose spike, the Missile's eight fin lines) as sliver triangles with one extra vertex 0.5 units off the line. Nine ships came out identical to 3ddemo.bas; the demo's Python, Thargoid, Cobra and Missile were each missing lines and the Thargon was absent.

meshview.bas in auto mode asked the engine's DIAGNOSE for every polygon at three orientations and meshcheck.py compared each verdict with the stored normal: 444 verdicts, 0 mismatches on real faces, 3 deviations on sliver lines (inherent: a sliver's normal must be perpendicular to its line). Draw times per ship match Phase 0 (0.6 to 1.7 ms wireframe, 0.8 to 3.8 ms solid; the Coriolis is the dearest). Full table in tests/meshview_results.txt.

Solid mode must use depthmode 1 (vertex-based sort). With centroid sorting the coplanar detail quads on the Cobra's rear face can be painted over by the face itself; the demo already did this.

Captured from the PC3 (SAVE IMAGE, then XMODEM S into pc3.py grab):

All twelve ships, wireframeAll twelve ships, solidPython, Missile, Thargoid and Coriolis close upCobra Mk III solid, engine recesses drawn over the rear face
2Flight

Space flight core

Size: large · Depends on: 1
  • Universe arrays, spawn/kill with slot shuffle, player controls (pitch, roll, speed with the original's counters and damping), universe rotation, four views.
  • Ship rendering (WRITE or dot), stardust, crater planet, sun, station mesh in slot 1.
  • Dashboard: speed, roll, pitch, four energy banks, fore and aft shields, fuel, cabin and laser temperature, altitude, missile blocks, compass, 3D scanner.
  • Fixed test scene: launched from Lave's station, planet and sun placed by SOLAR, a Cobra and two asteroids drifting.

Accept when flying around the station with 6 ships in the bubble holds the frame budget and the scanner agrees with the view.

3Universe

Galaxy, hyperspace and spawning

Size: medium · Depends on: 2
  • Seeds, twisting, names, system data, and the reference test over all 2048 systems.
  • Short-range and long-range charts with cursor, fuel circle and distance; data on system screen.
  • Hyperspace countdown, arrival placement (SOLAR), mis-jump to witchspace with Thargoids, in-system jump (J) with its blocking rules.
  • Main-loop spawning: traders, pirates (1 to 4), police weighted by legal status, bounty hunters, asteroids, canisters, Thargoids, using the MCNT scheduling counter.

Accept when Lave, Zaonce, Diso and Riedquat come out with the right names, economies and prices, and a 10-system tour spawns plausible traffic.

4Combat

Weapons, tactics and damage

Size: large · Depends on: 3
  • Lasers: pulse, beam, military, mining; sight; laser temperature and overheating; hit test in the crosshairs using the targetable area.
  • Missiles: target lock, launch, ECM (ours and theirs), ECM energy drain.
  • Ship tactics: aggression, hostility, firing arcs, fleeing, missiles, cargo dumping, station police.
  • Energy, shields, damage messages, cargo canisters on death, explosion clouds, kill counting and combat rank, legal status, death screen.

Accept when a Cobra and two Sidewinders fight back convincingly, the player can die, and the kill tally advances the rank.

5Station

Docking and launching

Size: medium · Depends on: 2
  • Rotating Coriolis, safe zone, station-launched police, docking checks (approach angle, roll alignment with the slot, speed).
  • Crash on a bad approach; docking computer as an equipment item.
  • Launch tunnel of expanding squares, escape pod.

Accept when manual docking succeeds and fails for the same reasons as on the BBC.

6Docked

Docked screens and commander

Size: medium · Depends on: 3
  • Status, inventory, market (buy and sell with the original price and quantity algorithm), equipment shop with tech-level gating and prices, fuel.
  • Commander save and load (file), new commander (Jameson at Lave with 100 Cr), title screen with a hidden-line Cobra.

Accept when a trade loop Lave to Zaonce and back turns the expected profit and the save survives a power cycle.

7Polish

Feel, sound and options

Size: medium · Depends on: 4, 5, 6
  • Sound effects on the four PLAY SOUND channels; the original had no music.
  • Solid-ship toggle, gamepad mapping, key remap, difficulty tuning against the frame-time scaling.
  • Demo mode (ships fighting behind the title) and the disc-version split if program size demands it.

Accept when a fresh commander can reach Competent without hitting a bug and the manual page is written.

The cassette version has no missions (the Constrictor and Thargoid plans are disc-version features), no Trumbles, and no docking animation. They are out of scope here and could be borrowed from the disc source later.

Risks

What could sink it, and the fallback

RiskLikelihoodMitigation
Interpreter time per ship blows the frame budget with 8+ shipsMediumPhase 0 measures before design locks. Levers: run tactics on alternate frames per slot (the original already does), lower the dot threshold, fewer ships in the bubble, keep all vector maths in MATH.
Tokenised program exceeds MAX_PROG_SIZEMediumAll tables external from the start; LIBRARY for shared utilities; docked/flight CHAIN split with VAR SAVE state as the disc version did.
Firmware with MAX3D = 12 not yet flashed on the targetLowPhase 0 runs on the rebuilt HDMIUSB image; until then the bench caps at 8 objects.
Wrong winding on a concave face after conversionLowConverter checks each face's computed normal against the stored one; SET FLAGS 4 per face as the manual override.
Near-plane wrap when a ship is very closeLowSkip WRITE below a z threshold; collision fires first in the original at those ranges.
Per-object minimum of 4 KBLowTwelve objects stay under 64 KB; the CLOSE ALL leak is fixed.
Hidden-line z-buffer exhausts the heapLowOnly used on the title screen; falls back to wireframe if MM.INFO(PSRAM SIZE) is 0.
CopyrightNoteElite is copyright Ian Bell and David Braben; the bbcelite.com repositories carry no licence. This is a personal port using the published data, like the existing Chuckie Egg port. Keep that in mind before distributing.
Appendix A

Ship data

The meshes in Bas/3ddemo.bas (already rendering on the engine) and the blueprint statistics from the cassette source.

Meshes proven in 3ddemo.bas

ShipVerticesFacesFace-vertex entriesDemo zLargest face
Viper159346006 (rear)
Thargoid2094016008 (ring)
Escape pod44124003
Asp Mk II1913508005
Asteroid9144210003
Canister107303005
Cobra Mk III28176010007 (rear)
Mamba259306004
Missile179326004
Python1192815004
Sidewinder108266004
Coriolis station16155225004; docking slot is face 2 (vertices 12..15)

The demo scales blueprint coordinates by 2 (MATH SCALE vertices(),2.0) and uses the demo z column above to frame each ship at viewplane 600. Two things to know before reusing these meshes:

Blueprint statistics (cassette source, XX21 order)

Header fields decoded from each SHIP_* block. Bounty is the 16-bit word divided by 10. Laser and missiles come from byte 19 (%00lllmmm). Types 5 and 7 share the Cobra Mk III blueprint.

TypeShipVertEdgesFacesCanistersArea (side)Bounty CrVis distEnergySpeedNormal scaleLaserMissilesMax |coord|
1Sidewinder101570655.020703722064
2Viper (police)152070750231203212172
3Mamba2528517015.025903022264
4Python112613312020.04025020033224
5Cobra Mk III (hunter)28381339505015028123128
6Thargoid20261009950.05524039226164
7Cobra Mk III (trader)28381339505015028123128
8Coriolis station162814016001202400006160
9Missile1724904001424420068
10Asteroid921140800.550603010080
11Cargo canister10157020012171520024
12Thargon101570405.020203022040
13Escape pod4640160817830036

Other header bytes the game needs: the gun vertex (Cobra 21, Thargoid 15, all others 0), the explosion count (4n+6 with n from 1 for the missile to 12 for the station), and the edge heap size. Source constants: NOSH=12, NTY=13, COPS=2, THG=6, CYL=7, SST=8, MSL=9, AST=10, OIL=11, TGL=12, ESC=13, POW=15. The station's docking slot is the 20 x 60 rectangle of vertices 12 to 15 on the +z face.

Appendix B

Algorithm crib from the deep dives

Condensed from bbcelite.com so the implementer does not need to re-read the 6502 listing for the parts that must be bit-exact or that define the feel of the game.

Everything here was read from the cassette source pages, not the prose overviews, unless marked "verify". Formulas are written in MMBasic terms where that removes 8-bit trickery; the ones that must stay bit-exact (seeds, twist, names, market, RNG, checksum) are written as the 6502 does them.

B1 · Galaxy, system and name generation (bit-exact)

A galaxy is three 16-bit seeds s0, s1, s2 stored little-endian (bytes s0_lo s0_hi s1_lo s1_hi s2_lo s2_hi). Galaxy 1: s0=&5A4A, s1=&0248, s2=&B753. System 0 of galaxy 1 is Tibedied; each further system is four twists on. Lave sits at chart position (20, 173).

' twist (TT54), 16-bit arithmetic, carry out of the high byte discarded
tmp = (s0 + s1) AND &HFFFF : s0 = s1 : s1 = s2 : s2 = (tmp + s1) AND &HFFFF

' system data (TT24) from the current seeds
gal_x        = s1_hi
gal_y        = s0_hi \ 2
government   = (s1_lo \ 8) AND 7
economy      = s0_hi AND 7 : IF government <= 1 THEN economy = economy OR 2
tech         = (economy XOR 7) + (s1_hi AND 3) + (government + 1) \ 2   ' displayed as tech + 1
population   = tech * 4 + economy + government + 1                       ' tenths of a billion
productivity = ((economy XOR 7) + 3) * (government + 4) * population * 8 ' M Cr
radius_km    = ((s2_hi AND 15) + 11) * 256 + s1_hi

' name (cpl): 3 or 4 digraphs, on a COPY of the seeds
n = 3 : IF s0_lo AND 64 THEN n = 4
FOR k = 1 TO n : i = s2_hi AND 31 : IF i <> 0 THEN name$ = name$ + digraph$(i) : twist : NEXT

Digraph tokens 128 to 159, index 0 prints nothing and index 15 ("A?") prints just "A":

12345678910111213141516
ALLEXEGEZACEBISOUSESARMAINDIREA?
1718192021222324252627282930310
ERATENBERALAVETIEDORQUANTEISRION

Governments 0 to 7: Anarchy, Feudal, Multi-government, Dictatorship, Communist, Confederacy, Democracy, Corporate State. Economy 0 to 7: bit 2 clear is Industrial, set is Agricultural; 0 and 5 Rich, 1 and 6 Average, 2 and 7 Poor, 3 and 4 Mainly. Check value: Tibedied is Feudal, Poor Industrial, tech level 9 (stored 8), population 3.6 billion, productivity 11520 M Cr, radius 4610 km. Galactic hyperspace rotates each of the six seed bytes left by one bit, independently, and clears the legal record.

B2 · Market (bit-exact)

One random byte QQ26 is drawn on arrival in a system and saved with the commander; prices do not change until the next arrival. Economy is 0 to 7 as above.

price (0.1 Cr) = ((base + (QQ26 AND mask) + economy * factor) AND 255) * 4   ' 8-bit wrap is intentional
quantity       = base_qty + (QQ26 AND mask) - economy * factor
IF quantity < 0 THEN quantity = 0 ELSE quantity = quantity AND 63
#ItemBaseFactorUnitBase qtyMask
0Food19-2t600000001
1Textiles20-1t1000000011
2Radioactives65-3t200000111
3Slaves40-5t22600011111
4Liquor/Wines83-5t25100001111
5Luxuries196+8t5400000011
6Narcotics235+29t801111000
7Computers154+14t5600000011
8Machinery117+6t4000000111
9Alloys78+1t1700011111
10Firearms124+13t2900000111
11Furs176-9t22000111111
12Minerals32-1t5300000011
13Gold97-1kg6600000111
14Platinum171-2kg5500011111
15Gem-Stones45-1g25000001111
16Alien items53+15t19200000111

Check: at Lave (economy 5) food is 3.6 to 4.0 Cr and computers 89.6 to 90.8 Cr with none available.

B3 · Ship data block and scale

Each slot is 36 bytes (INWK). Positions are 24-bit sign-magnitude, ±8388607, relative to the player, z into the screen. Orientation is three vectors (nosev forward, roofv up, sidev right) of 16-bit sign-magnitude components with unit length 96 (&6000); nearly every calculation uses only the high bytes (±96). A fresh ship has sidev (96,0,0), roofv (0,96,0), nosev (0,0,-96), nose toward the viewer.

BytesFieldNotes
0-8x, y, z (lo, hi, sign each)
9-26nosev, roofv, sidev (x_lo, x_hi, y_lo, y_hi, z_lo, z_hi each)
27speed 1-40NPC moves 1.5 x speed units per frame; player DELTA 0-40 units per frame
28acceleration, signedapplied then zeroed each frame
29, 30roll counter, pitch counterbits 0-6 magnitude, bit 7 direction; 127 = rotate forever (station, asteroids); otherwise decremented by 1 per frame
31flagsbits 0-2 missiles, 3 on screen, 4 on scanner, 5 exploding, 6 drawn/firing, 7 killed
32AI bytebit 7 AI on, bits 1-6 aggression 0-63, bit 0 ECM fitted
33-34line heap pointernot needed: the engine redraws
35energyblueprint byte 14 is the maximum

Scale: projection is 256 * value / z on a 256 x 192 view centred (128, 96), so 320 pixels wide means a 320-pixel focal length. The planet has radius 24576 (96 0) and is placed 65536 ahead on arrival, filling the view exactly; the sun is the same size, 1 to 7 planet-distances further. Ships are removed beyond 57344 on any axis (FAROF). The in-system jump moves 65536. Vertex coordinates in the blueprints are in the same units as positions (a Cobra is 256 units across), so the meshes go into Draw3D unscaled and WRITE takes the raw position. The blueprint visibility byte is compared with the distance reduced to 0..31 (LL9 part 2, which looks like the largest high byte divided by 8, so units of 2048: verify); Cobra 50 is never reached inside the bubble, Sidewinder 20 becomes a dot beyond about 40000, canister 12 beyond about 24000.

B4 · Player controls and the feel

Roll JSTX and pitch JSTY are 1..255 centred on 128. A held key moves roll by 7 and pitch by 14 per frame (saturating; pushing the other way from beyond centre first snaps to 128). With damping on, the value moves 1 toward 128 per call, called twice per frame for roll and once for pitch. Then:

d = JSTX - 128 : ALP1 = ABS(d) \ 4 : IF ALP1 < 8 THEN ALP1 = ALP1 \ 2       ' 0..31, sign in ALP2
e = ABS(JSTY - 128) + 4 : BET1 = e \ 16 : IF BET1 < 3 THEN BET1 = BET1 \ 2   ' 0..8,  sign in BET2
alpha = ALP1 / 256 : beta = BET1 / 256                                       ' radians per frame

Full roll builds in about 18 frames and decays at 2 per frame; full pitch builds in 9 and decays at 1. Speed DELTA is 0..40. These per-frame constants are what the frame-time scaling in the architecture section must preserve.

BBC keyActionBBC keyAction
S / Xpitch down / pull upAfire laser
< / >roll left / rightT, U, Mtarget, unarm, fire missile
Space / ?speed up / slow downEECM
Jin-system jumpCdocking computer
HhyperspaceTAB, ESCenergy bomb, escape pod
f0-f3front, rear, left, right viewf4-f9long chart, short chart, data, market, status, inventory

On the PC3 keep the BBC letters (they are all plain ASCII in KEYDOWN) and put the views and screens on F1 to F10. Arrow keys can double S/X/</>.

B5 · Moving a ship (MVEIT), per slot per frame, in this order

  1. If (MCNT XOR slot) AND 15 = 0: TIDY the orientation (renormalise nosev, make roofv perpendicular, sidev = cross product). Each ship every 16 frames.
  2. If AI on: missiles run TACTICS every frame, other ships when (MCNT XOR slot) AND 7 = 0.
  3. Move forward: pos += nosev_hi * speed / 64 per axis (unit 96, so 1.5 x speed units).
  4. speed += acceleration, clamp 1..max, zero the acceleration.
  5. Rotate the position by the player's alpha and beta (below), then z -= DELTA.
  6. Rotate nosev, roofv, sidev by alpha and beta (small-angle, roll then pitch, using updated intermediates).
  7. Apply the ship's own pitch counter to (roofv, nosev) and roll counter to (roofv, sidev): X = X*(1 - 1/512) ± Y/16 : Y = Y*(1 - 1/512) ∓ X/16, about 3.6° per frame; decrement the counter unless it is 127.

Universe rotation by the player (MVEIT part 5), high bytes only:

k = y - alpha * x
z = z + beta * k
y = k - beta * z        ' uses the new z
x = x + alpha * y       ' uses the new y

With quaternions: build r from the roll angle about Z and the pitch angle about X, invert it, and apply Q_ROTATE to positions and Q_MULT to orientations. The shear the original picks up from its Minsky rotation is what TIDY corrects; a normalised quaternion has none, so the feel is the same without the drift.

View flipping (PLUT), applied to positions and all three vectors before drawing: rear negates x and z; left swaps x and z then negates the new z; right swaps x and z then negates the new x.

B6 · Tactics (TACTICS), when a slot gets its turn

  1. Missiles: any ECM active destroys them. Target reached when all three high bytes are 0 (within 256 units): 250 damage to the player if very close, else 80; a target with ECM has a 6% chance (rnd < 16) to fire it. A missile reaching the station is simply destroyed.
  2. Escape pods head for the planet. A hostile station spawns a Viper. A Thargon without its mothership drifts. Pirates inside the safe zone do not attack. energy += 1.
  3. CNT = nosev · unit vector to the player.
  4. 2.5% chance (rnd ≥ 250) of a long roll (counter = rnd OR 104). If energy > max/2 go to lasers; if > max/4 go to missiles; else 10% chance (rnd ≥ 230) of bailing out in an escape pod.
  5. Missiles: m = missiles held; fire if m > 0, no ECM active and (rnd AND 31) < m. A Thargoid launches a Thargon instead. "INCOMING MISSILE".
  6. Lasers: only if every position high byte is under 32. CNT as sign-magnitude: below 160 cannot shoot; 160 to 162 fires and misses (flash and sound); 163 or more hits for laser_power \ 2 damage (blueprint byte 19 shifted right once) and the attacker's acceleration is decremented.
  7. Steering: XX15 = unit vector to the target. If very close (z_hi < 3 and x_hi, y_hi under 2) head away; else head toward with probability (rnd OR 128) < AI_byte. Pitch counter = 3 with sign from roofv · XX15; roll counter = 5 (only if current roll < 16) with sign from sidev · XX15 XOR the pitch sign. CNT ≥ 22 means accelerate by 3; |CNT| ≥ 18 means decelerate by 1 (missiles 2).

AI byte at spawn: traders rnd \ 2 with AI off (they never fight in the cassette version); pirates and bounty hunters rnd OR 192 (AI on, aggression 32-63) with ECM if a second random byte is 245 or more; Thargoids, Thargons, missiles and escape pods 63 with AI on; cops 32-63; station %00000001; asteroids and canisters 0.

B7 · Main-loop schedule (MCNT counts down every iteration)

ConditionTask
every iterationkeys, essential dials, laser temperature -1, message timer
every 4the non-essential bars
MCNT AND 7 = 0if energy ≥ 128 recharge fore and aft shields by 1 each (1 energy each); then energy = MIN(255, energy + 1 + energy_unit)
(MCNT XOR slot) AND 7 = 0TACTICS for that slot
(MCNT XOR slot) AND 15 = 0TIDY that slot
every 16dial flash phase (8 on, 8 off)
MCNT AND 31 = 0station proximity check and spawn (B8)
MCNT AND 31 = 10altitude = sqrt(x_hi² + y_hi² + z_hi² - 36); negative is a crash; "ENERGY LOW" below 50
MCNT AND 31 = 20cabin temperature from the sun distance, death past 255; scoops add speed/8 fuel when ≥ 224, cap 70
MCNT wraps (every 256)spawning: rnd < 35 gives a benign spawn at z_hi 38 (a Cobra trader 50% of the time, else an asteroid, or a canister with rnd < 5, skipped in the safe zone or with 3 asteroids present); cops with probability from contraband and legal status; pirates unless rnd ≥ 90 or (rnd AND 7) < government: rnd ≥ 200 gives a pack of (rnd AND 3) + 1 Sidewinders or Mambas, else one of Mamba, Python, Cobra hunter, Thargoid

MCNT is set to 0 on launch, buying fuel and hyperspace, 1 after an in-system jump, 255 on death.

B8 · Station, safe zone and docking

The station is placed at planet centre plus 2 x nosev(planet) x radius, so at altitude one radius, when MCNT AND 31 = 0, no station exists and the player is within 49152 (192 0) of that point on every axis. It spawns pointing at the planet with roll counter 255 (permanent 3.6° per frame) and AI byte %00000001. Inside the safe zone nothing hostile spawns, pirates do not attack, and the compass points at the station.

Docking checks in order: station not hostile; station nose within 26° of pointing at us (its nosev_z ≥ 86 of 96); station in front; unit vector to the station has z ≥ 89 of 96 (a 22° cone); our roll matches the slot (|roofv_x| ≥ 80 of 96, within 34°). A failed approach below speed 5 bounces without damage, otherwise it damages or kills.

B9 · Weapons, damage and rank

Fire when the laser timer is 0, the fire key is held, laser temperature < 242 and the current view has a laser. Damage is power AND 127, 15 for both pulse and beam. Each shot adds 8 to the laser temperature, which cools by 1 per iteration. Beam refires every frame; pulse sets the timer to 10, decremented at 50 Hz, so 5 shots a second. Hit test: the target is in front, not the planet or sun, not exploding, both x_hi and y_hi are 0, and x_lo² + y_lo² < targetable area. On a hit energy drops by the damage; going negative kills (canisters spawn 50% of the time, up to blueprint byte 0). NPC lasers hit the player for laser_power \ 2.

RankKillsRankKills
Harmless0-7Above Average64-127
Mostly Harmless8-15Competent128-511
Poor16-31Dangerous512-2559
Average32-63Deadly2560-6399
Elite6400 and up

One kill point per kill in the cassette version; "RIGHT ON COMMANDER" each time the low byte wraps. Killing a Viper sets the fugitive bit.

B10 · Drawing the rest of the view

B11 · Random numbers, text and the commander file (bit-exact where it matters)

' DORND: four seed bytes r0..r3; DORND2 clears the carry first, DORND does not
a = r0 : c = a \ 128 : a = ((a * 2) AND 255) OR carry_in : x = a
a = a + r2 + c : c = a \ 256 : a = a AND 255 : r0 = a : r2 = x
a = r1 : x = a
a = a + r3 + c : c = a \ 256 : a = a AND 255 : r1 = a : r3 = x
' result: a (random byte); carry c survives to the next call

Text tokens (TT27): 0-13 control codes (cash, galaxy number, current and selected system, commander name, fuel/cash line, sentence case, beep, all caps, tab to column 21, newlines), 14-31 recursive tokens 128-145, 32-95 ASCII, 96-127 recursive, 128-159 digraphs, 160-255 recursive tokens 0-95. Stored text is XORed with 35. The Python tool expands every fixed token to a plain string so only names, numbers and case handling remain at runtime.

Commander file: 76 data bytes. 0 mission flags; 1-2 chart position (20, 173 = Lave); 3-8 galaxy seeds; 9-12 cash in 0.1 Cr (1000.0 Cr = 10000; the source table shows the bytes as E8 03 00 00, so confirm the byte order against a real save); 13 fuel 70; 15 galaxy number; 16-19 lasers front/rear/left/right (15, 0, 0, 0); 22 hold size 22; 23-39 cargo x 17; 40-69 equipment flags, missiles, legal status, market availability x 17; 70 QQ26; 71-72 kills; 73 = 128; 74 CHK2 = &AA; 75 CHK = &03. Checksum: a = 73 : c = 0 : FOR x = 73 TO 1 STEP -1 : a = a + b(x-1) + c : c = a \ 256 : a = (a AND 255) XOR b(x) : NEXT gives CHK, and CHK2 = CHK XOR &A9. Our port only needs this if it wants to import real BBC saves; a plain text file is otherwise fine.

Appendix C

Sources