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>
334 lines
16 KiB
Markdown
334 lines
16 KiB
Markdown
# 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
|
|
|
|
<task id="I1" title="Implement tournament lifecycle and multi-tournament management">
|
|
**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
|
|
</task>
|
|
|
|
<task id="I2" title="Implement chop/deal support with ICM calculator">
|
|
**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
|
|
</task>
|
|
|
|
## 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
|