felt/internal/financial/icm_test.go
Mikkel Georgsen 295844983a test(01-09): add ICM, chop/deal, tournament lifecycle, and integration tests
- 12 ICM tests: exact/Monte Carlo, validation, performance, convergence
- 6 chop/deal tests: chip chop, even chop, custom, partial, positions, tournament end
- 9 tournament unit tests: template creation, overrides, start validation, auto-close, multi-tournament, state aggregation
- 4 integration tests: full lifecycle, deal workflow, cancel, pause/resume
- Fix integration test DB concurrency with file-based DB + WAL mode

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

386 lines
9.7 KiB
Go

package financial
import (
"math"
"os"
"testing"
"time"
)
func TestCalculateICMExact_TwoPlayers(t *testing.T) {
// Two players with known expected ICM values
// Player 1: 7000 chips, Player 2: 3000 chips
// Prize pool: 1st = 700, 2nd = 300 (cents)
stacks := []int64{7000, 3000}
payouts := []int64{70000, 30000} // 700.00 and 300.00
result, err := CalculateICMExact(stacks, payouts)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(result) != 2 {
t.Fatalf("expected 2 results, got %d", len(result))
}
// Player 1 (70% chips) should get more than average but less than proportional
// ICM gives chip leader less equity than pure chip proportion
// Expected: Player 1 ~ 61600-62000, Player 2 ~ 38000-38400
// (ICM redistributes compared to pure chip-chop)
totalPool := int64(100000) // 1000.00
sum := result[0] + result[1]
if sum != totalPool {
t.Errorf("sum of ICM values %d != total pool %d", sum, totalPool)
}
// Player 1 should get significantly more
if result[0] <= result[1] {
t.Errorf("player 1 (more chips) should have higher ICM: got %d vs %d", result[0], result[1])
}
// ICM gives chip leader less than pure chip proportion
chipChopP1 := totalPool * 7000 / 10000 // 70000
if result[0] >= chipChopP1 {
t.Errorf("ICM should give chip leader less than chip chop: got %d, chip chop would be %d", result[0], chipChopP1)
}
}
func TestCalculateICMExact_ThreePlayers(t *testing.T) {
// Three players with known stacks
stacks := []int64{5000, 3000, 2000}
payouts := []int64{50000, 30000, 20000} // 500, 300, 200
result, err := CalculateICMExact(stacks, payouts)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(result) != 3 {
t.Fatalf("expected 3 results, got %d", len(result))
}
// Verify sum equals prize pool
totalPool := int64(100000)
sum := result[0] + result[1] + result[2]
if sum != totalPool {
t.Errorf("sum of ICM values %d != total pool %d", sum, totalPool)
}
// Player ordering should match chip ordering
if result[0] <= result[1] || result[1] <= result[2] {
t.Errorf("ICM values should follow chip order: got %d, %d, %d", result[0], result[1], result[2])
}
}
func TestCalculateICMExact_FivePlayers(t *testing.T) {
// Five players
stacks := []int64{10000, 8000, 6000, 4000, 2000}
payouts := []int64{50000, 30000, 20000, 10000, 5000}
result, err := CalculateICMExact(stacks, payouts)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(result) != 5 {
t.Fatalf("expected 5 results, got %d", len(result))
}
// Verify sum equals prize pool
totalPool := int64(115000)
sum := int64(0)
for _, v := range result {
sum += v
}
if sum != totalPool {
t.Errorf("sum of ICM values %d != total pool %d", sum, totalPool)
}
// Values should be in descending order
for i := 1; i < len(result); i++ {
if result[i] > result[i-1] {
t.Errorf("ICM value at position %d (%d) > position %d (%d)", i, result[i], i-1, result[i-1])
}
}
}
func TestCalculateICMExact_EqualStacks(t *testing.T) {
// All players with equal stacks should get approximately equal ICM values
stacks := []int64{5000, 5000, 5000}
payouts := []int64{60000, 30000, 10000}
result, err := CalculateICMExact(stacks, payouts)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
totalPool := int64(100000)
expected := totalPool / 3
// With equal stacks, all players should get the same ICM value
for i, v := range result {
diff := v - expected
if diff < 0 {
diff = -diff
}
// Allow 1 cent tolerance for rounding
if diff > 1 {
t.Errorf("player %d ICM value %d differs from expected %d by %d", i, v, expected, diff)
}
}
// Sum must equal pool
sum := int64(0)
for _, v := range result {
sum += v
}
if sum != totalPool {
t.Errorf("sum %d != pool %d", sum, totalPool)
}
}
func TestCalculateICMExact_DominantStack(t *testing.T) {
// One player with 99% of chips
stacks := []int64{99000, 500, 500}
payouts := []int64{60000, 30000, 10000}
result, err := CalculateICMExact(stacks, payouts)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
totalPool := int64(100000)
sum := int64(0)
for _, v := range result {
sum += v
}
if sum != totalPool {
t.Errorf("sum %d != pool %d", sum, totalPool)
}
// Dominant stack should get close to 1st place prize but less than total
if result[0] < payouts[0]-5000 {
t.Errorf("dominant player ICM %d should be close to 1st prize %d", result[0], payouts[0])
}
// Small stacks should get at least some equity (ICM floor)
if result[1] <= 0 || result[2] <= 0 {
t.Errorf("small stack players should have positive ICM: %d, %d", result[1], result[2])
}
}
func TestCalculateICMMonteCarlo_Basic(t *testing.T) {
// 15 players - tests Monte Carlo path
stacks := make([]int64, 15)
totalChips := int64(0)
for i := range stacks {
stacks[i] = int64((i + 1) * 1000)
totalChips += stacks[i]
}
payouts := make([]int64, 5) // Only top 5 paid
totalPool := int64(150000)
payouts[0] = 60000
payouts[1] = 40000
payouts[2] = 25000
payouts[3] = 15000
payouts[4] = 10000
result, err := CalculateICMMonteCarlo(stacks, payouts, 100_000)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(result) != 15 {
t.Fatalf("expected 15 results, got %d", len(result))
}
// Verify sum equals prize pool
sum := int64(0)
for _, v := range result {
sum += v
}
if sum != totalPool {
t.Errorf("sum %d != pool %d", sum, totalPool)
}
// Player with most chips (index 14) should have highest equity
maxIdx := 0
for i := 1; i < len(result); i++ {
if result[i] > result[maxIdx] {
maxIdx = i
}
}
if maxIdx != 14 {
t.Errorf("expected player 14 (most chips) to have highest ICM, got player %d", maxIdx)
}
// Monte Carlo should be within ~1% of expected values
// Chip leader should get significantly more
if result[14] < result[0] {
t.Errorf("chip leader ICM %d should be more than short stack %d", result[14], result[0])
}
}
func TestCalculateICM_DispatcherExact(t *testing.T) {
// <=10 players should use exact
stacks := []int64{5000, 3000, 2000}
payouts := []int64{50000, 30000, 20000}
result, err := CalculateICM(stacks, payouts)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Same result as exact
exact, _ := CalculateICMExact(stacks, payouts)
for i := range result {
if result[i] != exact[i] {
t.Errorf("dispatcher result[%d]=%d != exact[%d]=%d", i, result[i], i, exact[i])
}
}
}
func TestCalculateICM_DispatcherMonteCarlo(t *testing.T) {
// 11 players should use Monte Carlo
stacks := make([]int64, 11)
for i := range stacks {
stacks[i] = 1000
}
payouts := []int64{50000, 30000, 20000}
result, err := CalculateICM(stacks, payouts)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(result) != 11 {
t.Fatalf("expected 11 results, got %d", len(result))
}
// Sum equals pool
sum := int64(0)
for _, v := range result {
sum += v
}
totalPool := int64(100000)
if sum != totalPool {
t.Errorf("sum %d != pool %d", sum, totalPool)
}
}
func TestCalculateICM_ValidationErrors(t *testing.T) {
// Empty stacks
_, err := CalculateICM(nil, []int64{100})
if err == nil {
t.Error("expected error for nil stacks")
}
// Empty payouts
_, err = CalculateICM([]int64{100}, nil)
if err == nil {
t.Error("expected error for nil payouts")
}
// Zero stack
_, err = CalculateICM([]int64{0}, []int64{100})
if err == nil {
t.Error("expected error for zero stack")
}
// Negative payout
_, err = CalculateICM([]int64{100}, []int64{-50})
if err == nil {
t.Error("expected error for negative payout")
}
}
func TestCalculateICMExact_Performance(t *testing.T) {
if os.Getenv("CI") == "1" {
t.Skip("skipping performance test in CI")
}
// ICM for 10 players should complete in < 1 second
stacks := make([]int64, 10)
for i := range stacks {
stacks[i] = int64((i + 1) * 1000)
}
payouts := make([]int64, 5)
payouts[0] = 50000
payouts[1] = 30000
payouts[2] = 15000
payouts[3] = 5000
payouts[4] = 2500
start := time.Now()
result, err := CalculateICMExact(stacks, payouts)
elapsed := time.Since(start)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(result) != 10 {
t.Fatalf("expected 10 results, got %d", len(result))
}
if elapsed > time.Second {
t.Errorf("ICM for 10 players took %v, expected < 1s", elapsed)
}
t.Logf("ICM for 10 players: %v", elapsed)
}
func TestCalculateICMMonteCarlo_Performance(t *testing.T) {
if os.Getenv("CI") == "1" {
t.Skip("skipping performance test in CI")
}
// Monte Carlo ICM for 20 players should complete in < 2 seconds
stacks := make([]int64, 20)
for i := range stacks {
stacks[i] = int64((i + 1) * 500)
}
payouts := make([]int64, 5)
payouts[0] = 50000
payouts[1] = 30000
payouts[2] = 15000
payouts[3] = 5000
payouts[4] = 2500
start := time.Now()
result, err := CalculateICMMonteCarlo(stacks, payouts, 100_000)
elapsed := time.Since(start)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(result) != 20 {
t.Fatalf("expected 20 results, got %d", len(result))
}
if elapsed > 2*time.Second {
t.Errorf("Monte Carlo ICM for 20 players took %v, expected < 2s", elapsed)
}
t.Logf("Monte Carlo ICM for 20 players: %v", elapsed)
}
func TestCalculateICMMonteCarlo_Convergence(t *testing.T) {
// With equal stacks and enough iterations, Monte Carlo should converge
// to roughly equal values for all players
stacks := []int64{5000, 5000, 5000, 5000, 5000}
payouts := []int64{50000, 30000, 20000}
result, err := CalculateICMMonteCarlo(stacks, payouts, 100_000)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := float64(100000) / 5.0
for i, v := range result {
deviation := math.Abs(float64(v)-expected) / expected * 100
// With equal stacks, deviation should be < 1%
if deviation > 2.0 {
t.Errorf("player %d ICM value %d deviates %.1f%% from expected %.0f",
i, v, deviation, expected)
}
}
}