Skip to content

V2 - #4

Draft
acedward wants to merge 91 commits into
mainfrom
v2
Draft

V2#4
acedward wants to merge 91 commits into
mainfrom
v2

Conversation

@acedward

Copy link
Copy Markdown
Contributor

No description provided.

acedward and others added 30 commits April 13, 2026 16:49
Collapse the 5-command lobby grammar to 3 (createdLobby, joinedLobby,
closedLobby) so users have a single action to create or join. The second
join auto-starts the game in the same state-machine transition, and open
lobbies drop off /open_lobbies 10 minutes after creation.

- Grammar: drop toggledReady, startedGame, leftLobby, askedForCard; drop
  maxPlayers from createdLobby (always 2 players).
- State machine: joinedLobby validates open status + not-full, inserts the
  player, and fires the startGame query in the same transition on real
  insert. closedLobby verifies host + alone + status=open, then deletes
  the lobby and its lobby_players rows. Lobby IDs are now deterministic
  (blockHeight + addr slug) instead of Date.now().
- DB: drop lobbies.max_players and lobby_players.is_ready via new
  migration 3_simplify_lobbies. Rewrite lobby-queries.sql with new
  CountLobbyPlayers, DeleteLobbyPlayers, DeleteLobby; regenerate pgtyped
  bindings.
- API: /open_lobbies filters created_at > NOW() - INTERVAL '10 minutes'
  and drops max_players from SELECT.
- Frontend: remove ready/start/leave UI. LobbyListScreen has a single
  Create/Join flow; LobbyScreen shows Cancel only to the host while alone
  and auto-navigates on status=in_progress. effectstreamBridge exports
  closeLobby instead of toggleReady/startGame/leaveLobby.
- Tests: api.test.ts adds /open_lobbies shape, TTL (9min keep, 11min hide),
  and no-max_players assertions; mock pool extended accordingly.
- e2e: runLobbyFlow simplified to create → join → wait for in_progress.
- Docs: README and CLAUDE.md reflect the 3-command set.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Eight incremental smoke tests isolate each layer of the e2e pipeline
(imports, fs config, indexer subscription, lobby via batcher, init_deck,
applyMask with secrets, full per-game setup, one-turn play) so failures
localize quickly without running the full ~20 min game.

test-witnesses.ts is a self-contained witness implementation for the
Go Fish contract. It replaces the default @go-fish/midnight-contract
witnesses in the e2e path: explicit per-session WitnessState, throws on
missing secrets instead of silently falling back to random Math.random
static keys, and captures each player's shuffle seed during
player_secret_key so the deck shuffle is deterministic per (game, player).

deno.json adds the import-map entries needed by the smokes and the
shared e2e helpers (rxjs, @paimaexample/concise, midnight-js providers,
compact-js, local @go-fish/data-types and midnight-contract aliases).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… double-count

Three contract changes resolve BACKEND_ISSUES #3 (game stalls in
TurnStart with empty hand) and #4 (winner ledger never written):

1. GoFish.compact:addScore — replace the 7-book cap with an early-win
   at score ≥ 4 (majority of 7). Also fixes a read-after-write
   double-count bug: Compact's queryLedgerState returns the post-write
   value on re-read, so the old `lookup → insert → lookup → +1`
   pattern was triggering game-over at 3 books instead of 4. Now
   computes post-increment scores from the pre-read locally.

2. game.compact:askForCard — relax rule 5 when the asker's hand is
   empty. Routes empty-hand turns through respondToAsk's existing
   go-fish branch (no new circuit needed). Matches real Go Fish rules.

3. game.compact — add top-level getWinner wrapper so the managed
   contract exposes the V4 winner ledger reader.

Client: e2e helpers drop the empty-hand bail, game-round test adds
hard assertions for finalPhase==GameOver and winner∈{1,2}.

Validated: 164/164 contract tests pass, 27/27 node tests pass,
ec_mul guard detection clean, 3 consecutive sim runs all end at
exactly 4 books with correct winner.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…arden witnesses

Phase 0 (EVM lobby alignment):
- Remove maxPlayers from GoFishGameState type and in-memory state
  (DB column dropped per EVM_UPDATE.md; game is always 2 players)

