MSW Apple Game
Game Development

MSW Apple Game

A deterministic MapleStory Worlds puzzle game — the server never sends the client the RNG seed, yet still re-scores every run by replaying the move list against a board it regenerates from that seed itself

GitHub
MapleStory Worlds mlua Lua 5.4 xorshift32 DataStorage ELO Git

Problem

A 10×17 grid of digits; drag a rectangle, and if the enclosed numbers sum to exactly 10 they clear. Best score in 120 seconds. The board is fully observable, which means the game has no secrets to protect during a run — but it does have one before the run starts, and one after it ends.

The obvious way to build a seeded puzzle is to send the client the seed and let it generate the board. That hands anyone reading the network traffic the entire board before the timer starts, which on a ranked daily leaderboard is the whole game. The obvious way to score is to trust the number the client reports, which is worse.

Approach

  • The seed stays on the server, and determinism is what makes that free. The server generates the board, flattens it to a 170-character digit string, and streams that. The seed lives only in the in-memory session record. On submit, the server regenerates the identical board from the retained seed and replays the submitted moves — so replay remains the single source of truth for the score without the client ever having held the seed.
  • Encode across the RPC boundary, don’t serialize. The flat string is deliberate: it sidesteps the ambiguity in nested-table RPC serialization around Lua’s 0-based grid keys.
  • Rate-limit on what the client cannot forge. Two gates exist and the code is explicit that they are not equivalent.
  • State the residual honestly in the source. The anti-cheat comments document the ceiling rather than overselling the floor.

Implementation

Withholding the seed

SeedService builds EncodeBoard(GenerateBoard(seed)) server-side. The client-bound reply carries the board, a single-use token, the date key, and the player’s personal best — and no seed parameter. There are zero code references to the seed anywhere in the client tree.

Two rate gates, ranked by forgeability

The burst guard reads client-supplied move timestamps and is labeled in-source as secondary — defeatable by a bot that fabricates spacing, kept only to catch naive ones cheaply. The authoritative gate measures server wall-clock from token issuance to submit. That predicate was extracted as a pure function precisely because a live submit can’t manufacture a sub-second elapsed (DataStorage round-trips alone add ~1s), so its arithmetic is unit-tested directly instead of being flaky. The token is burned before the replay runs, not after.

The comment then states what’s left: a bot that deliberately burns real wall-clock still gets through — but then holds no speed advantage over a human, and for a fully-observable board that residual is unavoidable.

Storage semantics found by probing, not by reading docs

A runtime probe found that MSW’s GetAndWait returns OK with an empty string for a missing key rather than a not-found error, and that UpdateAndWait — the compare-and-set primitive — cannot create a missing key. The daily ranked-attempt lock is built against the observed behavior: it branches on the value rather than the error code, uses a plain set for first write, and reserves CAS for the unexpected-value case. Since no create-CAS primitive exists, the double-grant window is closed with an in-memory per-user-per-day in-flight guard, justified in-source by the check-and-set being synchronous with no yield between the two halves. Genuine read failures fail closed.

Golden vectors as regression locks

Two stored expected values, compared at run time rather than regenerated by the code under test: one pins a single xorshift32 step, the other pins a 32-bit rolling fold over all 170 cells for a fixed seed. The second breaks on any change to the shift constants, the all-zero normalize fallback, the range mapping, or the row-major traversal order. The ELO suite follows the same discipline — its expectations are hardcoded so that a formula change is caught rather than absorbed.

Outcome

  • 5,092 LOC of .mlua across 27 files (4,244 excluding the four test harnesses), plus a vendored 1,877-LOC MIT ranking package.
  • 114 in-engine assertions across four harnesses, and 2 golden-vector regression locks.
  • 78 commits over four days, 7,529 lines added against 548 deleted.
  • Three modes ship: unlimited practice, a once-per-day ranked run seeded from the KST date, and 1v1 via random queue or a 4-digit room code, settled by pairwise ELO.

What is not proven here

The assertions run only inside the MapleStory Worlds Maker editor — there is no CI and no stored test output, so the 114 figure counts call sites, not a green run. The repository’s own planning docs mark the entire multiplayer path (real two-player racing, cross-client RPC isolation, instance rooms, leaver forfeit) as publish-only and not verifiable in the editor. The ELO settlement is exercised with synthetic user IDs injected into the match registry, not by two real clients racing.

Technologies

MapleStory Worlds · mlua (typed Lua dialect, @Logic / @ExecSpace / @Sync) · Lua 5.4 integer and bitwise semantics · xorshift32 · MSW UserDataStorage & SortableDataStorage · ELO · MSW UI and map assets