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) } } }