Phase 1 (critical fixes):
- Fix callAfterGoFish: remove stale drewRequestedCard param (4→3 args).
  Contract v3 decrypts the drawn card internally (game.compact:451-510).
  Updated across all 6 files in the call chain (GoFishContractService,
  MidnightService, MidnightOnChainService, midnightBridge, GameScreen,
  GameScene).
- Delete callGoFish / goFish: no goFish circuit exists in the compiled
  contract. respondToAsk handles the draw internally. Turn flow is now:
  askForCard → respondToAsk → (if WaitForDrawCheck) afterGoFish.
- Allow empty-hand asks: contract rule-5 relaxation permits asking for
  any rank when hand is empty. GameScreen now shows an "Ask for a card"
  button instead of the old passive "Drawing will happen automatically"
  message that stalled the game.

Phase 2 (book scoring — without this, games never end):
- Add callCheckAndScoreBook to GoFishContractService (batcher delegation)
- Add onChainCheckAndScoreBook to MidnightOnChainService
- Add checkAndScoreBook routing to MidnightService
- Add autoScoreBooks() to GameScreen: after respondToAsk and afterGoFish,
  scans the player's hand for ranks with ≥3 cards and submits
  checkAndScoreBook for each. The contract's addScore checks ≥4 books
  and sets phase=GameOver + winner.

Phase 3 (winner display):
- Update renderGameOverPanel to use ≥4 threshold for winner determination
  (matches the contract's early-win rule). Added TODO for reading
  getWinner from the contract once direct indexer access is available.

Phase 5 (witness hardening):
- Remove silent Math.random() fallback from witnesses.ts getSecretKey
  and getShuffleSeed. Both now throw immediately on a miss with a
  descriptive error naming the game+player that's missing. Delete the
  static `keys` object that powered the fallback (20-bit random values
  that produced wrong ec_mul results). See BACKEND_ISSUES.md #1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces zk-time-estimator.ts: circuit→k table (captured from
`compact compile +0.30.0`), REFERENCE_MS sourced from ZKTIME.md, and
calibrateFromProof that folds each observed proof into a running-mean
scaleFactor persisted to localStorage.

Wires calibration end-to-end: midnightBrowserProofProvider now
exposes getLastProveDurationMs(); callDelegated resets it pre-call
and feeds the measured duration into calibrateFromProof after every
proof on a CIRCUIT_K-known circuit.

GlobalLoader gains a countdown suffix — callDelegated passes the
per-circuit expectedProof() time during proving and a flat 20s during
awaiting-confirmation. Overtime is rendered as +Ns in amber.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Subscribe to go-fish-contract ledger state via parallelMidnight sync
protocol and PrimitiveTypeMidnightGeneric; event_midnight handler logs
the decoded payload as a first observability hook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rework dealCards so both players can submit simultaneously without
stale-read proof conflicts. dealCards now consumes disjoint fixed deck
indices (P1→0..3, P2→4..7) instead of read-incrementing the shared
gameTopCardIndex counter, only touches its own hasDealt flag, and no
longer transitions phase. A new exported startGame(gameId, playerId)
circuit owns the Setup → TurnStart transition — self-dedups via
assert(phase == Setup) so repeated calls are safe.

- Deck.compact: pre-seed gameTopCardIndex to 8 in init_deck so
  post-setup draws start at the first undealt index without dealing
  ever reading the counter.
- game.compact: inline the decrypt+store pipeline into dealCard with
  an explicit cardIndex parameter; drop getTopCardForOpponent; add
  the startGame export.

