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>
12 KiB
Plan G: Player Management
wave: 4 depends_on: [01-PLAN-C, 01-PLAN-E, 01-PLAN-F] files_modified:
- internal/player/player.go
- internal/player/ranking.go
- internal/player/qrcode.go
- internal/player/import.go
- internal/server/routes/players.go
- internal/player/ranking_test.go
- internal/player/player_test.go autonomous: true requirements: [PLYR-02, PLYR-03, PLYR-04, PLYR-05, PLYR-06, PLYR-07]
Goal
The player database supports search with typeahead (FTS5), merge duplicates, and CSV import. The buy-in and bust-out flows are the core transaction paths. Rankings are derived from the ordered bust-out list (never stored independently). Undo on any player action triggers full re-ranking. Per-player tracking captures all stats. QR codes are generated per player for future self-check-in.
Context
- Player database is persistent on Leaf (PLYR-01) — already in schema (Plan B)
- FTS5 virtual table for typeahead — already created in Plan B
- Financial engine for buy-in/rebuy/addon processing — Plan F
- Audit trail + undo — Plan C
- Auto-seating — Plan H (seating). Buy-in flow calls seating after financial processing
- Rankings are derived, not stored — from 01-RESEARCH.md Pitfall 6: "Rankings should be derived from the ordered bust-out list, not stored as independent values"
- See 01-RESEARCH.md Pitfall 6 (Undo Re-Rank)
User Decisions (from CONTEXT.md)
- Buy-in flow: search/select player → auto-seat suggests optimal seat → TD can override → confirm → receipt
- Bust-out flow: tap Bust → pick table → pick seat → verify name → confirm → select hitman (mandatory in PKO, optional otherwise) → done
- Undo is critical — bust-outs, rebuys, add-ons, buy-ins can all be undone with full re-ranking
- Bust-out flow must be as few taps as possible — TD is under time pressure
Tasks
**1. Player Service** (`internal/player/player.go`): - `PlayerService` struct with db, audit trail-
CreatePlayer(ctx, player Player) (*Player, error):- Generate UUID
- Insert into players table
- FTS5 auto-syncs via trigger (Plan B)
- Record audit entry
- Return player
-
GetPlayer(ctx, id string) (*Player, error) -
UpdatePlayer(ctx, player Player) error:- Update fields, record audit entry
- FTS5 auto-syncs via trigger
-
SearchPlayers(ctx, query string, limit int) ([]Player, error)— PLYR-02:- Use FTS5:
SELECT p.* FROM players p JOIN players_fts f ON p.rowid = f.rowid WHERE players_fts MATCH ? ORDER BY rank LIMIT ? - Support prefix matching for typeahead: append
*to query terms - Return results ordered by relevance (FTS5 rank)
- If query is empty, return most recently active players (for quick access)
- Use FTS5:
-
ListPlayers(ctx, limit, offset int) ([]Player, error)— paginated list -
MergePlayers(ctx, keepID, mergeID string) error— PLYR-02:- Merge player
mergeIDintokeepID:- Move all tournament_players records from mergeID to keepID
- Move all transactions from mergeID to keepID
- Merge any non-empty fields from mergeID into keepID (name takes keepID, fill in blanks)
- Delete mergeID player record
- Record audit entry with full merge details
- This is destructive — require admin role
- Merge player
-
ImportFromCSV(ctx, reader io.Reader) (ImportResult, error)— PLYR-02:- Parse CSV with headers: name, nickname, email, phone, notes (minimum: name required)
- For each row:
- Check for potential duplicates (FTS5 search on name)
- If exact name match exists, flag as duplicate (don't auto-merge)
- Otherwise create new player
- Return ImportResult: created count, duplicate count, error count, duplicate details
- Require admin role
2. Tournament Player Operations:
-
RegisterPlayer(ctx, tournamentID, playerID string) (*TournamentPlayer, error):- Create tournament_players record with status "registered"
- Check tournament isn't full (max_players)
- Return tournament player record
-
GetTournamentPlayers(ctx, tournamentID string) ([]TournamentPlayerDetail, error):- Join tournament_players with players table
- Include computed fields: total investment, net result, action count
- Sort by status (active first), then by name
-
GetTournamentPlayer(ctx, tournamentID, playerID string) (*TournamentPlayerDetail, error):- Full player detail within tournament context (PLYR-07):
- Current chips, seat (table + position), playing time
- Rebuys count, add-ons count, re-entries count
- Bounties collected, bounty value (PKO)
- Prize amount, points awarded
- Net take (prize - total investment)
- Full action history (from audit trail + transactions)
- Full player detail within tournament context (PLYR-07):
-
BustPlayer(ctx, tournamentID, playerID string, hitmanPlayerID *string) error— PLYR-05:- Validate player is active
- Set status to "busted", bust_out_at to now
- Calculate bust_out_order (count of currently busted players + 1, from the end)
- If PKO: hitman is mandatory, process bounty transfer (call financial engine)
- If not PKO: hitman is optional (for tracking who eliminated whom)
- Set hitman_player_id
- Clear seat assignment (remove from table)
- Record audit entry with previous state (for undo)
- Trigger balance check (notify if tables are unbalanced — seating engine, Plan H)
- Broadcast player bust event
- Check if tournament should auto-close (1 player remaining → tournament.end)
-
UndoBust(ctx, tournamentID, playerID string) error— PLYR-06:- Restore player to active status
- Clear bust_out_at, bust_out_order, finishing_position, hitman_player_id
- If PKO: reverse bounty transfer
- Re-seat player (needs a seat — auto-assign or put back in original seat from audit trail)
- Trigger full re-ranking
- Record audit entry
- Broadcast
3. QR Code Generation (internal/player/qrcode.go) — PLYR-03:
GenerateQRCode(ctx, playerID string) ([]byte, error):- Generate QR code encoding player UUID
- Return PNG image bytes
- QR code URL format:
felt://player/{uuid}(for future PWA self-check-in)
- API endpoint:
GET /api/v1/players/{id}/qrcode— returns PNG image
Verification:
- Player CRUD works via API
- FTS5 typeahead search returns results with prefix matching
- CSV import creates players and flags duplicates
- Merge combines two player records correctly
- QR code generates valid PNG image
-
RecalculateAllRankings(ctx, tournamentID string) error:- Called after any undo operation
- Recalculates ALL bust_out_order values from the bust_out_at timestamps
- Updates finishing_position for all busted players
- This ensures consistency even after undoing busts in the middle of the sequence
- Broadcast updated rankings
-
GetRankings(ctx, tournamentID string) ([]PlayerRanking, error):- Returns current rankings for display:
type PlayerRanking struct { Position int // Current ranking position PlayerID string PlayerName string Status string // active, busted, deal ChipCount int64 BustOutTime *int64 HitmanName *string BountiesCollected int Prize int64 Points int64 } - Active players sorted by chip count (if available), then alphabetically
- Busted players sorted by bust order (most recent first)
- Returns current rankings for display:
2. Player API Routes (internal/server/routes/players.go):
Player database (venue-level):
GET /api/v1/players— list all players (paginated)GET /api/v1/players/search?q=john— typeahead search (PLYR-02)POST /api/v1/players— create playerGET /api/v1/players/{id}— get playerPUT /api/v1/players/{id}— update playerGET /api/v1/players/{id}/qrcode— QR code PNG (PLYR-03)POST /api/v1/players/merge— body:{"keep_id": "...", "merge_id": "..."}(admin only)POST /api/v1/players/import— multipart CSV upload (admin only)
Tournament players:
GET /api/v1/tournaments/{id}/players— all players in tournament with statsGET /api/v1/tournaments/{id}/players/{playerId}— player detail with full tracking (PLYR-07)POST /api/v1/tournaments/{id}/players/{playerId}/buyin— buy-in flow (PLYR-04):- Calls financial engine (Plan F) for transaction
- Calls seating engine (Plan H) for auto-seat
- Returns: transaction, seat assignment, receipt
POST /api/v1/tournaments/{id}/players/{playerId}/bust— bust-out flow (PLYR-05):- Body:
{"hitman_player_id": "..."}(required for PKO, optional otherwise) - Calls financial engine for bounty transfer
- Calls ranking engine for re-ranking
- Calls seating engine for balance check
- Returns: updated rankings
- Body:
POST /api/v1/tournaments/{id}/players/{playerId}/rebuy— rebuy (delegates to financial engine)POST /api/v1/tournaments/{id}/players/{playerId}/addon— add-on (delegates to financial engine)POST /api/v1/tournaments/{id}/players/{playerId}/reentry— re-entry (delegates to financial engine + seating)POST /api/v1/tournaments/{id}/players/{playerId}/undo-bust— undo bust (PLYR-06)POST /api/v1/tournaments/{id}/transactions/{txId}/undo— undo any transaction (PLYR-06)
Rankings:
GET /api/v1/tournaments/{id}/rankings— current rankings
All mutation endpoints record audit entries and broadcast via WebSocket.
3. Tests (internal/player/ranking_test.go):
- Test rankings derived correctly from bust order
- Test undo bust triggers re-ranking of all subsequent positions
- Test undo early bust (not the most recent) re-ranks correctly
- Test re-entry doesn't count as new entry for ranking purposes
- Test deal players get manually assigned positions
- Test auto-close when 1 player remains
- Test concurrent busts (order preserved by timestamp)
Verification:
- Buy-in flow creates player entry, transaction, and seat assignment
- Bust-out flow busts player, processes bounty, re-ranks, checks balance
- Undo bust restores player and re-ranks all subsequent busts
- Rankings are always consistent with bust order
- Player detail shows complete tracking data (PLYR-07)
- Search returns results with typeahead behavior
Verification Criteria
- Player search with typeahead returns results via FTS5 prefix matching
- Duplicate merge combines records correctly (admin only)
- CSV import creates players and flags duplicates
- QR code generates valid PNG with player UUID
- Buy-in flow: search → financial transaction → auto-seat → receipt
- Bust-out flow: select → hitman → bounty → rank → balance check
- Undo bust restores player with full re-ranking of all positions
- Undo buy-in removes player from tournament
- Per-player tracking shows all stats (PLYR-07)
- Rankings are always derived from bust-out list, never stored independently
- Tournament auto-closes when one player remains
Must-Haves (Goal-Backward)
- Typeahead search on player names using FTS5
- Buy-in flow produces transaction + seat assignment + receipt
- Bust-out flow with hitman selection and bounty transfer (PKO)
- Undo capability for bust-out, rebuy, add-on, buy-in with full re-ranking
- Rankings derived from ordered bust-out list (not stored independently)
- Per-player tracking: chips, time, seat, moves, rebuys, add-ons, bounties, prize, points, net, history
- QR code generation per player