EXPcaliburINSIDE THE MACHINE

13 / THE PLATFORM

How a match becomes a ranked result

A match can earn EXP, but a tournament still needs paperwork. The service must link each upload to its owner and recover from failed workers. It must also pick the right result and save its replay. This chapter follows one upload from the player to the leaderboard.

Starts with: A player's uploaded program and a grading job.

Ends with: One saved result, plus its replay when replay storage succeeds.

Question: How does a repeatable match become a trustworthy leaderboard record?

First, keep the records straight

The server does not store an upload as one giant record of “the bot and its score”. Instead, it saves several small records, each with its own job. The diagram follows one upload from file to leaderboard. Each box is named after a database table in the source.

That gives you two separate questions. Did the match finish, and did the service save it? For that, look at the submission and the run. Is this now the player's best result? For that, look at the leaderboard row, because a saved result does not always become a personal best.

One engine, several entry points

Practice and ranked grading both call the same Rust game engine. The browser can animate the match, but it does not calculate EXP. So a replay in the browser cannot change the server's score.

The released runner accepts only compiled images. Only author builds can compile source or assemble instructions inside the runner. The player handout ships a separate compiler instead. A .bot file in the repository does not mean that the released runner accepts source.

The runner prepares the uploaded program once for the whole seven-run bundle, so its hash and engine mode stay fixed. Before any of those runs starts, the runner also checks the ABYSS protocol IDs. An upload with an unknown protocol ID is rejected, not quietly treated as CLASSIC.

For a ranked grade, the runner starts seven fresh matches with that same image. Each match gets a different hidden seed. The seed sets up its random choices. The runner sorts the seven scores and picks the middle-scoring match. It keeps that match's complete result and replay. It does not combine the seven replays.

Why a correct simulation still needs a service protocol

Even a correct EXP total can be saved under the wrong attempt, and two workers might try to save the same result. For example, a worker can stop mid-grade and return after another worker takes over. The game rules stay the same, but the service must control who may save.

The two sides solve different problems. The service tracks each step of the job: upload, queue, worker claim, matches, save, and leaderboard update. The game engine tracks the knight and the arena. Because of this split, if a worker fails, the service can give the same bot bytes to another worker. The combat rules do not change.

The submission lifecycle

From upload to saved result
Player uploadCheck the account and file size; hash the uploaded bytes.
Saved jobPut the grading request in the queue.
Worker claimOne worker takes the job and keeps checking in.
Run matchesUse the shared engine on seven seeds within limits.
Save safelyAccept a result only from the worker that still owns the job.
Best score and replayUpdate the player's record only for a higher EXP score.

The API checks player identity through the configured platform verification path. An account ID and a display name are not interchangeable. The API also gives distinct responses for bans, rate limits, invalid input, and identity conflicts, so a team gets a useful answer. Even so, a team cannot take over another account just by choosing that account's display name.

The player's credential can change at login. The service keeps the link to the platform identity. It uses that link to check account ownership for submissions, job status, and replay access. Administrator operations live in a separate service, not in hidden player API routes.

Leases make worker failure recoverable

Picture worker A grading a bot. Then A goes quiet, and worker B takes over the job. Later, A returns with its old result. Which worker may save? The service must accept a result only from the current owner:

The temporary reservation is called a lease. A worker keeps it alive by sending a heartbeat, a regular “still here” message. Each heartbeat pushes the lease's end time forward. If A stops sending heartbeats, its lease expires and B can claim the saved job. Coming back later does not make A's old lease valid again.

The final save checks the worker's identity again. Seeing running is not enough. The submission can still say running after B takes over. Here is the check A must pass before the service inserts any run row:

RUST crates/coordinator/src/main.rs · source lines 962–974
    let owned = tx
        .execute(
            "UPDATE submissions s SET updated_at=$2
              WHERE s.id=$1 AND s.status='running'
                AND EXISTS (SELECT 1 FROM jobs j
                             WHERE j.submission_id=s.id AND j.worker_id=$3)",
            &[&job.submission_id, &now, &worker_id],
        )
        .map_err(|e| e.to_string())?;
    if owned == 0 {
        tx.rollback().map_err(|e| e.to_string())?;
        return Ok(false);
    }

