01 / TRUSTED MOVEMENT
Trust the transition.
A delayed player should still move. An impossible move should not. Build a movement validator that can tell the difference, then test what it gets wrong.
Draw the trust boundary
A client is the program on the player’s machine. A server is the program that decides the shared game state. A modified client can invent a position or score. An authoritative server accepts requests, checks its own rules and computes the resulting state. A validator is the function that decides whether a requested change satisfies those rules.
seq: 1
dtMs: 100
dx: +0.6“Apply my next move.”speed: 6 m/s
position: 1 m
clock & last sequenceValidate, then update position.Our arena is a 12 m corridor. The server owns the starting position, 1 m, and the speed limit, 6 m/s. A command requests a signed displacement, dx, during dtMs milliseconds. At most 100 ms belongs in one command. For 100 ms, legal displacement is at most 6 × 100 / 1000 = 0.6 m in either direction.
A sequence number, seq, identifies a command’s order. Starting after sequence 0, this model expects 1, then 2. A duplicate must not move the player again. An early sequence 3 is rejected for retry after 2 arrives. That is a transport problem, not proof of cheating.
The threat model states what we protect and what an adversary controls. Here we protect legal movement and one application per command. The client controls seq, dtMs and dx. The test harness supplies server receipt time, at. In a real server, receipt time comes from a server clock, never from the request body.
Account for time, not just distance
Checking each command’s distance is necessary but insufficient. A client could submit ten legal-looking 100 ms moves at once. The server needs a time budget: movement cannot consume time faster than the server grants it.
Latency is delivery delay. Jitter is variation in that delay. A legal client may generate commands regularly while the network delivers several together. Charging each command only against the gap since the last receipt rejects that legal burst: the second command’s arrival gap is zero.
Instead, keep a time bank in server state. Add elapsed server time, cap the balance, then charge the command’s duration. The cap limits how much delayed movement can happen together. A small cap rejects longer legal delays; a large cap permits a larger catch-up burst. Neither setting reveals intent.
available = min(capacity, credit + at − lastAcceptedAt)
accept only if dtMs ≤ available
nextCredit = available − dtMsAll time values are milliseconds. Rejected commands leave the entire accepted state unchanged. On the next attempt, elapsed time is recomputed from that unchanged state; a rejection does not mint extra credit.
Worked example · two delayed commands
This is an author demonstration, separate from your attempts. Start with zero credit at time 0 and a 400 ms cap. Two 100 ms commands arrive at time 200. The first has 200 ms available and leaves 100 ms. The second has a zero arrival gap but spends the remaining 100 ms. Both moves fit the speed limit, so the position advances from 1 m to 2.2 m.
Assemble your validator
Choose the checks and timing policy that run in your component. The fixed scaffold always keeps numbers finite and the player inside the corridor; it cannot safely display an infinite position. The optional schema check also requires a positive integer sequence and a duration from 1 to 100 ms. Inspect the full generated code below.
Your contract allows up to 400 ms of delayed movement together: four full 100 ms commands, or five 80 ms commands. After any idle interval, total accepted duration at the same receipt time must not exceed 400 ms. Accept ordinary movement and reversals, reject impossible displacement, repeated commands and unearned time, and keep state unchanged after rejection. A 400 ms legal burst is intentionally longer than the prediction exercise’s cap.
Inspect a decision
All commands and decisions
The table scrolls horizontally on narrow screens. Focus it and use arrow keys, or swipe, to compare every column.
Your runnable validator · JavaScript
The full code below is the component that ran. Controls assemble a fixed implementation; this editor does not execute arbitrary input.
Try a test action you design
This separate scratch run uses your last submitted policy, fresh state (1 m, sequence 0, zero time/credit) and a simulated server receipt time. It does not submit a new assessed attempt.
Read the failures before repairing
A false positive here is a legal labeled action that your validator rejects. A false negative is an invalid labeled action it accepts. These are fixture labels, not diagnoses of real people. Out-of-order deliveries are counted separately. The duplicate fixture is labelled as an invalid second application and remains in the invalid-action counts. Neither label establishes malicious intent.
The suite counts actions, not players. One noisy session can dominate those counts. A staff engineer also needs per-session outcomes, realistic held-out traces, CPU cost, observation-only rollout and an appeal path before any enforcement. This small synthetic suite supplies none of that production evidence.
If a check fails, inspect its first differing action and its time bank. Then change one policy decision and submit again. Your first attempt remains in the notebook. Passing means your component matches this published contract, not that the system is secure.
Practice with new values
Practise the same rule without the fixture stepper. These new values test calculation and application, not transfer to an unseen mechanism. A smaller arena allows 4 m/s and commands up to 80 ms. The server promises to accommodate three full commands delivered together, but no more than that after any idle interval. Start with zero credit at time 0.
Enter time answers as exact milliseconds. Use a decimal point for metres; distance answers allow less than 0.000000001 m of numerical error.
From one rule to a system
This first component separates client claims from accepted state. It omits authentication, a real network, prediction and reconciliation, collision physics, clocks across reconnects, denial of service and adversarial adaptation. A browser cannot protect secrets from its own user.
Continue in the runnable Courier Lab software core for server-owned actions, input order, owned-file checks, replay, detector evaluation, retention and recovery. This earlier browser model is optional. It uses displacement-and-duration requests and consecutive sequence numbers; Courier Lab uses direction intents, strictly newer sequences that may have gaps, and expiry measured in processed ticks. Carry across the trust-boundary reasoning rather than copying this model’s protocol. Native investigation remains outside the required core. Start the runnable Courier Lab course →
Sources and optional reading
The arena, rules, code and exercises are original. Glenn Fiedler’s networking explanation supplies background on server authority and latency; Riot’s Fog of War account is a later case study in reducing information exposed to clients. Neither source specifies this time-bank policy.
Optional book context: Nick Cano, Game Hacking (2016), for threat concepts; Dang, Gazet and Bachaalany, Practical Reverse Engineering (2014), for later machine-level work; Ligh et al., The Art of Memory Forensics (2014), for evidence limits. These are reading pointers, not copied exercises or requirements. Version-specific tools need separate validation.
Your notebook
Attempts keep original wording, configuration, results, written explanations and recorded help. They stay on this browser origin. Export before moving computers or clearing storage. Text explanations are retained for review; no keyword checker claims to grade their quality.