Skip to content
Draft

V2 #4

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
081c87d
clean up stage 1
acedward Apr 13, 2026
e26452d
Continue cleanup
acedward Apr 15, 2026
ff9bdb3
Simplify EVM lobby flow to create/join with auto-start and TTL
acedward Apr 15, 2026
f5ff6e4
Add e2e smoke tests and test-owned witness module
acedward Apr 15, 2026
712bfe3
Fix end-game stall: early-win at ≥4 books + empty-hand ask + addScore…
acedward Apr 16, 2026
1555553
Updates
acedward Apr 16, 2026
68ec4e7
Align frontend with e2e reference: fix circuits, wire book scoring, h…
acedward Apr 16, 2026
e3bad27
e2e testing. from evm lobby to a full game.
acedward Apr 17, 2026
ec2eadd
frontend updates: new logic, flow, improved ui and ux
acedward Apr 17, 2026
3871f9d
Update GoFish contract. Now checking for Books after new cards in hand.
acedward Apr 17, 2026
c8a2f39
Contract update
acedward Apr 20, 2026
08dd9f3
frontend updates
acedward Apr 20, 2026
0e1fdc3
tests updates
acedward Apr 20, 2026
1fe25da
Add ZKTIME.md with ZK proof benchmark min/avg/max per circuit
acedward Apr 20, 2026
f667b1c
Add per-machine ZK proof time estimator + loader countdown
acedward Apr 20, 2026
98d5ea2
Wire minimal Midnight event handler into Paima STM
acedward Apr 20, 2026
7977691
V4.3: parallel-safe dealCards via fixed deck indices
acedward Apr 20, 2026
381a1e1
evm db update
acedward Apr 21, 2026
79e9c16
e2e update
acedward Apr 21, 2026
5339e39
favicon
acedward Apr 21, 2026
91dc1d0
major frontend refactor
acedward Apr 21, 2026
3a77d1c
test update
acedward Apr 21, 2026
96c0b83
UI UX State management fixes
acedward Apr 21, 2026
d6a425f
HUD empty-box + Loading placeholders (PR B1)
acedward Apr 21, 2026
39430b7
Loader overtime hint (PR B2)
acedward Apr 21, 2026
c455053
Mini-card correctness in Active Games list (PR B3)
acedward Apr 21, 2026
fc8af14
Fix empty sidebar after join + HUD blank on session swap
acedward Apr 21, 2026
b0878e6
Card-leaving animation (PR B5)
acedward Apr 21, 2026
25549ea
Shuffle animation during Setup (PR B6)
acedward Apr 21, 2026
377da03
Tx-in-flight interaction lock (PR B7)
acedward Apr 21, 2026
03508a7
Menu selection highlight + top-bar lobby name (PR B8)
acedward Apr 21, 2026
97ac461
Turn timeout → 600s, concede handles Setup, assertNowWithinWindow helper
acedward Apr 21, 2026
178fff1
FEATURES_PLAN: Part B complete, Part A plan, gitignore additions
acedward Apr 21, 2026
a5f2a53
Service plumbing for concede + winner / lastMoveAt reads (PR A1)
acedward Apr 21, 2026
c6a0bed
3-dots menu + Concede / Close Game + winner-aware GameOver (PR A2)
acedward Apr 21, 2026
de7701b
Re-export submodule ledgers at top-level contract ABI
acedward Apr 21, 2026
1aa52af
Timeout countdown pill + Claim Win button (PR A3)
acedward Apr 21, 2026
5ac27db
Add contract-initialize.ts for post-deploy owner + deck init
acedward Apr 22, 2026
0798185
Add How to Play and About modals to lobby sidebar
acedward Apr 23, 2026
6c2c846
Fix winner resolution, batch contract reads, and score panel UX
acedward Apr 23, 2026
90e591b
git ignore
acedward Apr 23, 2026
46c3d33
evm update w/contract @ mainnet
acedward Apr 23, 2026
a7bea49
fixed envs, using standard
acedward Apr 23, 2026
7c8a76f
added contract address
acedward Apr 23, 2026
d1394f2
mainnet updates
acedward Apr 23, 2026
6d98b72
frontend env
acedward Apr 23, 2026
3955872
update script. fix contract
acedward Apr 23, 2026
b402449
mainnet deno
acedward Apr 23, 2026
7b2ee77
fix vite-env.d.ts TS errors and revert unsupported NetworkId.mainnet
acedward Apr 23, 2026
a7399a5
fix broken BASE_URL_MIDNIGHT_INDEXER reference after env refactor
acedward Apr 23, 2026
40ebed8
fix all tsc errors: bump to ES2022, resolve SDK type mismatches
acedward Apr 23, 2026
effae73
batcher evm
acedward Apr 23, 2026
7d7c591
frontend arb
acedward Apr 23, 2026
92c21ce
hh->arb
acedward Apr 23, 2026
0c39c87
update
acedward Apr 23, 2026
f65ba58
fix purple sidebar in prod: remove legacy body gradient from game-sty…
acedward Apr 23, 2026
7375d07
remove legacy game-styles.css link from index.html
acedward Apr 24, 2026
14e9356
add missing resume-dismiss-btn styles to inline CSS
acedward Apr 24, 2026
4bbebce
fix leaderboard to aggregate by permanent EVM wallet address
acedward Apr 24, 2026
fc95708
fix card selection not appearing after successful ask
acedward Apr 24, 2026
2c11b4d
fix css
acedward Apr 24, 2026
8f46b9f
update @paimaexample packages to 0.11.1 and add stepSize: 30 to EVM R…
acedward Apr 30, 2026
0b754c9
chore: upgrade @paimaexample/* to 0.11.3 and add GTM tracking
acedward May 12, 2026
e2e13c0
Merge pull request #5 from effectstream/chore/upgrade-paimaexample-0.…
acedward May 12, 2026
430b9f7
added admin_secret_ky circuit stub
Yiakman May 13, 2026
79c4c1e
Merge pull request #6 from effectstream/fix/admin_secret_key-stub
Yiakman May 13, 2026
b11ab7b
update to effectstream 0.100.21 and bun migration
Yiakman Jun 12, 2026
8919236
Merge pull request #7 from effectstream/update/effectsream-0.100.21
Yiakman Jun 12, 2026
d6111db
fix: complete 0.100.21 mainnet deploy (witness, deploy script, fronte…
Yiakman Jun 12, 2026
ca9ef52
Merge pull request #8 from effectstream/fix/0.100.21-mainnet-deploy-f…
Yiakman Jun 12, 2026
de17031
fix: add wit_split_card_index to queryWitnesses too
Yiakman Jun 12, 2026
f9a5208
Merge pull request #9 from effectstream/fix/query-witness-split-card
Yiakman Jun 13, 2026
6f94e83
fix: swallow async opifex MQTT socket close rejection (node crash loop)
Yiakman Jun 13, 2026
f421d29
Merge pull request #10 from effectstream/fix/opifex-mqtt-close-crash
Yiakman Jun 13, 2026
f2dc8ef
update effectstream to 0.100.23
Yiakman Jun 17, 2026
5869c16
Merge pull request #11 from effectstream/update/effectstream-0.100.23
Yiakman Jun 17, 2026
b1e550d
Patch @seriousme/opifex socket close() and write() with .catch()
Yiakman Jun 17, 2026
38db4af
Merge pull request #12 from effectstream/fix/patch-opifex-socket-close
Yiakman Jun 17, 2026
ab3e7a2
patch midnight adress format sdk for frontend hoisting with vite-top-…
Yiakman Jun 17, 2026
cea4c5c
Merge pull request #13 from effectstream/fix/patch-bundle-wallet-sdk-…
Yiakman Jun 17, 2026
6e2d9bc
fix: locked handIsReady because of stuck expectedMinHandSize
Yiakman Jun 18, 2026
8723d30
added launch.json for .claude to preview frontend
Yiakman Jun 18, 2026
e66a8af
Merge pull request #14 from effectstream/fix/frontend-expectedMinHand…
Yiakman Jun 18, 2026
52f31f8
update to effectstream 0.100.26
Yiakman Jun 19, 2026
7f42ee0
Merge pull request #15 from effectstream/update/effectstream-0.100.26
Yiakman Jun 19, 2026
b534e4d
update to effectstream 0.100.27
Yiakman Jun 20, 2026
199d991
update effectstream to 0.100.28
Yiakman Jun 20, 2026
7975777
Merge pull request #16 from effectstream/update/effectstream-0.100.27
Yiakman Jul 14, 2026
09685e0
Bump effectstream top 0.101.1
Yiakman Jul 14, 2026
a4d7326
Merge pull request #17 from effectstream/update/effectstream-0.101.1
Yiakman Jul 14, 2026
12feaf9
Add MIT license
acedward Aug 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
12 changes: 12 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "frontend-preview",
"runtimeExecutable": "bunx",
"runtimeArgs": ["vite", "preview", "--mode", "mainnet", "--port", "4173", "--host"],
"cwd": "frontend",
"port": 4173
}
]
}
8 changes: 4 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ build/
.DS_Store
Thumbs.db

# Deno
deno.lock

# Database
*.db
*.sqlite
Expand All @@ -35,4 +32,7 @@ logs/
*.log

# Midnight database
packages/shared/contracts/midnight/midnight-level-db-deploy/*
midnight-level-db-deploy/
pending-inputs.jsonl
midnight-level-db-go-fish-initialize/
midnight-level-db-cleanup/
140 changes: 140 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Go Fish card game built on **Paima Engine (Effectstream)** with a hybrid blockchain architecture:
- **EVM (Solidity/Hardhat)**: Lobbies, matchmaking, player stats
- **Midnight (Compact language)**: Private game logic, card shuffling/dealing with ZK proofs
- **Paima Node (Bun)**: State machine processing blockchain events, REST API
- **Frontend**: Three.js 3D game scene with HTML overlay screens (Vite + TypeScript)

## Build & Run Commands

```bash
# First time setup
bun install

# Build EVM contracts (required before first run)
bun run build:evm

# Full stack dev (launches frontend:3000, API:9996, EVM:8545, explorer:10590)
bun run dev

# Dev with batcher mode (no Lace wallet needed)
bun run dev:batcher

# Dev with TypeScript contract (no Midnight infrastructure needed)
bun run dev:typescript

# Frontend only
bun run frontend:dev

# Build Midnight contracts
bun run build:midnight
```

### Testing

```bash
# Run node tests (bun:test, api.test.ts)
bun run test

# Watch mode
bun run --filter @go-fish/node test:watch

# Run a single test file directly
bun test packages/client/node/src/api.test.ts

# Midnight contract tests
cd packages/shared/contracts/midnight && bun run test

# EVM contract tests
cd packages/shared/contracts/evm && npx hardhat test

# E2E smoke tests
bun run --filter @go-fish/e2e smoke:imports
bun run test:e2e
```

### EVM Contract Build Details

```bash
# Full compile (forge + hardhat + deploy + artifact generation)
bun run --filter @go-fish/evm-contracts contract:compile

# Individual steps
cd packages/shared/contracts/evm
bun run build:forge # Forge only
bun run build:hardhat # Hardhat only
bun run deploy:standalone # Start chain, deploy, stop
```

### Midnight Infrastructure (for full Midnight dev)

```bash
# Terminal 1: Start all Midnight services
EFFECTSTREAM_STDOUT=true bun run --filter @go-fish/node midnight:setup

# Terminal 2: Dev server connecting to running infra
USE_TYPESCRIPT_CONTRACT=false EFFECTSTREAM_STDOUT=true USE_BATCHER_MODE=true SKIP_MIDNIGHT_INFRA=true bun run dev
```

## Architecture

```
Frontend (Vite/Three.js :3000)
├─ HTTP /api ──► Paima Node (:9996) ──► PGLite DB
│ ├─ EVM read (Hardhat :8545)
│ └─ Midnight query (Indexer :8088)
└─ HTTP /batcher-query ──► Batcher (:3334) ──► Midnight
```

**Data flow by phase:**
1. **Lobby** (EVM): Frontend → PaimaL2Contract → Paima state machine → PGLite
2. **Game** (Midnight): Frontend → Midnight contract (ZK proofs) → Indexer → Paima queries
3. **Scoring**: Game completion → leaderboard calculation → PGLite persistence

### Package Structure

- `packages/frontend/` — Vite SPA: Three.js scenes, screen overlays, wallet bridges
- `packages/client/node/` — Paima node: state machine (`state-machine.ts`), REST API (`api.ts`), Midnight integration
- `packages/client/batcher/` — Transaction batching service for Midnight
- `packages/client/database/` — PGLite schema, migrations (`mod.ts`), SQL queries (pgtyped)
- `packages/shared/data-types/` — Shared types, Paima concise grammar, config
- `packages/shared/contracts/evm/` — Solidity: `GoFishLobby.sol`, `PaimaL2Contract.sol`
- `packages/shared/contracts/midnight/` — Compact: `GoFish.compact`, `Deck.compact`, `Hand.compact`, etc.

### Key Environment Variables

- `USE_TYPESCRIPT_CONTRACT=true` — Local TypeScript contract (skip Midnight infra)
- `USE_BATCHER_MODE=true` — Batcher mode (no Lace wallet needed)
- `SKIP_EVM_LAUNCH=true` — Skip Hardhat if already running
- `SKIP_MIDNIGHT_INFRA=true` — Skip Midnight services
- `EFFECTSTREAM_STDOUT=true` — Log to stdout instead of tmux

## Game Domain

Simplified 21-card deck: 7 ranks (A,2-7) x 3 suits (hearts, diamonds, clubs). Book = 3 cards of same rank. 4 cards dealt per player. Card index formula: `rank_index + (suit_index * 7)` → 0-20.

Paima concise grammar commands (defined in `data-types/src/grammar.ts`):
- Lobby: `createdLobby`, `joinedLobby`, `closedLobby`
- `joinedLobby` auto-starts the game: the 2nd player always fills the lobby, so
the state machine flips `status='open'` → `'in_progress'` in the same transition.
- `closedLobby` is host-only and only valid while the host is alone.
- Open lobbies disappear from `/open_lobbies` 10 minutes after creation (soft TTL).
- Game-round actions (ask for card, respond, go fish, score books) run on the
Midnight contract as ZK circuits, not the EVM grammar.

## Critical: Midnight ec_mul Guard Bug

**Never put `std_ecMul` (or any function calling it) inside a Compact `if/else` branch.** The ZKIR compiler wraps branch inputs with guards that produce `(0,0)` on inactive branches, and `ec_mul` has no guard support — it panics on invalid curve points.

**Fix pattern:** Fetch all secrets unconditionally, use ternary (`? :`) to select the right value (compiles to safe `cond_select`), then call `ec_mul` once unconditionally.

**Verify after compiling:** Run the detection script in `ZKIR-EC-MUL-GUARD-BUG.md` after every `bun run --filter @go-fish/midnight-contract compact` to ensure no circuit has both `ec_mul` and guarded `public_input` ops.

## Compact Point Comparison

When comparing `JubjubPoint` values in Compact 0.30.0, do **not** use `===` (it will silently fail). Instead, hash both points and compare the hashes.
149 changes: 149 additions & 0 deletions EVM_UPDATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# EVM Lobby Simplification Spec

Collapse the EVM-side lobby flow to **two user actions** — *Create* and *Join* — with auto-start on join, host cancel, and a 10-minute TTL on stale lobbies.

## Goals

1. Two user actions only: **Create** and **Join** (join auto-starts the game).
2. Host can **Cancel** an open lobby while no one else has joined.
3. Open lobbies disappear from the UI after **10 minutes**.
4. Max players is a constant (**2**) — always two players.
5. Drop all dead or now-unused commands from the EVM grammar.

## Final command set

After this refactor, the EVM `goFishL2Grammar` has exactly three commands:

| Command | Fields | Semantics |
|---|---|---|
| `createdLobby` | `playerName`, `lobbyName` | Creates a new lobby, host inserted as sole player. |
| `joinedLobby` | `playerName`, `lobbyID` | Second player joins. Handler unconditionally flips `status='in_progress'` and sets `started_at`. |
| `closedLobby` | `lobbyID` | Host cancels while still alone. Deletes the lobby row (cascades to `lobby_players`). |

Commands **deleted**: `toggledReady`, `startedGame`, `leftLobby`, `askedForCard`.

Note on `askedForCard`: this is the Paima grammar stub (past tense). It has no state-machine handler, no frontend encoder, no test — only two doc references. **Do not confuse** with the Midnight circuit `askForCard` (imperative, no "ed") at `packages/shared/contracts/midnight/go-fish-contract/src/game.compact:279`, which is the actual ZK gameplay circuit and must not be touched.

## File-by-file changes

### `packages/shared/data-types/src/grammar.ts`
- Delete `toggledReady`, `startedGame`, `leftLobby`, `askedForCard` entries.
- Keep `closedLobby` (already defined) — just ensure it survives.
- Modify `createdLobby`: remove the `maxPlayers` field. Final shape:
```ts
createdLobby: [
['playerName', PlayerName],
['lobbyName', LobbyName],
],
```
- Final exported `goFishL2Grammar` contains only `createdLobby`, `joinedLobby`, `closedLobby`.

### `packages/client/node/src/state-machine.ts`
- **Delete handlers**: `toggledReady` (lines ~208–237), `startedGame` (~242–271), `leftLobby` (~276–305).
- **`createdLobby` handler (~22–118)**:
- Remove the `data.parsedInput.maxPlayers` read.
- Drop the `is_ready` column write when inserting the host into `lobby_players`.
- Replace `lobby_${data.blockHeight}_${Date.now()}` with a deterministic ID (see *Determinism* below).
- **`joinedLobby` handler (~123–203)**:
- After the `lobby_players` insert, unconditionally run the existing `startGame` update (set `status='in_progress'`, `started_at=<ts>`) in the same transition. No player-count check needed — the second `joinedLobby` is always the final player.
- Remove any host-only check (never applied here anyway).
- **Add new `closedLobby` handler**:
- Resolve signer → account_id.
- `SELECT host_account_id, status FROM lobbies WHERE lobby_id = ?`.
- Assert `host_account_id == signer_account_id`.
- Assert `status = 'open'`.
- Assert `(SELECT COUNT(*) FROM lobby_players WHERE lobby_id = ?) = 1` — only the host present.
- `DELETE FROM lobbies WHERE lobby_id = ?`. Relies on `ON DELETE CASCADE` on the `lobby_players.lobby_id` FK (see schema section).
- Any failed assertion: log and drop the transition (no-op), do not throw.

### `packages/client/database/migrations/database.sql`
- **Drop column** `lobbies.max_players`.
- **Drop column** `lobby_players.is_ready`.
- **Keep** `lobbies.created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP` — the TTL filter depends on it.
- **Pre-flight check**: verify the `lobby_players.lobby_id` FK has `ON DELETE CASCADE`. If missing, add it in the same migration. Otherwise the `closedLobby` handler needs a second explicit `DELETE FROM lobby_players WHERE lobby_id = ?` before the lobby delete.
- Indexes on `status` and `created_at DESC` stay — they support the TTL-filtered list query.

### `packages/client/database/src/lobby-queries.sql` (and generated pgtyped output)
- **Delete** the `togglePlayerReady` query.
- **Delete** the `startGame` query if it's still standalone — or keep it and call it from `joinedLobby`. Decide during implementation based on whether pgtyped reuse is cleaner than inlining.
- **Delete** any query that references `max_players` or `is_ready`.
- **Add** a `deleteLobby` query: `DELETE FROM lobbies WHERE lobby_id = :lobby_id`.
- **Add** a `countLobbyPlayers` query for the `closedLobby` assertion.
- Update the `openLobbies` SELECT — drop `max_players` from the column list.

### `packages/client/node/src/api.ts`
- `/open_lobbies` (~lines 78–125):
- Modify the WHERE clause at ~line 112:
```sql
WHERE l.status = 'open'
AND l.created_at > NOW() - INTERVAL '10 minutes'
```
- Drop `max_players` from the SELECT list.
- Drop `max_players` from the TypeScript response type.
- Keep pagination, ordering (`created_at DESC`), and the optional `?wallet=` + `is_player_in_lobby` subquery (harmless, frontend may still want it).

### Shared TS types
- Wherever the `LobbySummary` / `OpenLobbyRow` type lives (likely `packages/shared/data-types/src/`), remove `maxPlayers` / `max_players`.

### Frontend (`packages/frontend/src/`)
- `screens/LobbyListScreen.ts`:
- Per row: **Join** button for others' lobbies, **Cancel** button for your own open lobby (submits `closedLobby`). No ready toggle, no start button, no max-players display.
- Keep the 4000ms poll at line ~24.
- On poll tick, if the current player is a member of a lobby whose status flipped to `in_progress`, navigate to the game screen. The host, still on the waiting screen, routes through the same mechanism.
- `screens/CreateLobbyScreen.ts` (or equivalent):
- Remove any max-players selector. Submit `createdLobby(playerName, lobbyName)` only.
- `services/GoFishGameService.ts` (~line 133 area):
- Delete encoders/helpers for `toggledReady`, `startedGame`, `leftLobby`, and the Paima-grammar `askedForCard` if present. **Do not touch** any Midnight `askForCard` helpers.
- Update the `createLobby(...)` helper signature to drop `maxPlayers`.
- Update the `/open_lobbies` fetch response type to match the API change.

### Docs
- `README.md` line ~190: remove the `askedForCard` bullet, update the commands list to the new three-command set.
- `CLAUDE.md` line ~119: update the "Paima concise grammar commands" section. The **Lobby** group becomes `createdLobby`, `joinedLobby`, `closedLobby`. Delete the **Game: `askedForCard`** line entirely.

### Tests (`packages/client/node/src/api.test.ts`)
- **Delete / rewrite** any test referencing `toggledReady`, `startedGame`, `leftLobby`, or `maxPlayers`.
- **Add** — auto-start: submit `createdLobby` then `joinedLobby`; assert `lobbies.status = 'in_progress'` and `started_at IS NOT NULL`.
- **Add** — `closedLobby` happy path: host alone, submits close, lobby row gone, `lobby_players` rows gone.
- **Add** — `closedLobby` rejection: non-host signer → no-op; second player present → no-op; `status != 'open'` → no-op.
- **Add** — TTL filter: insert a lobby with `created_at = NOW() - INTERVAL '11 minutes'`, `GET /open_lobbies`, assert absent. Insert a sibling with `created_at = NOW() - INTERVAL '9 minutes'`, assert present.

## Determinism fix (separate commit, recommended)

Today `state-machine.ts:29` uses `Date.now()` in the lobby ID, and the DB defaults `created_at` to `CURRENT_TIMESTAMP`. Both are non-deterministic on state-machine replay. The TTL filter now makes this matter: two replays could land a lobby on different sides of the 10-minute cutoff.

- Replace the lobby ID suffix with the block timestamp from the event payload, if Paima exposes one. Fallback: `data.blockHeight` alone is sufficient for uniqueness.
- Write `created_at` from the handler (parameterised) using the same block timestamp, instead of relying on `DEFAULT CURRENT_TIMESTAMP`.
- Pre-flight: verify whether the Paima runtime surfaces a block timestamp on the event `data` object. If not, document the limitation and defer the fix.

This can ship as an independent commit before or after the main refactor.

## Implementation order

1. **Grammar** deletions + `createdLobby` field drop. Expect compile errors to pinpoint every call site that still references the old commands — they guide steps 2 and 5.
2. **State machine**: delete dead handlers, modify `createdLobby` and `joinedLobby`, add `closedLobby`.
3. **Database**: schema migration (drop columns, verify cascade), update `lobby-queries.sql`, regenerate pgtyped.
4. **API**: `/open_lobbies` TTL filter and response type.
5. **Frontend**: button cleanup, create-lobby form, auto-nav on status flip, response type update.
6. **Tests**: delete obsolete, add new (auto-start, close, TTL).
7. **Docs**: `README.md`, `CLAUDE.md`.
8. **(Optional)** Determinism fix as its own commit.

## Out of scope

- Midnight-side circuits, including `askForCard` / `respondToAsk` / `goFish` / `afterGoFish`. None of those are touched.
- Hard TTL (cron/state-machine sweep that writes an `expired` status). Soft query-time filter is sufficient for the UI requirement.
- Leaderboard / stats / scoring flows.
- Any `PaimaL2Contract.sol` change — the contract stays a thin `paimaSubmitGameInput(bytes)` wrapper.

## Acceptance checklist

- [ ] `goFishL2Grammar` exports exactly `createdLobby`, `joinedLobby`, `closedLobby`.
- [ ] Submitting `joinedLobby` as the second player transitions `lobbies.status` to `'in_progress'` in one state-machine step.
- [ ] Host submitting `closedLobby` while alone deletes the lobby and its `lobby_players` rows.
- [ ] Non-host `closedLobby`, or `closedLobby` with a second player present, is a no-op.
- [ ] `GET /open_lobbies` never returns rows older than 10 minutes.
- [ ] `lobbies.max_players` and `lobby_players.is_ready` columns no longer exist.
- [ ] Frontend shows only **Create**, **Join**, **Cancel** buttons in the lobby flow; no ready/start UI.
- [ ] No reference to `askedForCard` anywhere in `packages/` or docs. References to Midnight `askForCard` are untouched.
- [ ] `api.test.ts` passes with the new auto-start, close, and TTL tests.
Loading