Read the key condition as “This submission is still running, and I still own its job.” After B takes over, A changes zero rows. The service then rolls back A's transaction and saves nothing from it. B can still finish the grade. The same check also stops A from saving an error result.

The current setup gives each worker node one grading job. Inside that job, the runner can run the seven seeds in separate threads. Those are two different kinds of parallel work, so if operators increase both, memory use can rise sharply.

Resource bounds exist at multiple layers

Limits exist at several layers. The outer VM limits instructions per tick. The Gate limits inner work, table sizes, and how long paused work can live. The game limits enemy and gem pools and total match time. The coordinator limits process memory and real running time.

The process wrapper limits memory with prlimit-style controls. It also stops a run that takes too long. Inside it, the Rust VM keeps bot memory separate from host memory and checks what bot instructions may do. But calling something a VM does not, by itself, add another operating-system sandbox.

The coordinator currently permits a 2,048 MiB process address space and 540 seconds of real time. Deployment notes report about 0.9 GiB of memory actually in use for related grading work. The limit, the memory in use, and the container's allocation are three different values.

Two comparators, two purposes

The service sorts results in two places. Each sort happens at a different moment and answers a different question.

Inside one submission: Sort the seven matches by EXP, with the highest first. If two scores are equal, the shorter game time comes first. If those times are also equal, the smaller seed comes first. Match number four in that order is the median, with three matches ranked above it and three below. The source uses exactly this order:

RUST crates/runner/src/lib.rs · source lines 171–181
fn median_index(per_seed: &[(u32, RunResult)]) -> usize {
    let mut order: Vec<usize> = (0..per_seed.len()).collect();
    order.sort_by(|&a, &b| {
        let (ra, rb) = (&per_seed[a].1, &per_seed[b].1);
        rb.exp
            .cmp(&ra.exp)
            .then(ra.time_ms.cmp(&rb.time_ms))
            .then(per_seed[a].0.cmp(&per_seed[b].0))
    });
    order[order.len() / 2]
}

The saved ABYSS Apex artifact has seven recorded EXP scores: 321,637, 414,290, 298,344, 478,518, 199,323, 336,338, and 367,461. Sort them, and the fourth score is 336,338. It is a recorded example, not a promise for other bots or seeds.

The middle score belongs to one complete runThe middle score belongs to one complete runSAVED ABYSS APEX · SEVEN RESULTS SORTED BY EXP1478,518 EXP2414,290 EXP3367,461 EXP4336,338 EXP5321,637 EXP6298,344 EXP7199,323 EXPThe fourth run supplies EXP, time, kills, and replay together.
Sort seven runs and keep the complete middle run. The bars show saved ABYSS apex EXP scores. The fourth result is the median. Its time, kills, and replay stay with that result. View diagram at full size

The grader keeps the selected match's level, kills, game time, victory result, and replay together. That replay was recorded during the actual match. It was not rebuilt by averaging frames or running the bot again. The runner records all seven matches, because it cannot know the median until every match ends. Then it keeps the selected replay and discards the other six.

Across players: The public board compares each player's saved best result. More EXP ranks higher. If EXP is equal, the earlier submission ranks higher. The board also shows game time and kills, but neither one affects rank. These are the two sort keys in the next code block:

RUST crates/api/src/rank.rs · source lines 28–37
pub fn rank_cmp(a: &RankKey, b: &RankKey) -> Ordering {
    b.exp
        .cmp(&a.exp)
        .then_with(|| a.created_at.cmp(&b.created_at))
}

A player's saved best changes only when a new grade earns more EXP. An equal score does not replace the earlier result. Try the recorded 336,338-EXP result against three possible earlier bests:

Does this result replace the old best?

Teaching examples: the new graded run is the repository’s 336,338-EXP ABYSS Apex median. Choose an imaginary previous best. Your choice changes which run the leaderboard shows. It does not change the new run’s score.

NEW GRADED RUN336,338 EXPThe run is saved either way.
PREVIOUS BEST300,000 EXPWhat the board pointed to before this grade.
BOARD AFTER THE CHECKNew run takes the slot336,338 is strictly greater than 300,000.

The leaderboard keeps one best run per player. An equal score does not replace the earlier one.

So finishing faster does not help when EXP is tied. A longer match with more EXP still ranks higher, and at equal EXP, the earlier result keeps its place.

Replays record outcomes rather than re-running the bot

