Your combat rank is decided by one number: a running total of kill-points that only ever goes up. This page is the full breakdown of how that number works, what every ship is worth, and which targets actually move the needle.
Addresses below are absolute Z80 addresses. CODE.COM loads at 0x0100, so
addr = file_offset + 0x100. Everything here was read from the disassembly
(tools/zdis.py) and confirmed against live openMSX RAM dumps; SHAPES.DAT relocates
to 0xa857 at load, which matters below.
The tally is a single 24-bit counter at 0xeb13/14/15 (low byte 0xeb13, high byte
0xeb15). The scorer at 0x32e7, called from the ship-destruction handler at 0x31d5,
adds the destroyed ship’s own value to it:
0x32e7 ; kill scorer
LD HL,(0xeb13) : LD A,(0xeb15) ; running kill-points total (24-bit)
LD E,(IY+0x18) : LD D,(IY+0x19) ; THIS ship's value, from its blueprint
ADD HL,DE : ADC A,0 ; total += ship value
LD (0xeb13),HL : LD (0xeb15),A
IY is the killed ship’s blueprint pointer. The lookup that gets there is:
ship destroyed -> A = ship type (shipblock + 0x1a)
CALL 0x2fe9 : HL = 0xa857 + type*2 ; blueprint pointer table at 0xa857
IY = (HL) ; IY -> this type's blueprint
scorer : tally += (IY+0x18/0x19) ; the type's 2-byte kill value
So the reward is a per-ship-type field at blueprint offset +0x18/+0x19, not a flat +1.
It is effectively each ship’s bounty, doubling as its rank award. The pointer table lives
at 0xa857 because that is exactly where SHAPES.DAT loads: its first bytes are the
blueprint pointer table, relocated to absolute addresses at load.
The rank index is a single byte at 0xeb12, running 0 to 8, that only ever increases and
is saved in the commander file. Promotion is checked on every kill against a threshold
table at 0x336d:
LD A,(0xeb12) ; current rank index (0..8)
ADD A,A ; x2 (2 bytes per entry)
LD HL,0x336d + idx ; -> threshold table
... SBC HL,DE / SBC A,C ; compare 24-bit tally vs threshold
JR c,not_promoted
LD HL,0xeb12 : INC (HL) ; promote one rank
-> print "Right On Commander!" (msg 0x3396)
Because the compare is one notch at a time against monotonically rising thresholds, you can never skip a rank. The full table:
| Rank | Kill-points to reach | hex | bytes at 0x336d |
|---|---|---|---|
| Harmless | 0 (start) | n/a | n/a |
| Mostly Harmless | 2,048 | 0x000800 | 08 00 |
| Poor | 4,096 | 0x001000 | 10 00 |
| Average | 8,192 | 0x002000 | 20 00 |
| Above Average | 16,384 | 0x004000 | 40 00 |
| Competent | 32,768 | 0x008000 | 80 00 |
| Dangerous | 131,072 | 0x020000 | 00 02 |
| Deadly | 655,360 | 0x0A0000 | 00 0a |
| E-L-I-T-E | 1,638,400 | 0x190000 | 00 19 |
The name shown next to “Rating” comes from the 9-entry table at 0x0f25 (LD A,(0xeb12) : LD HL,0x0f25 : RST 0x10), high-bit-terminated strings Harmless … E-L-I-T-E.
The totals roughly double up to Competent (2k, 4k, 8k, 16k, 32k), then jump hard (131k, 655k, 1.64M). That final stretch alone is more than the entire climb before it. The old legend of “8, 16, … 6400 kills to Elite” is just these thresholds divided by 256, which assumes an imaginary “standard kill” worth 256 points; no real ship is worth that, so count in kill-points instead.
The kill-value field was read straight from live memory after SHAPES.DAT loaded, by
walking the pointer table at 0xa857 and reading blueprint[type]+0x18 for each entry:
| Kill-points | Ship or object | Spawns in play? |
|---|---|---|
| 0 | Missile, Cargo Canister, Asteroid, Alloy | yes (objects, no value) |
| 3 | Orbit Shuttle | yes (trader) |
| 10 | Transporter | yes (trader) |
| 20 | Thargon | yes |
| 40 | Python | yes (trader) |
| 50 | Viper, Moray Star Boat | yes (Viper is police) |
| 60 | Cobra Mk1, Boa Class Cruiser, Mamba | yes |
| 80 | Cobra MkIII, Sidewinder | yes |
| 90 | Escape Capsule, Anaconda, Krait, Gecko | yes |
| 100 | Fer-de-Lance, Adder | yes |
| 110 | Asp MkII | yes |
| 150 | Thargoid, Wolf MkII | yes |
| 500 | Space Station | only in the invasion mission |
| 1000 | Cougar | mission only |
| 5000 | Constrictor | mission only (unique) |
Ships are spawned by writing the type to 0xf7e7. Traders come from 0x2bc7 (types
14 to 17) and 0x2b5b (28, 29); pirates from 0x3326/0x3331 (types 18 to 27); the
police Viper (type 0x0c) from the periodic hostile launch at 0x3176; mission ships
Cougar (30) and Constrictor (31) from the scripted table at 0x1f4f. Types 8 to 11 are
not ships at all: they are the wireframe “E-L-I-T-E” letters in the intro demo (spawned by
0x1e1a as 8,9,10,11,8), which is why they carry value 0 and garbage names.
The spread among killable targets is over 1600 to 1. Non-combat objects and the peaceful traders at the bottom barely register, which is exactly why rank progress feels erratic and cannot be farmed on soft targets.
Raw kill-points are only half the story. The blueprint also stores the combat stats that
decide how much work each kill costs. The AI and combat code read these fields off the
same IY blueprint pointer:
| offset | field | meaning | key code |
|---|---|---|---|
+0x0c | hull -> 0xf7e9 | laser hits to destroy | damaged by SUB C at 0x297b, dies on borrow |
+0x0d | max speed | chase / flee / dodge | init at 0x3068 |
+0x12 | behaviour &0x1c | 0 = passive trader, set = attacks | AI gate 0x29fa |
+0x13 | missiles &0x7f -> 0xf7ea | ranged threat | AI fires 0x29c4, self-zeroes 0x2a95 |
+0x18/19 | kill-value | kill-points + credit reward | scorer 0x32e7 |
A hit is a projected bounding-box test, not a stat lookup. Each frame the main loop at
0x1450 projects every ship (0x422e, which records the drawn half-width in 0xf800),
then runs the laser at 0x28f6. The test at 0x4740 is:
|screenX| < size AND |screenY| < size -> hittable ; size = (0xf800) = projected half-width
So hittability is angular size (model size over distance): a small ship is hard to hit at range and trivial point-blank, and orientation matters (edge-on is briefly smaller). The Sidewinder is the smallest, simplest model in the game (9 vertices, 15 edges), which is why it punches above its stats defensively.
Effort is laser-only by necessity: your Cobra carries at most 4 missiles (0xf776, capped
at 0x5ed1) and cannot reload short of docking, so hull dominates the grind. Ranking by
kill-points divided by effort, with
value = kill-points / ( hull * (1 + speed/40) ) ; speed 0 -> x1.0, 20 -> x1.5, 40 -> x2.0
| ship | hull | spd | miss | pts | value | class |
|---|---|---|---|---|---|---|
| Constrictor | 255 | 40 | 127 | 5000 | 9.8 | mission (once) |
| Cougar | 255 | 40 | 7 | 1000 | 1.96 | mission (once) |
| Fer-de-Lance | 40 | 30 | 1 | 100 | 1.43 | pirate: big and easy to hit, fast, aggressive, rarer |
| Sidewinder | 40 | 27 | 1 | 80 | 1.19 | pirate: smallest ship, hard to hit at range, common |
| Mamba | 40 | 32 | 1 | 60 | 0.83 | pirate: fastest of the cheap trio |
| Cobra MkIII | 72 | 30 | 3 | 80 | 0.63 | trader (a crime to attack) |
| Anaconda | 112 | 14 | 3 | 90 | 0.60 | trader |
| Adder | 112 | 24 | 1 | 100 | 0.56 | pirate |
| Thargoid | 255 | 20 | 1 | 150 | 0.39 | hostile: richest farmable kill |
| Krait | 180 | 30 | 0 | 90 | 0.29 | pirate |
| Wolf MkII | 255 | 40 | 3 | 150 | 0.29 | pirate |
| Asp MkII | 200 | 40 | 1 | 110 | 0.28 | pirate: worst grind (tank plus fastest) |
| Gecko | 190 | 30 | 2 | 90 | 0.27 | pirate |
| Viper | 120 | 30 | 0 | 50 | 0.24 | police: small, low bounty |
That produces three tiers:
+0x40 at
0x31e9).The catch that governs everything: the best repeatable target is the Thargoid at 150 points against 255 hull (value 0.39), and Thargoids only appear in witchspace and missions. In day-to-day play the bread and butter is a pirate around 80 points. Here is what the climb costs at each rate:
| To reach | needs (points) | Thargoids at 150 | typical pirate at 80 | Orbit Shuttles at 3 |
|---|---|---|---|---|
| Mostly Harmless | 2,048 | 14 | 26 | 683 |
| Deadly | 655,360 | 4,370 | 8,192 | 218,454 |
| E-L-I-T-E | 1,638,400 | 10,923 | 20,480 | 546,134 |
So Elite is a roughly 11,000-kill climb no matter how you optimise, and over 20,000 on typical pirates.
The promotion message is reached by fall-through from two different paths, and one of them does not touch your rank:
3313 JR c,0x331b ; tally < threshold -> NOT promoted
3315 LD HL,0xeb12 : INC (HL) : JR 0x3320 ; promoted: bump rank, then print
331b LD HL,0xe930 : INC (HL) ; not promoted: bump a counter...
331f RET nz ; ...return silently, UNLESS it wrapped 0xff -> 0x00
3320 LD HL,0x3396 : JP 0x39c8 ; print "Right On Commander!" (shared)
0xe930 is bumped once per non-promoting kill, is never reset, and is never read anywhere
except the commander-save at 0xa5c3. So every 256th non-promoting kill it rolls over,
falls into the shared print, and you get “Right On Commander!” with no INC (0xeb12). It
is most visible during the Deadly to Elite grind, where it fires dozens of times while
your rating stays put.
There is no code path that traps the rank at Deadly. 0xeb12 is only ever incremented,
never capped or reset, and the tally 0xeb13/14/15 is never clamped: its high byte can
reach 0x19 (25), the Deadly to Elite threshold. Elite is fully reachable. It only feels
stuck because that last step is 983,040 kill-points (more than the whole climb before it)
and the phantom message above keeps firing throughout.
Legal status has nothing to do with kills. It is classified live at 0x62fc from the
bounty byte 0xeb65:
Bounty 0xeb65 | Status |
|---|---|
0 | Clean |
1 – 49 (0x01–0x31) | Offender |
>= 50 (0x32) | Fugitive |
Names print from the table at 0x5aaa (Clean / Offender / Fugitive). You can be Deadly
and a Fugitive at once, or Elite and squeaky Clean. For how that byte can be washed away
in a few jumps or wiped by an escape capsule, see the exploits page.
The thresholds are identical to the original BBC Micro Elite, and the top three even share
the same high-byte constants (0x02, 0x0a, 0x19 = 2 / 10 / 25). The only difference is
the implementation: BBC used inline CP compares in its STATUS routine, while this MSX
port keeps the thresholds in the table at 0x336d and promotes incrementally. That is why
an earlier search for CP 2/10/25 instructions came up empty here: the values live in
data, not code.