- 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>
386 lines
9.7 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|