The replay stores copies of world state and the changes between them. The viewer simply reads those records instead of running the bot again, which is why it does not need the hidden seed. Recording only observes the game; it does not change combat or random draws.

Think of a replay block as one full picture followed by a list of changes. The full picture is a keyframe, and the smaller change records are delta frames. A new keyframe starts every 300 ticks (five game seconds), and up to 299 delta frames follow it.

Version 4 starts with a 108-byte header, while the older version-3 reader accepts a 96-byte header. Replay positions are rounded to the nearest half-pixel.

What a replay stores
HeaderVersion, size, timing, and record details.
KeyframeA full picture of the game at the start of a block.
ChangesOnly what changed in later ticks of that block.
IndexWhere each block begins, so seeking is quick.
ViewerDraw the saved arena and smooth motion between records.

To jump to a tick, the viewer finds the nearest earlier keyframe. It then applies only the changes since then, so it does not need to decode the match from the start. The half-pixel rounding saves replay space. It does not change the positions the game used during combat.

The engine codec produces raw replay blocks. Server storage then compresses replay data with Brotli. When a client asks for a replay, the API checks whether the client accepts br. It then either sends the compressed bytes or decodes them to the raw replay format. Uploaded bot images are not treated as compressed replay data.

The service does not keep every old replay forever. After a grade, it keeps the replay of the player's current best and the replay of the new grade. A later grade can remove an older replay that is neither. The old run record and its score remain, though. If replay storage fails after the grade is saved, the service clears the broken replay link. It does not take away the earned EXP.

Work through a seek to tick 312

A replay is a sequence of recorded states, not a second matchA replay is a sequence of recorded states, not a second matchSimulationTick 300: full worldstateTick 301: changed fieldsTick 302: changed fields…Tick 600: next full stateReplay blocksKeyframe + deltasAn index locates eachblock. Half-pixel stepssave storage.Viewer seeks to 312Read keyframe 300.Apply deltas 301–312.Render reconstructedstate.recorddecodeThe viewer reads saved states. It does not need the hidden seed or bot program.Recording reads the world after each tick. It does not change the match.
A replay block stores a full state and later changes. The index locates the block. The viewer reads its keyframe. It then applies the delta frames needed to reach the requested tick. View diagram at full size

With a 300-tick interval, tick 312 falls in the block that starts at tick 300. The viewer finds that keyframe and applies the next twelve tick changes, without running the bot from tick zero or using the hidden seed. Seeking into a later block works the same way, with another index entry.

Replay positions use half-pixel steps, while the game itself uses more exact integer-based positions. The replay shows the visible path, but the browser does not use it to calculate hits. The browser can also smooth motion between saved frames without changing the score.

Replay visibility is configurable

By default, only the owner can view a replay. The gamemaster can let other authenticated teams view replays linked to the leaderboard. Even then, this setting does not make every stored replay public.

If a requester cannot access a replay, the API answers as if the replay did not exist. That way, the requester cannot use sequential replay IDs to learn which private replays exist. And if the service cannot read the replay-access setting, it permits owner access only.

The service removes hidden seed values from the replays that it publishes or serves. Viewers can still watch the match, because the replay records the resulting state. Nothing has to be simulated again from the seed.

The handout is an allowlisted product

The full repository holds much more than the player handout. It has author tools, older designs, reference bots, timing studies, release scripts, and tests. Players received a smaller set of files for learning and reversing the challenge.

The Stage 7 report lists 19 exact archive entries. They include a stripped compiler and grader, sample source and binary, public practice seeds, and world notes. The handout leaves out the source tree, assembler, hidden scores, author fixtures, solver models, and test traces.

The handout build is reproducible. Release checks can therefore rebuild it and compare the archive and executable byte for byte. Other checks scan the feature graph and the executable for leaked internal tooling. The tests also run the packaged binary itself. A pass on a development build alone cannot show that stripping, feature selection, and packaging preserved the release.

Front end and operations complete the system

The PixiJS client renders the arena, sprites, enemies, world assets, and replays. A separate React dashboard supports gamemaster operations. Neither client calculates EXP; that stays with the server. They display results and let operators configure or observe the service.

Three records must agree: the engine result, the database result for the correct player, and the replay shown to the viewer. Together, they turn a simulated match into a competition record.