felt/internal/seating/breaktable_test.go
Mikkel Georgsen 2d3cb0ac9e feat(01-08): implement balance engine, break table, and seating API routes
- TDA-compliant balance engine with live-adaptive suggestions
- Break table distributes players evenly across remaining tables
- Stale suggestion detection and invalidation on state changes
- Full REST API for tables, seating, balancing, blueprints, hand-for-hand
- 15 tests covering balance, break table, auto-seat, and dealer button

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 04:24:52 +01:00

208 lines
5.9 KiB
Go

package seating
import (
"context"
"fmt"
"testing"
)
func TestBreakTable_DistributesEvenly(t *testing.T) {
db := testDB(t)
ctx := context.Background()
tournamentID := "t1"
seedTournament(t, db, tournamentID)
tbl1 := seedTable(t, db, tournamentID, "Table 1", 9)
tbl2 := seedTable(t, db, tournamentID, "Table 2", 9)
tbl3 := seedTable(t, db, tournamentID, "Table 3", 9)
// Table 1: 6 players (will be broken)
// Table 2: 5 players
// Table 3: 5 players
for i := 1; i <= 6; i++ {
pid := fmt.Sprintf("p1_%d", i)
seedPlayer(t, db, pid, fmt.Sprintf("Player 1-%d", i))
seatPlayer(t, db, tournamentID, pid, tbl1, i)
}
for i := 1; i <= 5; i++ {
pid := fmt.Sprintf("p2_%d", i)
seedPlayer(t, db, pid, fmt.Sprintf("Player 2-%d", i))
seatPlayer(t, db, tournamentID, pid, tbl2, i)
}
for i := 1; i <= 5; i++ {
pid := fmt.Sprintf("p3_%d", i)
seedPlayer(t, db, pid, fmt.Sprintf("Player 3-%d", i))
seatPlayer(t, db, tournamentID, pid, tbl3, i)
}
svc := NewBreakTableService(db, nil, nil)
result, err := svc.BreakTable(ctx, tournamentID, tbl1)
if err != nil {
t.Fatalf("break table: %v", err)
}
if result.BrokenTableID != tbl1 {
t.Errorf("expected broken table id %d, got %d", tbl1, result.BrokenTableID)
}
if len(result.Moves) != 6 {
t.Fatalf("expected 6 moves, got %d", len(result.Moves))
}
// Count how many went to each table: should be 3 each (5+3=8, 5+3=8)
movesToTable := make(map[int]int)
for _, m := range result.Moves {
movesToTable[m.ToTableID]++
}
for tid, count := range movesToTable {
if count < 2 || count > 4 {
t.Errorf("table %d got %d players, expected roughly even distribution", tid, count)
}
}
// Verify table 1 is deactivated
var isActive int
db.QueryRow(`SELECT is_active FROM tables WHERE id = ?`, tbl1).Scan(&isActive)
if isActive != 0 {
t.Error("broken table should be deactivated")
}
// Verify all players are seated at other tables
var unseatedCount int
db.QueryRow(
`SELECT COUNT(*) FROM tournament_players
WHERE tournament_id = ? AND status = 'active' AND seat_table_id IS NULL`,
tournamentID,
).Scan(&unseatedCount)
if unseatedCount != 0 {
t.Errorf("expected 0 unseated players, got %d", unseatedCount)
}
}
func TestBreakTable_OddPlayerCount(t *testing.T) {
db := testDB(t)
ctx := context.Background()
tournamentID := "t1"
seedTournament(t, db, tournamentID)
tbl1 := seedTable(t, db, tournamentID, "Table 1", 9)
tbl2 := seedTable(t, db, tournamentID, "Table 2", 9)
tbl3 := seedTable(t, db, tournamentID, "Table 3", 9)
// Table 1: 5 players (will be broken)
// Table 2: 4 players
// Table 3: 3 players
for i := 1; i <= 5; i++ {
pid := fmt.Sprintf("p1_%d", i)
seedPlayer(t, db, pid, fmt.Sprintf("Player 1-%d", i))
seatPlayer(t, db, tournamentID, pid, tbl1, i)
}
for i := 1; i <= 4; i++ {
pid := fmt.Sprintf("p2_%d", i)
seedPlayer(t, db, pid, fmt.Sprintf("Player 2-%d", i))
seatPlayer(t, db, tournamentID, pid, tbl2, i)
}
for i := 1; i <= 3; i++ {
pid := fmt.Sprintf("p3_%d", i)
seedPlayer(t, db, pid, fmt.Sprintf("Player 3-%d", i))
seatPlayer(t, db, tournamentID, pid, tbl3, i)
}
svc := NewBreakTableService(db, nil, nil)
result, err := svc.BreakTable(ctx, tournamentID, tbl1)
if err != nil {
t.Fatalf("break table: %v", err)
}
if len(result.Moves) != 5 {
t.Fatalf("expected 5 moves, got %d", len(result.Moves))
}
// Count final player counts at tbl2 and tbl3
var count2, count3 int
db.QueryRow(`SELECT COUNT(*) FROM tournament_players WHERE tournament_id = ? AND seat_table_id = ? AND status = 'active'`,
tournamentID, tbl2).Scan(&count2)
db.QueryRow(`SELECT COUNT(*) FROM tournament_players WHERE tournament_id = ? AND seat_table_id = ? AND status = 'active'`,
tournamentID, tbl3).Scan(&count3)
// 12 total players across 2 tables -> 6 each
if count2+count3 != 12 {
t.Errorf("expected 12 total, got %d", count2+count3)
}
// Difference should be at most 1
diff := count2 - count3
if diff < 0 {
diff = -diff
}
if diff > 1 {
t.Errorf("expected tables balanced to within 1, got diff=%d (table2=%d, table3=%d)", diff, count2, count3)
}
}
func TestBreakTable_DeactivatesTable(t *testing.T) {
db := testDB(t)
ctx := context.Background()
tournamentID := "t1"
seedTournament(t, db, tournamentID)
tbl1 := seedTable(t, db, tournamentID, "Table 1", 9)
tbl2 := seedTable(t, db, tournamentID, "Table 2", 9)
// 2 players at each table
seedPlayer(t, db, "p1", "P1")
seatPlayer(t, db, tournamentID, "p1", tbl1, 1)
seedPlayer(t, db, "p2", "P2")
seatPlayer(t, db, tournamentID, "p2", tbl1, 2)
seedPlayer(t, db, "p3", "P3")
seatPlayer(t, db, tournamentID, "p3", tbl2, 1)
seedPlayer(t, db, "p4", "P4")
seatPlayer(t, db, tournamentID, "p4", tbl2, 2)
svc := NewBreakTableService(db, nil, nil)
_, err := svc.BreakTable(ctx, tournamentID, tbl1)
if err != nil {
t.Fatalf("break table: %v", err)
}
var isActive int
db.QueryRow(`SELECT is_active FROM tables WHERE id = ?`, tbl1).Scan(&isActive)
if isActive != 0 {
t.Error("broken table should be deactivated (is_active = 0)")
}
}
func TestBreakTable_InactiveTableReturnsError(t *testing.T) {
db := testDB(t)
ctx := context.Background()
tournamentID := "t1"
seedTournament(t, db, tournamentID)
tbl := seedTable(t, db, tournamentID, "Table 1", 9)
_ = seedTable(t, db, tournamentID, "Table 2", 9)
// Deactivate the table first
db.Exec(`UPDATE tables SET is_active = 0 WHERE id = ?`, tbl)
svc := NewBreakTableService(db, nil, nil)
_, err := svc.BreakTable(ctx, tournamentID, tbl)
if err == nil {
t.Fatal("expected error for inactive table")
}
}
func TestBreakTable_NoDestinationTablesReturnsError(t *testing.T) {
db := testDB(t)
ctx := context.Background()
tournamentID := "t1"
seedTournament(t, db, tournamentID)
tbl := seedTable(t, db, tournamentID, "Table 1", 9)
seedPlayer(t, db, "p1", "P1")
seatPlayer(t, db, tournamentID, "p1", tbl, 1)
svc := NewBreakTableService(db, nil, nil)
_, err := svc.BreakTable(ctx, tournamentID, tbl)
if err == nil {
t.Fatal("expected error when no destination tables")
}
}