|
|
|
|
|
|
|
|
import pytest |
|
|
from math import isclose |
|
|
from functools import lru_cache |
|
|
|
|
|
|
|
|
from squid_game_core import ( |
|
|
parse_tier_map, |
|
|
tierValue, |
|
|
is_terminal, |
|
|
compute_final_payout, |
|
|
get_expected_value, |
|
|
next_squid_gain_for_nonzero, |
|
|
hypothetical_next_round_gain |
|
|
) |
|
|
|
|
|
def nearly_equal(a, b, tol=1e-9): |
|
|
return isclose(a, b, abs_tol=tol) |
|
|
|
|
|
@pytest.fixture |
|
|
def tier_map_example(): |
|
|
""" |
|
|
We'll define a bracket: |
|
|
1-4:1 => if you have 1..4 squids => total = k * 1 |
|
|
5-6:3 => if you have 5..6 squids => total = k * 3 |
|
|
0 => always 0 |
|
|
""" |
|
|
|
|
|
|
|
|
return ( |
|
|
(0,0,0.0), |
|
|
(1,4,1.0), |
|
|
(5,6,3.0) |
|
|
) |
|
|
|
|
|
def test_multiple_losers_pay_individually(tier_map_example): |
|
|
""" |
|
|
Scenario: 3 players => final distribution=(0,0,4). |
|
|
According to bracket (1-4:1 => 4=>4*1=4). |
|
|
- 2 losers => each pays 4 => each has payoff=-4 |
|
|
- 1 winner => receives 2 * 4=8. |
|
|
""" |
|
|
dist = (0,0,4) |
|
|
payoffs = compute_final_payout(dist, tier_map_example) |
|
|
|
|
|
assert nearly_equal(payoffs[0], -4) |
|
|
assert nearly_equal(payoffs[1], -4) |
|
|
assert nearly_equal(payoffs[2], 8) |
|
|
|
|
|
def test_single_loser(tier_map_example): |
|
|
""" |
|
|
Scenario: 2 players => final distribution=(1,0). |
|
|
=> 1 zero => that zero pays sum_of_winners= tierValue(1)=1 |
|
|
=> payoff=(1, -1) because: |
|
|
- Winner keeps their tierValue (1) |
|
|
- Loser pays that amount (-1) |
|
|
""" |
|
|
dist = (1,0) |
|
|
payoffs = compute_final_payout(dist, tier_map_example) |
|
|
assert nearly_equal(payoffs[0], 1) |
|
|
assert nearly_equal(payoffs[1], -1) |
|
|
|
|
|
def test_no_losers(tier_map_example): |
|
|
""" |
|
|
Scenario: 2 players => final distribution=(2,3). |
|
|
=> no zero => no payment => payoff=(0,0). |
|
|
""" |
|
|
dist = (2,3) |
|
|
payoffs = compute_final_payout(dist, tier_map_example) |
|
|
|
|
|
|
|
|
|
|
|
assert nearly_equal(payoffs[0], 0) |
|
|
assert nearly_equal(payoffs[1], 0) |
|
|
|
|
|
def test_next_squid_gain_for_nonzero(tier_map_example): |
|
|
""" |
|
|
distribution=(4,0,2) => |
|
|
- Player0=4 => tierValue(4)=4 => tierValue(5)=15 => gain=11 |
|
|
(since 5 squids => bracket => 5*3=15) |
|
|
- Player1=0 => skip |
|
|
- Player2=2 => tierValue(2)=2 => tierValue(3)=3 => gain=1 |
|
|
""" |
|
|
dist = (4,0,2) |
|
|
gains = next_squid_gain_for_nonzero(dist, tier_map_example) |
|
|
assert 0 in gains |
|
|
assert gains[0] == 11 |
|
|
assert 2 in gains |
|
|
assert gains[2] == 1 |
|
|
assert 1 not in gains |
|
|
|
|
|
def test_ev_with_leftover_multiple_losers(tier_map_example): |
|
|
""" |
|
|
We'll test a DP scenario: |
|
|
N=3, X=4 total squids |
|
|
Current distribution=(0,1,1) |
|
|
=> sum=2 => leftover=2 => not terminal. |
|
|
|
|
|
Let's see possible final states: |
|
|
- They could keep awarding 2 more squids in 2 rounds. |
|
|
- It's possible we end with multiple zeros if the 2 additional squids both go to the same non-zero player, |
|
|
or exactly one zero, etc. |
|
|
|
|
|
We'll just check the computed EV matches a hand-run or at least we confirm no errors. |
|
|
""" |
|
|
dist = (0,1,1) |
|
|
X = 4 |
|
|
r = X - sum(dist) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from squid_game import get_expected_value |
|
|
get_expected_value.cache_clear() |
|
|
ev = get_expected_value(dist, r, tier_map_example) |
|
|
assert len(ev) == 3 |
|
|
|
|
|
total_ev = sum(ev) |
|
|
assert nearly_equal(total_ev, 0.0), f"Sum of EVs is not near 0, got {total_ev}" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_ev_multiple_losers_specific(tier_map_example): |
|
|
""" |
|
|
A more direct test for multiple losers via DP: |
|
|
N=3, X=2, distribution=(0,0,2) |
|
|
=> sum=2 => leftover=0 => terminal => multiple zero => each zero pays tierValue(2)=2 => payoff=(-2,-2,4) |
|
|
|
|
|
Then we check that the DP logic returns the same final payoff if is_terminal is triggered. |
|
|
""" |
|
|
dist = (0,0,2) |
|
|
X = 2 |
|
|
r = X - sum(dist) |
|
|
from squid_game import get_expected_value |
|
|
get_expected_value.cache_clear() |
|
|
ev = get_expected_value(dist, r, tier_map_example) |
|
|
|
|
|
|
|
|
assert nearly_equal(ev[0], -2) |
|
|
assert nearly_equal(ev[1], -2) |
|
|
assert nearly_equal(ev[2], 4) |
|
|
|
|
|
assert nearly_equal(sum(ev), 0.0) |
|
|
|
|
|
def test_hypothetical_next_round_gain_with_penalty(): |
|
|
""" |
|
|
Test scenario: 3 players with distribution=(0,0,2) |
|
|
Expected values would be (-2,-2,4) as shown in test_ev_multiple_losers_specific |
|
|
So penalty should be 2 (abs of -2) |
|
|
|
|
|
For zero-squid players (0,1): |
|
|
- Getting 1 squid = tierValue(1) = 1 |
|
|
- Avoiding penalty share = 2/2 = 1 (penalty/zero_count) |
|
|
- Total gain = 2 |
|
|
|
|
|
For player with 2 squids: |
|
|
- Going from 2 to 3 = tierValue(3) - tierValue(2) = 3 - 2 = 1 |
|
|
""" |
|
|
tier_map = ( |
|
|
(0,0,0.0), |
|
|
(1,4,1.0), |
|
|
(5,6,3.0) |
|
|
) |
|
|
dist = (0,0,2) |
|
|
penalty = 2 |
|
|
|
|
|
gains = hypothetical_next_round_gain(dist, tier_map, penalty) |
|
|
|
|
|
|
|
|
assert nearly_equal(gains[0], 2.0), f"Expected gain 2.0 for player 1, got {gains[0]}" |
|
|
assert nearly_equal(gains[1], 2.0), f"Expected gain 2.0 for player 2, got {gains[1]}" |
|
|
|
|
|
|
|
|
assert nearly_equal(gains[2], 1.0), f"Expected gain 1.0 for player 3, got {gains[2]}" |
|
|
|
|
|
def test_hypothetical_next_round_gain_no_zeros(): |
|
|
""" |
|
|
Test scenario: 2 players with distribution=(1,2) |
|
|
No zero-squid players, so penalty doesn't matter |
|
|
|
|
|
Player 1: going from 1 to 2 = tierValue(2) - tierValue(1) = 2 - 1 = 1 |
|
|
Player 2: going from 2 to 3 = tierValue(3) - tierValue(2) = 3 - 2 = 1 |
|
|
""" |
|
|
tier_map = ( |
|
|
(0,0,0.0), |
|
|
(1,4,1.0), |
|
|
(5,6,3.0) |
|
|
) |
|
|
dist = (1,2) |
|
|
penalty = 0 |
|
|
|
|
|
gains = hypothetical_next_round_gain(dist, tier_map, penalty) |
|
|
|
|
|
assert nearly_equal(gains[0], 1.0), f"Expected gain 1.0 for player 1, got {gains[0]}" |
|
|
assert nearly_equal(gains[1], 1.0), f"Expected gain 1.0 for player 2, got {gains[1]}" |
|
|
|