Codemojex · C3.1 · a dive of Guesses on Fair Lanes
The guess and the lock.
A guess is six emoji codes. Before it can become work, Codemojex.Guesses.submit/3 runs a short admission gate and overlays the player’s locked positions — both server-side, against the system of record. The client sends six codes and a lock intent; the server decides what is real.
Grounding — echo/…/codemojex/game.ex (Codemojex.Guesses.submit/3, valid_guess?/2) · locks.ex (merge/3, the per-player lock hash). The play path from the scoring story feature’s submit setup.
cond of guards, in orderStore.game(game) reads the game from the system of record. A missing game is {:error, :no_game} — the status is read from Postgres, not the cache, because it is mutable and must be authoritative.The admission gate
game.exsubmit/3’s head clause is when length(emojis) == 6, so a wrong-length input falls to the catch-all clause and is a plain :bad_guess — the arity of a guess is enforced by pattern match, not a runtime check. A live, unexpired game then validates the six codes against its own snapshotted keyboard: cell_codes, the randomized subset the game pinned at open (C2). A code from outside that subset is rejected before any money moves.
def submit(game, player, emojis) when length(emojis) == 6 do r = Store.game(game) # the system of record, not the cache now = System.system_time(:millisecond) cond do r == nil -> {:error, :no_game} Map.get(r, :status, :open) != :open -> {:error, :closed} expired?(r, now) -> {:error, :expired} not valid_guess?(r, emojis) -> {:error, :bad_guess} true -> guess = Locks.merge(game, player, emojis) # locks overlaid → the scored guess # … charge the wallet, then enqueue (C3.2) end end def submit(_game, _player, _emojis), do: {:error, :bad_guess}
The guards run in order and short-circuit: the first that matches returns its error, and only true reaches the write. The surface turns each {:error, reason} into a rejection the player sees immediately, and turns the accepted branch into an on-its-way response — the score returns later, over the live channel.
The lock — durable, server-side
locks.exA player sure of a position locks it. The lock is one field in a per-player Valkey hash for the round — key cm:{round}:lock:{player}, field the position 0..5, value the code — so it persists across guesses with no client state to lose. At submit time, Locks.merge/3 overlays the locked positions onto the six submitted codes; a locked position wins regardless of what the keyboard sent.
# key: cm:{round}:lock:{player} field: position 0..5 value: code def merge(round, player, guess) do locks = locked(round, player) # %{pos => code} from the hash guess |> Enum.with_index() |> Enum.map(fn {code, i} -> Map.get(locks, i, code) end) # lock wins at its position end
So a confirmed cell is guaranteed on every subsequent guess: the player commits it once, and the server enforces it on the merged guess that becomes the job. The guess that gets scored is always the merged one, never the raw client input.
07 31 18 22 44 09 — each drawn from the game’s snapshotted keyboard. This is the raw input, before any lock is applied.References
sources · relatedSources
- Mastermindthe guess-against-a-hidden-code loop the game belongs to
- Helland — Life Beyond Distributed Transactionsthe entity behind its boundary; the server decides what is real
Related
- C2 — Rooms, Modes, and the Secretthe snapshotted keyboard the guess is validated against
- C1 — The Game as Branded Systemsthe
GESbrand and the privacy boundary - EchoMQ — the protocolthe bus the accepted guess is enqueued onto