From 41dcb9cf2266b78caa9ab1bea9934708a9e09f15 Mon Sep 17 00:00:00 2001 From: Brandon Cook Date: Fri, 3 Jul 2026 20:24:25 +1000 Subject: [PATCH] fix: persistent guest room so casino Credits resolve against the live roster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The guest runtime created a fresh `room` every callback but built the game's Handler + stored Services only ONCE at OnStart, binding svc.Credits to the start callback's room. creditsSvc resolves a player's host index against that room's ctx.members — empty for a solo game (the player joins after start) — so every Balance/Wager/Settle was denied at the guest facade (ErrCreditsDenied) and never reached the host: casino games showed a 0 balance and refused bets. Use a single persistent `theRoom`, refreshed in place each callback, so the game's once-built Services always resolve against the current ctx. No behavior change for non-casino games (the room reuse is transparent; ctx is the same data, just not reallocated). The kittest/native/memsvc doubles resolve credits by account id rather than roster index, which is why unit tests and `shellcade-kit check` conformance missed this — it only manifests on the real wasm ABI path. Follow-up: add a casino wasm-fixture host test that Start()s empty then Join()s and asserts a guest credits call reaches the host service. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01W7oJQSTva5xgbAZomdV5Jj --- .changeset/fix-guest-room-roster.md | 5 +++++ internal/game/run.go | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-guest-room-roster.md diff --git a/.changeset/fix-guest-room-roster.md b/.changeset/fix-guest-room-roster.md new file mode 100644 index 0000000..53297f2 --- /dev/null +++ b/.changeset/fix-guest-room-roster.md @@ -0,0 +1,5 @@ +--- +"kit": patch +--- + +Fix casino games showing a 0 balance and refusing wagers. The guest runtime built a fresh `room` every callback but created the game's `Handler` (and its stored `Services`) only once at `OnStart`, so `svc.Credits` was permanently bound to the *start* callback's roster. `creditsSvc` resolves a player's host-side index against that roster's `ctx.members`, which is empty for a solo game (the player joins after start) — so every `Balance`/`Wager`/`Settle` was denied at the guest facade (`ErrCreditsDenied`) and never reached the host. The room is now a single persistent instance refreshed in place each callback, so the game's stored `Services` always resolve against the live roster. (The `kittest`/native/memsvc doubles resolve by account id, not roster index, which is why unit tests and conformance did not catch this — it only manifested on the real wasm ABI path.) Casino games must be rebuilt against this release to pick up the fix. diff --git a/internal/game/run.go b/internal/game/run.go index 574436c..348f490 100644 --- a/internal/game/run.go +++ b/internal/game/run.go @@ -18,6 +18,16 @@ var ( theGame Game handler Handler rng *rand.Rand + // theRoom is the single, persistent Room, refreshed in place each callback + // (its ctx is reassigned in decodeCall). The game's Services are built ONCE + // from it at NewRoom and stored by the game, so any roster-dependent surface + // they expose must see the LIVE roster: Credits resolves a player's host + // index against ctx.members, so a fresh room per callback would bind the + // game's stored svc.Credits to the START roster forever — denying credits to + // everyone who joins after start (a solo player joins after OnStart, so its + // start roster is empty and every Balance/Wager is denied). One reused room + // keeps the stored Services pointed at the current ctx. + theRoom = &room{} ) // Run installs the game. The instance's Handler is created lazily on start. @@ -68,7 +78,12 @@ func decodeCall() (*room, *wire.Rd) { if rosterChanged { invalidateBaselines() } - rm := &room{ctx: ctx, rng: rng} + // Refresh the single persistent room in place (NOT a fresh room each + // callback) so the game's once-built Services keep resolving against the + // live ctx — see theRoom. + theRoom.ctx = ctx + theRoom.rng = rng + rm := theRoom if epochMismatch && !epochMismatchLogged { // Host fault: an unchanged-form ctx carried an epoch we don't hold. // Degraded (cached roster kept, baselines invalidated) — warn once.