14 plans in 6 waves covering all 68 requirements for the Tournament Engine phase. Includes research (go-libsql, NATS JetStream, Svelte 5 runes, ICM complexity), plan verification (2 iterations), and user feedback (hand-for-hand UX, SEAT-06 reword, re-entry semantics, integration test, DKK defaults, JWT 7-day expiry, clock tap safety). Wave structure: 1: A (scaffold), B (schema) 2: C (auth/audit), D (clock), E (templates), J (frontend scaffold) 3: F (financial), H (seating), M (layout shell) 4: G (player management) 5: I (tournament lifecycle) 6: K (overview/financials), L (players), N (tables/more) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
16 KiB
Plan I: Tournament Lifecycle + Multi-Tournament + Chop/Deal
wave: 5 depends_on: [01-PLAN-D, 01-PLAN-F, 01-PLAN-G, 01-PLAN-H] files_modified:
- internal/tournament/tournament.go
- internal/tournament/state.go
- internal/tournament/multi.go
- internal/financial/icm.go
- internal/financial/chop.go
- internal/server/routes/tournaments.go
- internal/financial/icm_test.go
- internal/financial/chop_test.go
- internal/tournament/tournament_test.go
- internal/tournament/integration_test.go autonomous: true requirements: [MULTI-01, MULTI-02, FIN-11, SEAT-06]
Goal
The tournament lifecycle — create from template, configure, start, run, and close — works end-to-end. Multiple simultaneous tournaments run with fully independent state (clocks, financials, players, tables). A tournament lobby shows all active tournaments. Chop/deal support (ICM, chip-chop, even-chop, custom) handles end-game scenarios. The tap-tap seat move flow from the seating engine is wired into the full tournament context.
Context
- Clock engine — Plan D (running, independent per tournament)
- Financial engine — Plan F (transactions, prize pool, payouts)
- Player management — Plan G (buy-in, bust-out, rankings)
- Seating engine — Plan H (tables, balancing, break table)
- Template-first creation — CONTEXT.md: TD picks template, everything pre-fills
- Tournament auto-closes when one player remains (CONTEXT.md)
- ICM calculator: Malmuth-Harville exact for <=10 players, Monte Carlo for 11+ (01-RESEARCH.md Pitfall 3)
User Decisions (from CONTEXT.md)
- Template-first creation — pick template → pre-fill → tweak → Start
- Local changes by default — tournament gets a copy of building blocks
- Tournament auto-closes when one player remains — no manual "end tournament" button
- Multi-tournament switching — tabs at top (phone) or split view (tablet landscape)
- Flexible chop/deal support — ICM, custom split, partial chop, any number of players
- Prize money and league positions are independent — money can be chopped but positions determined by play
- Minimum player threshold — Start button unavailable until met
Tasks
**1. Tournament Service** (`internal/tournament/tournament.go`): - `TournamentService` struct with db, clock registry, financial engine, player service, seating service, audit trail, hub-
CreateFromTemplate(ctx, templateID string, overrides TournamentOverrides) (*Tournament, error):- Load expanded template (all building blocks)
- Create tournament record with status "created"
- Copy all building block references (chip_set_id, blind_structure_id, etc.) — local copy semantics
- Apply overrides (name, min/max players, bonuses, PKO flag, etc.)
- Create tables from blueprint if specified, or empty table set
- Record audit entry
- Broadcast tournament.created event
- Return tournament
-
CreateManual(ctx, config TournamentConfig) (*Tournament, error):- Create without a template — manually specify all config
- Same flow but no template reference
-
GetTournament(ctx, id string) (*TournamentDetail, error):- Return full tournament state:
type TournamentDetail struct { Tournament Tournament ClockSnapshot *ClockSnapshot Tables []TableDetail Players PlayerSummary // counts: registered, active, busted PrizePool PrizePoolSummary Rankings []PlayerRanking RecentActivity []AuditEntry // last 20 audit entries BalanceStatus *BalanceStatus }
- Return full tournament state:
-
StartTournament(ctx, id string) error:- Validate minimum players met
- Validate at least one table exists with seats
- Set status to "registering" → "running"
- Start the clock engine (loads blind structure, starts countdown)
- Set started_at timestamp
- Record audit entry
- Broadcast tournament.started event
-
PauseTournament(ctx, id string) error:- Pause the clock
- Set status to "paused"
- Record audit entry, broadcast
-
ResumeTournament(ctx, id string) error:- Resume the clock
- Set status to "running"
- Record audit entry, broadcast
-
EndTournament(ctx, id string) error:- Called automatically when 1 player remains, or manually for deals
- Stop the clock
- Assign finishing positions to remaining active players
- Calculate and apply final payouts
- Set status to "completed", ended_at timestamp
- Record audit entry
- Broadcast tournament.ended event
-
CancelTournament(ctx, id string) error:- Set status to "cancelled"
- Stop clock if running
- Void all pending transactions (mark as cancelled, not deleted)
- Record audit entry, broadcast
-
CheckAutoClose(ctx, id string) error:- Called after every bust-out (from player management)
- Count remaining active players
- If 1 remaining: auto-close tournament
- If 0 remaining (edge case): cancel tournament
2. Tournament State Aggregation (internal/tournament/state.go):
-
GetTournamentState(ctx, id string) (*TournamentState, error):- Aggregates all state for WebSocket snapshot:
- Clock state (from clock engine)
- Player counts (registered, active, busted)
- Table states (all tables with seats)
- Financial summary (prize pool, entries, rebuys)
- Rankings (current)
- Balance status
- This is what new WebSocket connections receive (replaces the stub from Plan A)
- Sent as a single JSON message on connect
- Aggregates all state for WebSocket snapshot:
-
BuildActivityFeed(ctx, tournamentID string, limit int) ([]ActivityEntry, error):- Convert recent audit entries into human-readable activity items:
type ActivityEntry struct { Timestamp int64 `json:"timestamp"` Type string `json:"type"` // "bust", "buyin", "rebuy", "clock", "seat", etc. Title string `json:"title"` // "John Smith busted by Jane Doe" Description string `json:"description"` // "Table 1, Seat 4 → 12th place" Icon string `json:"icon"` // For frontend rendering }
- Convert recent audit entries into human-readable activity items:
3. Multi-Tournament Manager (internal/tournament/multi.go) — MULTI-01:
-
MultiManagerstruct — manages multiple tournament services (or uses tournament-scoped queries) -
ListActiveTournaments(ctx) ([]TournamentSummary, error)— MULTI-02:- Return all tournaments with status in (registering, running, paused, final_table)
- Summary: ID, name, status, player count, current level, blind, remaining time
- This powers the tournament lobby view
-
GetTournamentSummary(ctx, id string) (*TournamentSummary, error):- Lightweight summary for lobby display
-
Independence guarantee: Every piece of state (clock, players, tables, financials) is scoped by tournament_id. No global singletons. Multiple tournaments run simultaneously with zero interference.
4. API Routes (internal/server/routes/tournaments.go):
POST /api/v1/tournaments— create from template: body:{"template_id": "...", "name": "Friday Night Turbo", "overrides": {...}}POST /api/v1/tournaments/manual— create without templateGET /api/v1/tournaments— list all (active and recent)GET /api/v1/tournaments/active— active only (lobby, MULTI-02)GET /api/v1/tournaments/{id}— full tournament stateGET /api/v1/tournaments/{id}/state— WebSocket-compatible full state snapshotPOST /api/v1/tournaments/{id}/start— start tournamentPOST /api/v1/tournaments/{id}/pause— pausePOST /api/v1/tournaments/{id}/resume— resumePOST /api/v1/tournaments/{id}/cancel— cancelGET /api/v1/tournaments/{id}/activity— recent activity feed
5. WebSocket Integration:
- Update the WebSocket hub (Plan A) to send full tournament state on connect
- Messages are typed:
{type: "clock.tick", data: {...}},{type: "player.bust", data: {...}}, etc. - Client subscribes to a specific tournament by sending
{type: "subscribe", tournament_id: "..."} - Lobby clients subscribe without tournament_id to receive all tournament summaries
Verification:
- Create tournament from template with all config pre-filled
- Start tournament after minimum players met
- Multiple tournaments run simultaneously with independent state
- Tournament auto-closes when 1 player remains
- Lobby shows all active tournaments
- WebSocket sends full state on connect
-
Monte Carlo ICM(approximate, for 11+ players):CalculateICMMonteCarlo(stacks []int64, payouts []int64, iterations int) ([]int64, error):- Default iterations: 100,000 (converges to <0.1% error per 01-RESEARCH.md)
- Simulate tournament outcomes based on chip probabilities
- For each iteration: randomly determine finishing order weighted by chip stacks
- Average results across all iterations
- Return ICM values (int64 cents)
-
CalculateICM(stacks []int64, payouts []int64) ([]int64, error):- Dispatcher: use exact if <= 10 players, Monte Carlo otherwise
- Validate inputs: stacks and payouts must be non-empty, all positive
- ICM values must sum to total prize pool (validate before returning)
2. Chop/Deal Engine (internal/financial/chop.go):
-
ChopEnginestruct with financial engine, audit trail -
ProposeDeal(ctx, tournamentID string, dealType string, params DealParams) (*DealProposal, error):- Deal types:
- ICM: TD inputs chip stacks → system calculates ICM values → shows proposed payouts
- Chip Chop: Divide pool proportionally by chip count (simpler than ICM)
- Even Chop: Equal split among all remaining players
- Custom: TD enters specific amounts per player
- Partial Chop: Split some money, keep remainder + points in play (CONTEXT.md: "split some money, play on for remaining + league points")
- DealParams:
type DealParams struct { PlayerStacks map[string]int64 // playerID -> chip count (for ICM/chip chop) CustomAmounts map[string]int64 // playerID -> custom payout (for custom) PartialPool int64 // Amount to split (for partial chop) RemainingPool int64 // Amount left in play (for partial chop) } - Returns proposal for all remaining players with calculated payouts:
type DealProposal struct { ID string TournamentID string DealType string Payouts []DealPayout // {PlayerID, PlayerName, Amount, ChipStack, ICMValue} TotalAmount int64 // Must equal prize pool (or partial pool for partial chop) IsPartial bool RemainingPool int64 // If partial, what's still in play CreatedAt int64 } - Validate: sum of proposed payouts == total pool (or partial pool)
- Does NOT apply yet — returns proposal for TD approval
- Deal types:
-
ConfirmDeal(ctx, tournamentID, proposalID string) error:- Apply all payouts as transactions
- If full chop: set all players' status to "deal", assign finishing positions
- If partial chop: apply partial payouts, tournament continues with remaining pool
- Prize money and league positions are independent (CONTEXT.md): positions for league points are based on chip counts at deal time (or play on for partial chop)
- Record audit entry with full deal details
- If full chop: end tournament (set status "completed")
- Broadcast
-
CancelDeal(ctx, tournamentID, proposalID string) error
3. API Routes (add to tournaments.go):
POST /api/v1/tournaments/{id}/deal/propose— body:{"type": "icm", "stacks": {"player1": 50000, "player2": 30000}}GET /api/v1/tournaments/{id}/deal/proposals— list proposalsPOST /api/v1/tournaments/{id}/deal/proposals/{proposalId}/confirm— confirm dealPOST /api/v1/tournaments/{id}/deal/proposals/{proposalId}/cancel— cancel
4. Tests:
-
internal/financial/icm_test.go:- Test ICM exact with 2 players: known expected values
- Test ICM exact with 3 players: verify against known ICM tables
- Test ICM exact with 5 players: verify sum equals prize pool
- Test ICM Monte Carlo with 15 players: verify result is within 1% of expected (statistical test)
- Test ICM with equal stacks: all players should get approximately equal ICM values
- Test ICM edge case: one player has 99% of chips
- Test performance: ICM for 10 players completes in < 1 second
- Test performance: Monte Carlo ICM for 20 players completes in < 2 seconds
-
internal/financial/chop_test.go:- Test chip chop: payouts proportional to chip stacks
- Test even chop: all players get equal amounts
- Test custom split: arbitrary amounts summing to pool
- Test partial chop: partial payouts + remaining pool for continuation
- Test deal sets finishing positions correctly
- Test full chop ends tournament
-
internal/tournament/tournament_test.go:- Test create from template pre-fills all config
- Test start requires minimum players
- Test auto-close when 1 player remains
- Test multiple tournaments run independently
- Test tournament state aggregation includes all components
-
internal/tournament/integration_test.go— End-to-end tournament lifecycle test:- Create tournament from template (standard 10-player, PKO)
- Register 10 players with buy-ins (verify prize pool, rake, seat assignments)
- Start tournament, advance clock through 3 levels
- Process 2 rebuys, 1 add-on (verify prize pool update, transaction log)
- Bust 5 players with bounty transfers (verify rankings, hitman chain, balance check)
- Undo 1 bust → verify re-ranking of all subsequent positions
- Run table balancing (verify suggestion → accept → move)
- Break a table (verify even redistribution)
- Propose and confirm ICM deal with remaining 5 players
- Final assertions:
- Sum of all payouts == prize pool (int64, zero deviation)
- All 10 players have correct finishing positions
- Audit trail contains every state-changing action
- All transactions are accounted for (buyin + rebuy + addon = contributions, payouts + rake = disbursements)
- Tournament status is "completed"
Verification:
- ICM calculation produces correct values for known test cases
- All chop types produce valid payouts summing to prize pool
- Partial chop allows tournament to continue
- Full deal ends the tournament and assigns positions
- Multiple tournaments run simultaneously without interference
- Tournament lobby shows all active tournaments
Verification Criteria
- Tournament creation from template pre-fills all configuration
- Tournament start requires minimum players met
- Tournament auto-closes when one player remains
- Multiple simultaneous tournaments with independent clocks, financials, and players
- Tournament lobby shows all active tournaments (MULTI-02)
- ICM calculator works for 2-20+ players (exact for <=10, Monte Carlo for 11+)
- All chop types (ICM, chip-chop, even-chop, custom, partial) work correctly
- Prize money and league positions are independent in deal scenarios
- Full tournament state sent on WebSocket connect
- Activity feed shows recent actions in human-readable form
Must-Haves (Goal-Backward)
- Template-first tournament creation with local copy semantics
- Tournament auto-closes when one player remains
- Multiple simultaneous tournaments with fully independent state
- Tournament lobby view for multi-tournament overview
- ICM calculator (exact <=10, Monte Carlo 11+) produces correct values
- Chop/deal support (ICM, chip-chop, even-chop, custom, partial)
- Prize money and league positions independent
- Full tournament state available on WebSocket connect