# 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: ```go 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 } ``` - `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 - `BuildActivityFeed(ctx, tournamentID string, limit int) ([]ActivityEntry, error)`: - Convert recent audit entries into human-readable activity items: ```go 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 } ``` **3. Multi-Tournament Manager** (`internal/tournament/multi.go`) — MULTI-01: - `MultiManager` struct — 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 template - `GET /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 state - `GET /api/v1/tournaments/{id}/state` — WebSocket-compatible full state snapshot - `POST /api/v1/tournaments/{id}/start` — start tournament - `POST /api/v1/tournaments/{id}/pause` — pause - `POST /api/v1/tournaments/{id}/resume` — resume - `POST /api/v1/tournaments/{id}/cancel` — cancel - `GET /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 **1. ICM Calculator** (`internal/financial/icm.go`) — FIN-11: - `Malmuth-Harville Algorithm` (exact, for <= 10 players): - Calculate each player's equity in the prize pool based on chip stacks - Recursively calculate probability of each player finishing in each position - `CalculateICMExact(stacks []int64, payouts []int64) ([]int64, error)`: - Input: chip stacks (int64) and payout amounts (int64 cents) - Output: ICM value for each player (int64 cents) - Algorithm: for each player, calculate P(finish in each position) using chip-proportion recursion - Sum P(position) * payout(position) for all positions - `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`): - `ChopEngine` struct 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: ```go 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: ```go 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 - `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 proposals - `POST /api/v1/tournaments/{id}/deal/proposals/{proposalId}/confirm` — confirm deal - `POST /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 1. Tournament creation from template pre-fills all configuration 2. Tournament start requires minimum players met 3. Tournament auto-closes when one player remains 4. Multiple simultaneous tournaments with independent clocks, financials, and players 5. Tournament lobby shows all active tournaments (MULTI-02) 6. ICM calculator works for 2-20+ players (exact for <=10, Monte Carlo for 11+) 7. All chop types (ICM, chip-chop, even-chop, custom, partial) work correctly 8. Prize money and league positions are independent in deal scenarios 9. Full tournament state sent on WebSocket connect 10. 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