ec_mul guard rule preserved — partial_decryption is still called
unconditionally from the unrolled dealCard loop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Score panel, game log, and turn bar now paint titles + shimmering "Loading…" text
the moment the HUD mounts, instead of sitting as empty backing boxes until the
first game-state poll settles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a ZK proof or tx send runs past its estimated duration, the bottom-left
loader now shows an orange subtitle at +2.0s ("Taking more than expected, please
wait…") and swaps to a longer reassurance message at +10.0s so users on slow
hardware know the app is still working.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Active Games sidebar was rendering a deterministic 4-card mock hand (seeded
from the lobby id) whenever no real hand was cached yet. Users saw card previews
that didn't match the actual game state. Also hardens the session→state
fallback path to skip rendering until playerId resolves to a real (1|2) value.

- Remove mockMiniHand entirely; miniHandFor now returns an empty array when
  there's no authoritative data.
- Only render the .mini-hand strip when there's at least one real card to show
  (or a +N overflow chip) — otherwise omit it entirely.
- Guard buildActiveGamesFromCacheAndSessions against the playerId=0 sentinel
  so we don't compute `state.scores[-1]` during the post-join indexing window.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- LobbyListScreen.joinLobby: clear pendingJoinLobbyId BEFORE dispatching
  navigate. dispatchEvent is synchronous; UIManager's listener called
  show() → innerHTML='' → render(), which bailed on the still-set
  pending flag and left the sidebar blank.

- GameSession: add refreshSnapshot() public method that emits a
  stateChange with empty changes. GameScene.attach calls it after
  hydrate so loader/turn-indicator render immediately instead of
  waiting for a real on-chain change that may never come. Also emitted
  when setupPhase flips to 'done' so rejoins of already-set-up games
  don't strand the view with setupPhase='idle' snapshot.

- Setup-flow canvas log entries: deduplicated "waiting for opponent"
  lines plus concrete progress markers (mask applied / deal confirmed
  / start submitted) so the user sees meaningful progress during the
  30–120s setup window.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cards taken by the opponent or scored into a book previously vanished instantly
from the hand — only a camera shake and toast notification signalled the loss.
Now the departing cards arc up and forward with a slight spin and shrink out
over ~900ms so the player actually sees the card go.

- animateCardLeave: new gsap tween (arc + spin + scale-to-zero) in the 800ms–
  1000ms band — the "fast enough to feel responsive, slow enough to be seen"
  range.
- CardHand.detachCards: split matching cards out of the hand without disposing
  them so the caller can reparent and animate independently.
- ThreeApp.animateCardLoss: detach by rank+suit, scene.attach to preserve world
  transform across the upcoming setCards clear(), animate, dispose on complete.
- GameScene.onStateChange: diff previous vs current myHand on handChanged and
  call animateCardLoss before setPlayerHand so the remaining cards re-close the
  fan immediately instead of waiting for the animation.
- Idle demo REMOVE branch: 50/50 between fly-to-deck and the new leave
  animation so the menu scene showcases both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Setup can block the UI for 30–90s while proofs generate (applyMask + dealCards),
but the 3D canvas previously sat static the whole time — users had no signal
that the app was still working. The deck now visibly shuffles through a rotating
sequence of 5 motions until setup completes.

- New ShuffleAnimations module with 5 variants (riffle, overhand, cascade, table
  spread, Hindu) and a non-repeating orchestrator. Returns a stop fn that kills
  in-flight tweens and settles every card back to its rest pose in ≤350ms so
  the subsequent deal animation inherits a clean stack.
- Respects prefers-reduced-motion: single slow cascade, no variant cycling.
- Deck3D exposes getCardMeshes() for the orchestrator to snapshot.
- ThreeApp.startDeckShuffle/stopDeckShuffle — start is kill-and-restart so
  repeated calls re-snapshot after setDeckCount rebuilds the card meshes.
- ThreeApp.setDeckCount auto-restarts the shuffle when one is active so the
  new card meshes replace the now-disposed snapshot.
- GameScene: start shuffle unconditionally in mount() (the host's pre-P2
  window has null contract state — a phase='dealing' gate would never fire);
  stop it once snapshot.setupPhase flips to 'done' or we detect attach to an
  already-in-progress game; stop on scene clear.
- GameScene.mount now also clears the idle-demo hand + resets deck count so
  a freshly-created lobby doesn't inherit random cards from the menu scene.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Users could previously click Create Lobby, Join, Resume, or Dismiss while a
proof was running or a tx was posting, triggering either a racing second tx
or an orphaned operation (the loader then pointed at a game the user had
just left). Every game-switch / game-mutation entry point is now guarded.

- GlobalLoader: new isTxInFlight() (true for 'proving' or 'sending', not
  for passive 'waiting') and onTxInFlightChange(cb) subscription API that
  fires on every transition including those hidden by mute.
- txGuard.ts: ensureNotBusy() shows a centered orange toast ("Blockchain
  operation in progress — please wait.") and returns false. guardedClick()
  wraps a handler for drop-in guarding.
- main.ts: subscribes at bootstrap to toggle body.tx-in-flight so every
  .tx-guarded element visibly dims (0.55 opacity, not-allowed cursor).
- LobbyListScreen: guards Create (confirm button), Join (tx path), Resume,
  Join-with-Rejoin (foreground swap), and Dismiss. All the same buttons
  also get the .tx-guarded class for visible disabled state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
acedward and others added 30 commits April 29, 2026 23:24
…PC sync

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update all @paimaexample dependencies from 0.11.1/0.10.24 to 0.11.3
and add Google Tag Manager (GTM-KJX5XHXL) to the frontend index.html.
…11.3-and-gtm

chore: upgrade @paimaexample/* to 0.11.3 and add GTM
update to effectstream 0.100.21 and bun migration
…nd build, gateway routing)

Follow-up fixes found while deploying the effectstream 0.100.21 + bun migration
to mainnet. Each is required for go-fish to actually run on the new contract.

- node: add missing `wit_split_card_index` witness to actionWitnesses. The
  compiled contract requires it; without it `initializeActionContract` throws
  and the node crash-loops once sync reaches live game-action events. Mirrors
  the frontend midnightBridge witness (index = suit*7 + rank, returned as
  bigints).

- evm: fix deploy.mainnet.ts — it still imported the deleted GoFishLobby module
  and referenced result.paimaL2Contract. Point it at effectstreaml2-module on
  the arbitrum network so the mainnet deploy works.

- deployed_addresses.json + frontend VITE_PAIMA_L2_CONTRACT_ADDRESS: record the
  redeployed mainnet effectstreaml2 contract 0x148bC957bFbCd344A50ec9cC08654c6e2A9656c2.

- frontend/vite.config.ts: nodePolyfills `overrides: { fs: memfs, 'node:fs': memfs }`
  so server-only modules pulled into the bundle (get-wallet-info via
  @effectstream/wallets) resolve named fs exports instead of the empty mock.
  Matches block-kart. Without it `build:mainnet` fails.

- config.mainnet.ts: route EVM reads through ARBITRUM_ONE_RPC (the local
  evm-gateway cache) instead of arbitrum's default public RPC, matching the
  other migrated games.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ixes

fix: complete 0.100.21 mainnet deploy (witness, deploy script, frontend build, gateway routing)
Follow-up to #8. The node constructs the Midnight contract in two places —
actionWitnesses (midnight-actions.ts, fixed in #8) and queryWitnesses
(midnight-query.ts, missed). Both must declare every witness the compiled
contract requires, or `new Contract(...)` throws and the node crash-loops.

initializeQueryContract was still dying on the missing wit_split_card_index.
Added as a throwing stub matching the other query witnesses (the query
contract only reads ledger state, never runs circuits).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix: add wit_split_card_index to queryWitnesses (follow-up to #8)
After the witness fixes (#8, #9) the node still crash-looped — but on a
different, silent exit(1) during live Midnight event processing. Traced via a
process.exit/unhandledRejection probe to:

  TypeError: Cannot close a writable stream that is closed or errored
    at close (@seriousme/opifex/dist/socket/socket.js:33)   <- this.writer.close()
    at close (.../mqttConn/mqttConn.js:171)
    at #receive (.../mqttConn/mqttConn.js:131)               <- MQTT client teardown
  -> unhandledRejection -> @effectstream/log process-handlers.ts:48 -> process.exit(1)

opifex's socket close() wraps `this.writer.close()` in a synchronous try/catch
("swallow any errors on close"), but `WritableStreamDefaultWriter.close()`
returns a Promise that rejects *asynchronously* when the stream is already
closed. The sync try/catch can't catch it; under bun the floating rejection is
fatal (deno tolerated it, which is why this only bites post-bun-migration).

Patch opifex in patch.sh (same mechanism we already use for hardhat/fetch-blob)
to add `.catch(() => {})` so the benign async close rejection is swallowed too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix: swallow async opifex MQTT socket close rejection (node crash loop)
Patch @seriousme/opifex socket close() and write() with .catch()
…address-format

Patch @midnight-ntwrk/wallet-sdk-address-format 3.x for the frontend bundle.
…Size-reset

Fix: Frontend case locked `isHandReady`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants