jonnify·codemojex /codemojex/scoring-and-settlement/settlement-strategies

Codemojex · C4.3 · a dive of Scoring and Settlement

Settlement strategies.

A game closes from more than one trigger, and the triggers can race — a perfect crack of 600, an expired timer under the sweep, a settle-queue job. Every close funnels through one atomic SET cm:<game>:closed NX, and only the caller that wins it pays. The winner runs one of three strategies the game snapshotted at start.

Grounding — echo/…/codemojex/rooms.ex (close_game/1, do_close/2, the three close_*, close_if_expired/1, void_if_stale/1, close_void/2) · economy.ex (winner_take_all, top_k_split) · game.ex (Settle) · sweep.ex · the settlement · golden-blind · golden-tournament stories.

The winner of the lock runs one strategy — select it
SET NX won do_close(game, r) close_live settlement = "live" winner-take-all Economy.winner_take_all/2 close_sealed settlement = "sealed" reveal → top-K Economy.top_k_split/3 close_split settlement = "live_split" live top-K + clips Economy.top_k_split/3
live → the classic close. close_live/2 reads the top of the board and pays the diamond pool winner-take-all to the max scorer, split evenly on a tie (Economy.winner_take_all/2), bumps cm:total_won, and returns the room to waiting.
§1

The close race, resolved exactly once

rooms.ex · close_game/1

The perfect-crack close and the timer close can both fire, and a settle-queue job is a third path. All three take the same one-shot lock. Only the caller whose SET … NX returns "OK" proceeds to do_close; the losers — and a second close after :settled — are no-ops. No distributed transaction: one Valkey SET NX plus a pure, re-runnable payout.

echo/…/codemojex/rooms.ex · close_game/1 source
def close_game(game) do
  case Store.game(game) do
    nil -> {:error, :no_game}
    %{status: :settled} -> {:ok, :already_closed}          # the pay-once guard
    r ->
      case Cmd.set("cm:" <> game <> ":closed") |> Cmd.value("1") |> Cmd.nx() |> Wire.run(Bus.conn()) do
        {:ok, "OK"} -> do_close(game, r)                      # the winner settles
        _ -> {:ok, :already_closed}                          # the loser is a no-op
      end
  end
end
Which trigger wins the lock — select one
SET NX :closed 600 crack ScoreWorker · classic wins → do_close settles timer (sweep) close_if_expired/1 wins → do_close settles settle job Settle.handle · cm-settle wins → do_close settles 2nd close after :settled no-op · :settled guard
600 crack → a perfect score inside ScoreWorker calls Rooms.close_game/1 directly. If it wins the SET NX it settles; a timer sweep or a settle job that fires at the same moment loses the SET and is a no-op. The settlement story pins it: a second close pays nothing.

The lock is the whole exactly-once story. The payout functions are pure and re-runnable, so even a re-delivered settle job re-computes the same numbers — but the lock means only one of them ever books the payout. Both the settlement and golden-blind stories assert it: the sealed settlement is exactly-once — a second close pays nothing.

§2

Three strategies, one switch

rooms.ex · do_close/2

The winner of the lock runs the strategy the game snapshotted at start — a pure function of the game’s type and its golden marker (policies_for/2), stored as the settlement column and read back at close.

echo/…/codemojex/rooms.ex · do_close/2 source
defp do_close(game, r) do
  case Map.get(r, :settlement, "live") do
    "sealed"     -> close_sealed(game, r)   # golden (blind): reveal, pay top-K
    "live_split" -> close_split(game, r)    # golden room: live top-K + clips
    _            -> close_live(game, r)     # classic: winner-take-all
  end
end

live — the classic close. close_live/2 pays the diamond pool winner-take-all to the max scorer, split evenly on a tie, and returns the room to waiting.

sealed — the golden (blind) close. close_sealed/2 sets the game :revealing, exposes the secret and nonce, ranks every guess by its best linear points, pays the top top_k (default 5) a payout_split share of the pool, and emits one fat revealed event — the first and only results a blind client receives, carrying the now-exposed secret, nonce, commitment, board, and payouts, so a player recomputes the commitment and checks it.

live_split — the Golden Room tournament close. close_split/2 reuses the same top-K math over a buy-in-funded pool but pays live (no reveal phase), and grants every member outside the top-K a consolation clip of max_score / 10. It mirrors the classic close’s shape, not the blind reveal.

echo/…/codemojex/codemojex.design.md · the game as a state machine design
          ┌─────────┐   guess → score (the cm lane)
          │   open   │◀─────────────────┐
          └────┬─────┘                   │ board + events (classic)
   600 crack   │  or timer ends_ms       │
   or a sweep  ▼                          │
        SET cm:<game>:closed NX ──────────┘
                │  (only the winner of the SET proceeds)
                ▼
    classic ─► settled          golden ─► revealing ─► settling ─► settled

A classic game traverses open → settled; a blind golden-type game traverses open → revealing → settling → settled; a Golden Room traverses gathering → open → settled. All of them leave open through the same lock.

§3

The pool drains; the void keeps the fees

economy.ex · sweep.ex

Economy.top_k_split/3 weights rank i by split[i] / Σ split (default weights [40, 25, 15, 12, 8]). Integer division strands a few diamonds; the remainder — the dust — is added to rank 1, so the whole pool is distributed and none is stranded. The pass is pure, so the guarded re-run pays identically.

echo/…/codemojex/economy.ex · top_k_split/3 (the dust) source
payouts = Enum.map(ranked, fn {{p, _s}, w} -> {p, div(pool * w, sum)} end)
# the floor strands up to (paid_ranks - 1) diamonds; give the dust to rank 1
paid = payouts |> Enum.map(&elem(&1, 1)) |> Enum.sum()
add_dust(payouts, pool - paid)

defp add_dust([{p, d} | rest], dust), do: [{p, d + dust} | rest]

No player action closes a timed-out game — the periodic Codemojex.Sweep does. Every tick it walks Store.due_games/0 and, under the same close locks, closes an :open game past its ends_ms (close_if_expired/1) and voids a :gathering Golden Room past its room_deadline (void_if_stale/1).

A never-fills Golden Room voids with no refund. close_void/2 takes the same SET cm:<game>:closed NX, transitions :gathering → :voided, and books only the seed-cancelling deposit_reclaim — no player money moves, the platform keeps the collected fees. The golden-tournament story pins both the void (voided, the room reset, no refund TXN exists) and the sweep’s idempotence (a second tick changed nothing).

Where the diamonds and clips actually move — the wallet, the pool, and the forward rake — is C5. This dive is the close: the lock, the strategy, and the split.

§4

References

sources · related

Sources

  • Commitment schemesthe hiding and binding the golden SHA-256(secret ‖ nonce) commitment gives before the sealed reveal
  • Repo.transactionthe all-or-nothing transaction the payout and the void reclaim run inside

Related