- 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>
208 lines
5.9 KiB
Go
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")
|
|
}
|
|
}
|