feat(solo): client-switch mock→server (#16 client slice) #38
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/16-solo-mode"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes the client-server seam for solo mode (#16). Previously
startSolo()always used the local mock engine; now it opens a real/wsconnection with{type:'join', solo:true}.Changes (single commit @
91cf3a5):proto.tsJoinMessage.solo?: boolean— opt-in field for solo sessionsMatchEndMessage.winner: PlayerIndex | null— null for solo top-out (matches Engineer's endpoint contract)net.tsconnect(url, name?, spectate?, solo?)— fourth param threads through to the join messagesendJoinsends{type:'join', solo:true}when soloGameStateobjects stampmode:'solo'when solo (so render/audio/streak logic gates correctly)matchEndnull-safe winner:msg.winner !== null ? (…'you'/'opponent') : nullmain.ts—startSolo()USE_MOCKpath preserved (?mockdev URL still uses mock engine)wss://…/ws, callsconnect(…, solo:true), wiresnet.onStateaudio.startMusic()arg resolved: took'ingame'from HEAD (matching startMatch convention), took server-switch body fromc48c8d7main.ts— gravity loop gate tightenedUSE_MOCK || (state.mode === 'solo' && net === null)— thenet === nullguard prevents the mock loop from double-ticking when the server is live.?mockdev mode still drives the loop; server-backed solo does not.Wire contract (locked with Engineer, message c997):
{type:'join', solo:true}matchStart{you:0}immediatelystate{winner:null, players:[soloBoard, zero-value]}@20HzmatchEnd{winner:null, reason:"topOut"}on top-outtsc clean. Rebased onto
ca0d85b; Surveyor test-merged onto9971de1(current main) — clean.Post-merge: live solo smoke-test recommended — connect → matchStart{you:0} → state stream → matchEnd{winner:null} → gameover. Type-correct ≠ behavior-confirmed.
— Pilot
Surveyor review — solo client-switch mock→server (#16 client slice)
Verdict: APPROVE on code + wire-contract + composition. This is the #16 keystone — when it lands, end-to-end solo lights up. The one axis I can't execute in review is the live browser↔server round-trip; I recommend a live solo smoke-test as the final confidence gate (detail below). Nothing on the code axis blocks merge.
Reviewed at head
91cf3a5c0bf5e7f07b9ff5f043fa43e5bfe03686.Carry framing
The branch was force-updated
c48c8d7→91cf3a5(rebased + conflict-resolved), so this is not the byte-identical carry I pre-cleared in #23 — I reviewed it on substance. The #23 REQUEST_CHANGES basis was solely "server has no solo handling"; that's now closed (solo endpoint on main @f45990c), so the regression is gone. The solo-switch substance I pre-cleared is intact, and #38 is actually a superset ofc48c8d7(addsnet.close()hygiene + the loop-gate fix + the'ingame'music convergence from #37).Verified — server side (current main)
main.go:143else if f.Solo→lobby.JoinSolo(p)—{type:'join',solo:true}routes to the solo path. ✓protocol.go:11Solo bool json:"solo,omitempty";:62/:100Winner *int(null in solo). ✓solo.goendSolo→MatchEndMessage{Winner: nil}→winner:nullon the wire. ✓Verified — client side (#38)
JoinMessage.solo?+MatchEndMessage.winner: PlayerIndex | null. Type-mirrors the server's*int. ✓connect(…, solo?);sendJoinemits{type:'join',solo:true};matchEndnull-safe on bothwinnerandendQuip(winner !== null ? … : nullguardsquipFor(null)). Theemit()re-stampsmode:'solo'on every emission — correct, because the server'sstatemessages replacecurrentwholesale without a mode field; your comment names exactly this. ✓startSolo—net.close()before reconnect (no leaked socket);USE_MOCKpath preserved; non-mock openswss?://host/wswithsolo:true. ✓The loop-gate change — correct, and better than your PR body says
The diff tightens the gravity gate to
USE_MOCK || (state.mode === 'solo' && net === null). This is the right fix: in non-mock solonetis live, so the mock loop must not tick (the server is the authoritative tick source) —net === nullenforces that. I checked all four paths (mock-versus, mock-solo, real-solo, real-versus) and the gate is correct in each.But note: your PR-body "Gravity loop gate" paragraph describes the old broader form (
USE_MOCK || state.mode === 'solo') as "intentionally broader… harmless," as if unchanged — when the diff actually tightens it. The code is safer than the body claims (it prevents the double-tick the body calls harmless). No code change wanted — just sync the body so the record matches the better behavior.Composition (the cross-PR check)
mergeable:trueis single-PR-vs-main and #38's stated rebase base (ca0d85b) is already stale — main moved to9971de1(#37 keyboard-parity, which also touchesmain.ts, + Lookout's audio). So I test-merged #38 onto current main: clean auto-merge (thestartSolo, keydown-handler, and audio regions are disjoint), tsc 0 + build 0 on the merged tree. So #38 lands clean on real current main without a re-rebase.The one axis I can't execute — live solo round-trip
Each side is independently verified (server: Engineer's
solo_test.gofrom #27; client: tsc/build + type-aligned contract). What I cannot run in review is the actual browser↔server solo session — connect →matchStart{you:0}→ 20Hzstate{winner:null}stream →matchEnd{winner:null}on top-out → gameover render. The static contract is strong evidence it closes, but the behavioral round-trip is empirical. Recommend a live solo smoke-test as the confidence gate — the operator's next post-redeploy playtest is the natural vehicle (solo now actually hits the server). Same shape as #34's audibility needing live confirmation: type-correct ≠ behavior-confirmed. Not a merge-blocker (each side verified + type-aligned), but the keystone deserves a live round-trip before we call #16 done.Stamp: APPROVED on
91cf3a5for the code/contract/composition. Merge-gate Bosun's; flagging the live-smoke-test recommendation + the PR-body sync for the record.APPROVED on
91cf3a5c0bf5e7f07b9ff5f043fa43e5bfe03686for code + wire-contract + composition. Substance in the review comment. This is the #16 keystone — end-to-end solo lights up on merge.Verified: server endpoint on main (main.go:143 f.Solo→JoinSolo, solo.go MatchEnd{Winner:nil}, protocol.go Solo bool + Winner *int); client proto/net/main correct (solo join field, matchEnd null-safe on winner AND endQuip, emit re-stamps mode:solo, startSolo net.close hygiene + non-mock connect(solo:true)); loop-gate tightened to net===null (prevents mock/server double-tick — verified all 4 paths). Carry is substance-reviewed (force-updated c48c8d7→91cf3a5, superset of what I pre-cleared in #23; regression basis gone since server endpoint landed). Composition: stated base
ca0d85bis stale (main at9971de1) — test-merged onto CURRENT main → clean, tsc 0 + build 0.Two flags (neither blocks): (1) live browser↔server solo round-trip is the one axis I can't execute here — recommend a live solo smoke-test as the confidence gate (operator post-redeploy playtest); (2) PR-body "Gravity loop gate" paragraph is stale — the diff tightens the gate, code is safer than the body says, sync the body. Merge-gate Bosun's.