sidnvy commited on
Commit
231a0fb
·
verified ·
1 Parent(s): 6976150

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. .gitignore +69 -0
  2. squid_game.py +12 -3
  3. squid_game_core.py +107 -84
  4. test_squid_game.py +112 -136
.gitignore ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual Environment
24
+ venv/
25
+ env/
26
+ ENV/
27
+ .env
28
+ .venv
29
+ env.bak/
30
+ venv.bak/
31
+
32
+ # IDE - PyCharm
33
+ .idea/
34
+ *.iml
35
+ *.iws
36
+ .idea_modules/
37
+
38
+ # IDE - VSCode
39
+ .vscode/
40
+ *.code-workspace
41
+ .history/
42
+
43
+ # IDE - Jupyter Notebook
44
+ .ipynb_checkpoints
45
+ *.ipynb
46
+
47
+ # Testing
48
+ .pytest_cache/
49
+ .coverage
50
+ htmlcov/
51
+ .tox/
52
+ .nox/
53
+ coverage.xml
54
+ *.cover
55
+ *.py,cover
56
+ .hypothesis/
57
+
58
+ # Misc
59
+ .DS_Store
60
+ *.log
61
+ *.swp
62
+ *.swo
63
+ .env.local
64
+ .env.development.local
65
+ .env.test.local
66
+ .env.production.local
67
+
68
+
69
+ .gradio/*
squid_game.py CHANGED
@@ -1,5 +1,5 @@
1
  import gradio as gr
2
- from squid_game_core import parse_tier_map, get_expected_value
3
  from typing import List, Tuple
4
 
5
  def validate_distribution(dist_str: str) -> Tuple[bool, str, List[int]]:
@@ -70,6 +70,13 @@ def solve_game(distribution: str, total_squids: int, tier_map_str: str) -> str:
70
  for i, ev in enumerate(expected_values):
71
  result += f"Player {i+1}: {ev:.3f}\n"
72
 
 
 
 
 
 
 
 
73
  # Add tier map interpretation
74
  result += "\nTier Map Interpretation:\n"
75
  for low, high, mult in tier_map:
@@ -139,8 +146,10 @@ Edit these values to match your game rules."""
139
  Game Rules:
140
  1. Players take turns collecting squids randomly
141
  2. Game ends when either:
142
- - Exactly one player has 0 squids (they pay the total value of others' squids), OR
143
- - No squids remain to distribute (if multiple players have 0, they share the payment; if no one has 0, no payment)
 
 
144
  """,
145
  examples=[
146
  # Common scenarios with descriptive labels
 
1
  import gradio as gr
2
+ from squid_game_core import parse_tier_map, get_expected_value, next_squid_gain_for_nonzero, hypothetical_next_round_gain
3
  from typing import List, Tuple
4
 
5
  def validate_distribution(dist_str: str) -> Tuple[bool, str, List[int]]:
 
70
  for i, ev in enumerate(expected_values):
71
  result += f"Player {i+1}: {ev:.3f}\n"
72
 
73
+ # Add next squid gains information
74
+ gains = next_squid_gain_for_nonzero(dist, tier_map)
75
+ if gains:
76
+ result += "\nPotential Gains from Next Squid:\n"
77
+ for player_idx, gain in gains.items():
78
+ result += f"Player {player_idx+1}: +{gain:.1f} \n"
79
+
80
  # Add tier map interpretation
81
  result += "\nTier Map Interpretation:\n"
82
  for low, high, mult in tier_map:
 
146
  Game Rules:
147
  1. Players take turns collecting squids randomly
148
  2. Game ends when either:
149
+ - Exactly one player has 0 squids (they pay the total value of others' squids, winners keep their squids), OR
150
+ - No squids remain to distribute:
151
+ * If multiple players have 0, each pays the total value and winners get multiplied payouts
152
+ * If no one has 0, no payment occurs
153
  """,
154
  examples=[
155
  # Common scenarios with descriptive labels
squid_game_core.py CHANGED
@@ -4,11 +4,11 @@ from functools import lru_cache
4
 
5
  def parse_tier_map(tier_str: str):
6
  """
7
- Parses lines like "2-4:2" => for players with 2..4 squids,
8
- each squid is worth multiplier=2. So if a player has 2 squids,
9
- total = 2 * 2 = 4. If a player has 4 squids, total = 4 * 2 = 8.
10
-
11
- Returns a list of (min_k, max_k, per_squid_mult).
12
  """
13
  lines = tier_str.strip().splitlines()
14
  tier_map = []
@@ -16,42 +16,33 @@ def parse_tier_map(tier_str: str):
16
  range_part, mult_part = line.split(":")
17
  per_squid_mult = float(mult_part.strip())
18
 
19
- if '-' in range_part:
20
- low_str, high_str = range_part.split('-')
21
  low, high = int(low_str), int(high_str)
22
  else:
23
  low = high = int(range_part.strip())
24
 
25
  tier_map.append((low, high, per_squid_mult))
26
-
27
  tier_map.sort(key=lambda x: x[0])
28
  return tier_map
29
 
30
  def tierValue(k: int, tier_map) -> float:
31
  """
32
- Given k squids, find which tier bracket (low, high, mult) it falls into.
33
- Returns: k * per_squid_mult for that bracket.
34
- If k is larger than any bracket's high value, use the last bracket's multiplier.
35
  """
36
  if k <= 0:
37
  return 0.0
38
-
39
- # Find matching bracket
40
  for (low, high, mult) in tier_map:
41
  if low <= k <= high:
42
  return k * mult
43
-
44
- # If k is larger than any bracket, use last bracket's multiplier
45
  if tier_map and k > tier_map[-1][1]:
46
  return k * tier_map[-1][2]
47
-
48
- return 0.0
49
 
50
  def is_terminal(distribution, remaining) -> bool:
51
  """
52
- Checks if the game should end:
53
- 1) Exactly one player has 0 squids, OR
54
- 2) No squids remain (remaining == 0).
55
  """
56
  zero_count = sum(1 for x in distribution if x == 0)
57
  if zero_count == 1:
@@ -62,89 +53,121 @@ def is_terminal(distribution, remaining) -> bool:
62
 
63
  def compute_final_payout(distribution, tier_map):
64
  """
65
- Computes final payouts when game ends.
66
 
67
- Rules:
68
- - If exactly one player has 0 squids: they pay sum of all other players' tier values
69
- - If multiple players have 0 squids: they share the total cost equally
70
- - If no players have 0 squids: everyone gets 0 payout
71
-
72
- Args:
73
- distribution: tuple of squids per player
74
- tier_map: list of (low, high, multiplier) brackets
75
-
76
- Returns:
77
- List of payouts (positive = receive, negative = pay)
78
  """
79
  n = len(distribution)
80
  zero_indices = [i for i, x in enumerate(distribution) if x == 0]
81
- m = len(zero_indices)
82
-
83
- # Calculate total value from non-zero players
84
- total_cost = sum(tierValue(x, tier_map) for x in distribution if x > 0)
85
 
86
- payouts = [0.0] * n
 
 
87
 
 
88
  if m == 0:
89
- return payouts # No zeros = no payments
 
90
  elif m == 1:
91
- payouts[zero_indices[0]] = -total_cost # Single zero pays all
 
 
 
 
 
 
 
92
  else:
93
- cost_per_player = total_cost / m
94
- for i in zero_indices:
95
- payouts[i] = -cost_per_player # Multiple zeros share cost
96
-
97
- return payouts
 
 
 
 
 
 
98
 
99
  @lru_cache(None)
100
  def get_expected_value(distribution, remaining, tier_map_tuple):
101
  """
102
- Returns an N-tuple of expected payoffs for each player in the partial state.
103
-
104
- Args:
105
- distribution: tuple of ints representing squids per player, e.g. (1,0,2)
106
- remaining: how many squids left to deal
107
- tier_map_tuple: tuple of (low, high, multiplier) brackets
108
-
109
- Special cases:
110
- - For 2 players with 2 remaining squids, uses exact probabilities:
111
- - (2,0) and (0,2) each have 25% chance
112
- - (1,1) has 50% chance
113
- Otherwise averages over all possible next-squid distributions.
114
  """
115
- distribution = tuple(distribution)
116
-
117
- # 1. Terminal?
118
  if is_terminal(distribution, remaining):
119
- final_payout = compute_final_payout(distribution, tier_map_tuple)
120
- return tuple(final_payout)
121
-
122
- # 2. Otherwise, average over all possible ways to distribute the remaining squids
123
- n = len(distribution)
124
 
125
- # Special case: 2 players, 2 remaining squids
126
- if remaining == 2 and n == 2:
127
- payout_2_0 = compute_final_payout((distribution[0]+2, distribution[1]), tier_map_tuple)
128
- payout_0_2 = compute_final_payout((distribution[0], distribution[1]+2), tier_map_tuple)
129
- payout_1_1 = compute_final_payout((distribution[0]+1, distribution[1]+1), tier_map_tuple)
130
-
131
- return tuple(
132
- 0.25 * payout_2_0[i] + 0.25 * payout_0_2[i] + 0.5 * payout_1_1[i]
133
- for i in range(n)
134
- )
135
-
136
- # Original logic for other cases
137
- accumulated_ev = [0.0]*n
138
  for winner in range(n):
139
  new_dist = list(distribution)
140
  new_dist[winner] += 1
141
- new_dist = tuple(new_dist)
142
- sub_ev = get_expected_value(new_dist, remaining - 1, tier_map_tuple)
143
  for i in range(n):
144
- accumulated_ev[i] += sub_ev[i]
145
-
146
- # Probability = 1/n for each winner
147
  for i in range(n):
148
- accumulated_ev[i] /= n
 
149
 
150
- return tuple(accumulated_ev)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  def parse_tier_map(tier_str: str):
6
  """
7
+ Example tier_str:
8
+ \"1-4:1\n5-6:3\"
9
+ means:
10
+ if a player has 1..4 squids => each worth x1 => total = k*1
11
+ if a player has 5..6 squids => each worth x3 => total = k*3
12
  """
13
  lines = tier_str.strip().splitlines()
14
  tier_map = []
 
16
  range_part, mult_part = line.split(":")
17
  per_squid_mult = float(mult_part.strip())
18
 
19
+ if "-" in range_part:
20
+ low_str, high_str = range_part.split("-")
21
  low, high = int(low_str), int(high_str)
22
  else:
23
  low = high = int(range_part.strip())
24
 
25
  tier_map.append((low, high, per_squid_mult))
 
26
  tier_map.sort(key=lambda x: x[0])
27
  return tier_map
28
 
29
  def tierValue(k: int, tier_map) -> float:
30
  """
31
+ For k squids, find bracket => return k * bracket_multiplier.
32
+ If k <= 0 => 0.
 
33
  """
34
  if k <= 0:
35
  return 0.0
 
 
36
  for (low, high, mult) in tier_map:
37
  if low <= k <= high:
38
  return k * mult
 
 
39
  if tier_map and k > tier_map[-1][1]:
40
  return k * tier_map[-1][2]
41
+ return 0.0 # fallback if no bracket matches
 
42
 
43
  def is_terminal(distribution, remaining) -> bool:
44
  """
45
+ Game ends if exactly one zero-squid player or no squids remain.
 
 
46
  """
47
  zero_count = sum(1 for x in distribution if x == 0)
48
  if zero_count == 1:
 
53
 
54
  def compute_final_payout(distribution, tier_map):
55
  """
56
+ distribution: e.g. (0,0,4)
57
 
58
+ NEW RULES:
59
+ - If exactly 1 zero-squid => that one pays sum_of_winners_tier
60
+ - If multiple zeros => each zero-squid pays sum_of_winners_tier individually
61
+ => each winner gets (number_of_zero_squids * winner_tier_value).
 
 
 
 
 
 
 
62
  """
63
  n = len(distribution)
64
  zero_indices = [i for i, x in enumerate(distribution) if x == 0]
65
+ m = len(zero_indices) # number of zero-squid players
66
+ winner_indices = [i for i, x in enumerate(distribution) if x > 0]
 
 
67
 
68
+ # sum of each winner's bracketed total
69
+ winner_values = [tierValue(distribution[w], tier_map) for w in winner_indices]
70
+ sum_winner_values = sum(winner_values)
71
 
72
+ payoffs = [0.0]*n
73
  if m == 0:
74
+ # No zeros => no payment => everyone gets 0
75
+ return payoffs
76
  elif m == 1:
77
+ # Exactly one zero-squid => that player pays the entire sum
78
+ z = zero_indices[0]
79
+ payoffs[z] = -sum_winner_values
80
+ # each winner receives exactly their own tierValue
81
+ # so we set payoffs[winner] = winner_values[i]
82
+ for i, w in enumerate(winner_indices):
83
+ payoffs[w] = winner_values[i]
84
+ return payoffs
85
  else:
86
+ # multiple zeros => each zero pays sum_winner_values
87
+ # => each winner receives m * (their own tierValue)
88
+ for z in zero_indices:
89
+ payoffs[z] = -sum_winner_values
90
+ for i, w in enumerate(winner_indices):
91
+ payoffs[w] = m * winner_values[i]
92
+ return payoffs
93
+
94
+ def format_state(distribution, remaining):
95
+ """Format a game state for display"""
96
+ return f"({','.join(map(str, distribution))}, {remaining})"
97
 
98
  @lru_cache(None)
99
  def get_expected_value(distribution, remaining, tier_map_tuple):
100
  """
101
+ Memoized DP: returns an N-tuple of payoffs from state=(distribution, remaining).
102
+ distribution: tuple of ints
103
+ remaining: int (squids left)
104
+ tier_map_tuple: bracket info (like ( (1,4,1.0), (5,6,3.0) ))
 
 
 
 
 
 
 
 
105
  """
 
 
 
106
  if is_terminal(distribution, remaining):
107
+ final_pay = compute_final_payout(distribution, tier_map_tuple)
108
+ return tuple(final_pay)
 
 
 
109
 
110
+ n = len(distribution)
111
+ accumulated = [0.0]*n
 
 
 
 
 
 
 
 
 
 
 
112
  for winner in range(n):
113
  new_dist = list(distribution)
114
  new_dist[winner] += 1
115
+ sub_ev = get_expected_value(tuple(new_dist), remaining-1, tier_map_tuple)
 
116
  for i in range(n):
117
+ accumulated[i] += sub_ev[i]
118
+ # average
 
119
  for i in range(n):
120
+ accumulated[i] /= n
121
+ return tuple(accumulated)
122
 
123
+ def next_squid_gain_for_nonzero(distribution, tier_map):
124
+ """
125
+ Returns a dict: {player_index: gain in tierValue if that player goes from s_i to s_i+1}.
126
+ Only for players who currently hold > 0 squids.
127
+
128
+ Example:
129
+ if distribution=(4,0,2) with "1-4:1,5-6:3":
130
+ - Player0 has 4 => tierValue(4)=4 => tierValue(5)=15 => gain=11
131
+ - Player1 has 0 => skip
132
+ - Player2 has 2 => tierValue(2)=2*1=2 => tierValue(3)=3 => gain=1
133
+ """
134
+ results = {}
135
+ for i, s in enumerate(distribution):
136
+ curr_val = tierValue(s, tier_map)
137
+ next_val = tierValue(s+1, tier_map)
138
+ results[i] = next_val - curr_val
139
+ return results
140
+
141
+ def hypothetical_next_round_gain(distribution, tier_map, penalty=24):
142
+ """
143
+ Returns a list (or dict) of length N, indicating how much "extra" reward
144
+ each player would get if they, individually, are the *sole* winner next round.
145
+
146
+ - If s[i] > 0:
147
+ gain[i] = tierValue(s[i]+1) - tierValue(s[i])
148
+ - If s[i] == 0:
149
+ gain[i] = tierValue(1) + (1/zero_count)*penalty
150
+ (assuming your simplified logic that 1/zero_count is
151
+ the chance of "dodging" the final cost of 24 by winning a squid)
152
+ """
153
+ n = len(distribution)
154
+ gains = [0.0]*n
155
+
156
+ zero_count = sum(1 for x in distribution if x==0)
157
+
158
+ for i, s_i in enumerate(distribution):
159
+ if s_i > 0:
160
+ current_val = tierValue(s_i, tier_map)
161
+ next_val = tierValue(s_i + 1, tier_map)
162
+ gains[i] = next_val - current_val
163
+ else:
164
+ # s_i=0
165
+ val_if_win = tierValue(1, tier_map) # from 0 => 1
166
+ # plus the "avoid paying 24" portion *if you assume
167
+ # it is equally likely you'd be the one stuck paying if you remain at 0
168
+ if zero_count > 0:
169
+ gains[i] = val_if_win + (penalty / zero_count)
170
+ else:
171
+ # edge case: if zero_count=0? not possible if s_i=0.
172
+ gains[i] = val_if_win
173
+ return gains
test_squid_game.py CHANGED
@@ -4,165 +4,141 @@ import pytest
4
  from math import isclose
5
  from functools import lru_cache
6
 
7
- # Import from our main solver
8
- from squid_game import get_expected_value
 
 
 
 
 
 
 
 
 
 
9
 
10
  @pytest.fixture
11
- def corrected_tier_map():
12
  """
13
- A simple bracket-based tier for test:
14
- 0-0:0 => 0 squids => total=0
15
- 1-1:1 => 1 squid => total=1
16
- 2-2:2 => 2 squids => total=4
17
- 3-100:2 => 3 or more => total=k*2
18
  """
 
 
19
  return (
20
  (0,0,0.0),
21
- (1,1,1.0),
22
- (2,2,2.0),
23
- (3,100,2.0)
24
  )
25
 
26
- def nearly_equal(a, b, tol=1e-9):
27
- return isclose(a, b, abs_tol=tol)
 
 
 
 
 
 
 
 
 
 
 
28
 
29
- def test_caseA(corrected_tier_map):
30
  """
31
- N=2, X=1, dist=(0,0)
32
- -> EV=(-0.5, -0.5)
33
- Explanation:
34
- - 1 squid left, 50% P1 gets => payoff=(0, -1)
35
- - 50% P2 gets => payoff=(-1, 0)
36
- => each player => -0.5
37
  """
38
- dist = (0,0)
39
- X = 1
40
- r = X - sum(dist)
41
- get_expected_value.cache_clear()
42
- ev = get_expected_value(dist, r, tier_map_tuple=corrected_tier_map)
43
- assert nearly_equal(ev[0], -0.5)
44
- assert nearly_equal(ev[1], -0.5)
45
 
46
- def test_caseB(corrected_tier_map):
47
- """
48
- N=2, X=2, dist=(0,0)
49
- Possible final distributions:
50
- (2,0) => zero pays tierValue(2)=4 => payoff=(0,-4)
51
- (0,2) => payoff=(-4,0)
52
- (1,1) => payoff=(0,0)
53
- Prob(2,0)=Prob(0,2)=0.25, Prob(1,1)=0.5 => EV=(-1, -1)
54
- """
55
- dist = (0,0)
56
- X = 2
57
- r = X - sum(dist)
58
- get_expected_value.cache_clear()
59
- ev = get_expected_value(dist, r, tier_map_tuple=corrected_tier_map)
60
- assert nearly_equal(ev[0], -1.0)
61
- assert nearly_equal(ev[1], -1.0)
62
 
63
- def test_caseC(corrected_tier_map):
64
- """
65
- N=2, X=2, dist=(1,0)
66
- => Exactly one zero => immediate terminal => zero pays tierValue(1)=1 => payoff=(0, -1).
67
- """
68
- dist = (1,0)
69
- X = 2
70
- r = X - sum(dist)
71
- get_expected_value.cache_clear()
72
- ev = get_expected_value(dist, r, tier_map_tuple=corrected_tier_map)
73
- assert nearly_equal(ev[0], 0.0)
74
- assert nearly_equal(ev[1], -1.0)
 
 
 
75
 
76
- def test_caseD(corrected_tier_map):
77
- """
78
- N=3, X=3, dist=(0,0,3)
79
- => sum=3 => no squids remain => multiple zeros share cost= tierValue(3)=3*2=6 => each pays 3 => payoff=(-3, -3, 0)
80
  """
81
- dist = (0,0,3)
82
- X = 3
83
- r = X - sum(dist)
84
- get_expected_value.cache_clear()
85
- ev = get_expected_value(dist, r, tier_map_tuple=corrected_tier_map)
86
- assert nearly_equal(ev[0], -3.0)
87
- assert nearly_equal(ev[1], -3.0)
88
- assert nearly_equal(ev[2], 0.0)
89
 
90
- def test_caseE(corrected_tier_map):
91
- """
92
- N=3, X=2, dist=(0,1,1)
93
- => sum=2 => no squids remain => exactly one zero => cost= tierValue(1)+ tierValue(1)=1+1=2
94
- => payoff=(-2, 0, 0)
 
95
  """
96
  dist = (0,1,1)
97
- X = 2
98
- r = X - sum(dist)
99
- get_expected_value.cache_clear()
100
- ev = get_expected_value(dist, r, tier_map_tuple=corrected_tier_map)
101
- assert nearly_equal(ev[0], -2.0)
102
- assert nearly_equal(ev[1], 0.0)
103
- assert nearly_equal(ev[2], 0.0)
104
 
105
- def test_caseF(corrected_tier_map):
106
- """
107
- N=3, X=2, dist=(1,0,1)
108
- => sum=2 => no squids => exactly one zero => cost=1+1=2 => payoff=(0, -2, 0)
109
- """
110
- dist = (1,0,1)
111
- X = 2
112
- r = X - sum(dist)
113
  get_expected_value.cache_clear()
114
- ev = get_expected_value(dist, r, tier_map_tuple=corrected_tier_map)
115
- assert nearly_equal(ev[0], 0.0)
116
- assert nearly_equal(ev[1], -2.0)
117
- assert nearly_equal(ev[2], 0.0)
 
118
 
119
- def test_caseG(corrected_tier_map):
120
- """
121
- N=2, X=3, dist=(1,1)
122
- => sum=2 => 1 leftover => final => (2,1) or (1,2) => no zero => payoff=(0,0) => EV=(0,0)
123
- """
124
- dist = (1,1)
125
- X = 3
126
- r = X - sum(dist)
127
- get_expected_value.cache_clear()
128
- ev = get_expected_value(dist, r, tier_map_tuple=corrected_tier_map)
129
- assert nearly_equal(ev[0], 0.0)
130
- assert nearly_equal(ev[1], 0.0)
131
 
132
- def test_caseH(corrected_tier_map):
133
  """
134
- Demonstrates a scenario with a currently NON-zero player
135
- AND the game is NOT terminal. Specifically:
136
-
137
- N=3, X=3, dist=(0,0,2)
138
- => sum=2 => leftover=1 => zero_count=2 => not terminal (must continue).
139
-
140
- Next round (1 leftover) goes to:
141
- - P1 => final (1,0,2) => sum=3 => leftover=0 => EXACTLY ONE zero => that zero pays:
142
- cost = tierValue(1)+ tierValue(2)= 1 + 4= 5 => payoff=(0, -5, 0).
143
- - P2 => final (0,1,2) => sum=3 => leftover=0 => EXACTLY ONE zero => that zero pays:
144
- cost = tierValue(1)+ tierValue(2)= 5 => payoff=(-5, 0, 0).
145
- - P3 => final (0,0,3) => sum=3 => leftover=0 => MULTIPLE zeros => cost= tierValue(3)=3*2=6 => each zero pays -3 => payoff=(-3, -3, 0).
146
- Each outcome has probability 1/3.
147
-
148
- So final EV:
149
- Player1 => 1/3(0) + 1/3(-5) + 1/3(-3) = -8/3 = approx -2.6667
150
- Player2 => 1/3(-5) + 1/3(0) + 1/3(-3) = -8/3 = approx -2.6667
151
- Player3 => 1/3(0) + 1/3(0) + 1/3(0) = 0
152
-
153
- This confirms the code gives EVs for both zero and non-zero players.
154
  """
155
  dist = (0,0,2)
156
- X = 3
157
- r = X - sum(dist)
158
- # Zero_count=2 => not terminal => must see who wins the final squid
159
- # We can compute expected payoffs by hand => see docstring above.
160
-
161
  get_expected_value.cache_clear()
162
- ev = get_expected_value(dist, r, tier_map_tuple=corrected_tier_map)
163
- assert len(ev) == 3
164
-
165
- # Compare to the exact values: ( -2.666..., -2.666..., 0 )
166
- assert nearly_equal(ev[0], -8.0/3.0, 1e-7)
167
- assert nearly_equal(ev[1], -8.0/3.0, 1e-7)
168
- assert nearly_equal(ev[2], 0.0)
 
 
4
  from math import isclose
5
  from functools import lru_cache
6
 
7
+ # Import from our main module
8
+ from squid_game_core import (
9
+ parse_tier_map,
10
+ tierValue,
11
+ is_terminal,
12
+ compute_final_payout,
13
+ get_expected_value,
14
+ next_squid_gain_for_nonzero
15
+ )
16
+
17
+ def nearly_equal(a, b, tol=1e-9):
18
+ return isclose(a, b, abs_tol=tol)
19
 
20
  @pytest.fixture
21
+ def tier_map_example():
22
  """
23
+ We'll define a bracket:
24
+ 1-4:1 => if you have 1..4 squids => total = k * 1
25
+ 5-6:3 => if you have 5..6 squids => total = k * 3
26
+ 0 => always 0
 
27
  """
28
+ # We'll store it as a tuple for use in DP
29
+ # parse_tier_map would do the same, but let's define it directly
30
  return (
31
  (0,0,0.0),
32
+ (1,4,1.0),
33
+ (5,6,3.0)
 
34
  )
35
 
36
+ def test_multiple_losers_pay_individually(tier_map_example):
37
+ """
38
+ Scenario: 3 players => final distribution=(0,0,4).
39
+ According to bracket (1-4:1 => 4=>4*1=4).
40
+ - 2 losers => each pays 4 => each has payoff=-4
41
+ - 1 winner => receives 2 * 4=8.
42
+ """
43
+ dist = (0,0,4)
44
+ payoffs = compute_final_payout(dist, tier_map_example)
45
+ # payoffs => [ -4, -4, 8 ]
46
+ assert nearly_equal(payoffs[0], -4)
47
+ assert nearly_equal(payoffs[1], -4)
48
+ assert nearly_equal(payoffs[2], 8)
49
 
50
+ def test_single_loser(tier_map_example):
51
  """
52
+ Scenario: 2 players => final distribution=(1,0).
53
+ => 1 zero => that zero pays sum_of_winners= tierValue(1)=1 => payoff=(0, -1).
 
 
 
 
54
  """
55
+ dist = (1,0)
56
+ payoffs = compute_final_payout(dist, tier_map_example)
57
+ assert nearly_equal(payoffs[0], 0)
58
+ assert nearly_equal(payoffs[1], -1)
 
 
 
59
 
60
+ def test_no_losers(tier_map_example):
61
+ """
62
+ Scenario: 2 players => final distribution=(2,3).
63
+ => no zero => no payment => payoff=(0,0).
64
+ """
65
+ dist = (2,3)
66
+ payoffs = compute_final_payout(dist, tier_map_example)
67
+ # 2 => bracket(1..4 => *1)=>2
68
+ # 3 => bracket(1..4 => *1)=>3
69
+ # but since no zero => payoff=(0,0).
70
+ assert nearly_equal(payoffs[0], 0)
71
+ assert nearly_equal(payoffs[1], 0)
 
 
 
 
72
 
73
+ def test_next_squid_gain_for_nonzero(tier_map_example):
74
+ """
75
+ distribution=(4,0,2) =>
76
+ - Player0=4 => tierValue(4)=4 => tierValue(5)=15 => gain=11
77
+ (since 5 squids => bracket => 5*3=15)
78
+ - Player1=0 => skip
79
+ - Player2=2 => tierValue(2)=2 => tierValue(3)=3 => gain=1
80
+ """
81
+ dist = (4,0,2)
82
+ gains = next_squid_gain_for_nonzero(dist, tier_map_example)
83
+ assert 0 in gains
84
+ assert gains[0] == 11 # 15-4
85
+ assert 2 in gains
86
+ assert gains[2] == 1 # 3-2
87
+ assert 1 not in gains # because that player has 0
88
 
89
+ def test_ev_with_leftover_multiple_losers(tier_map_example):
 
 
 
90
  """
91
+ We'll test a DP scenario:
92
+ N=3, X=4 total squids
93
+ Current distribution=(0,1,1)
94
+ => sum=2 => leftover=2 => not terminal.
 
 
 
 
95
 
96
+ Let's see possible final states:
97
+ - They could keep awarding 2 more squids in 2 rounds.
98
+ - It's possible we end with multiple zeros if the 2 additional squids both go to the same non-zero player,
99
+ or exactly one zero, etc.
100
+
101
+ We'll just check the computed EV matches a hand-run or at least we confirm no errors.
102
  """
103
  dist = (0,1,1)
104
+ X = 4
105
+ r = X - sum(dist) # 4-2=2
 
 
 
 
 
106
 
107
+ # We'll do a quick partial analysis by enumerating the 2 leftover squids:
108
+ # Round 1 => three possibilities: P0, P1, P2
109
+ # But let's just rely on the solver to give us a final result,
110
+ # and we'll assert that we get a numeric 3-tuple and
111
+ # the sum of payoffs is near 0 (since it's a zero-sum game).
112
+
113
+ from squid_game import get_expected_value
 
114
  get_expected_value.cache_clear()
115
+ ev = get_expected_value(dist, r, tier_map_example)
116
+ assert len(ev) == 3
117
+ # Because it's zero-sum, the sum of EVs should be very close to 0:
118
+ total_ev = sum(ev)
119
+ assert nearly_equal(total_ev, 0.0), f"Sum of EVs is not near 0, got {total_ev}"
120
 
121
+ # We won't do a full hand enumeration here,
122
+ # but we at least confirm the DP runs and yields a plausible sum=0 result.
 
 
 
 
 
 
 
 
 
 
123
 
124
+ def test_ev_multiple_losers_specific(tier_map_example):
125
  """
126
+ A more direct test for multiple losers via DP:
127
+ N=3, X=2, distribution=(0,0,2)
128
+ => sum=2 => leftover=0 => terminal => multiple zero => each zero pays tierValue(2)=2 => payoff=(-2,-2,4)
129
+
130
+ Then we check that the DP logic returns the same final payoff if is_terminal is triggered.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  """
132
  dist = (0,0,2)
133
+ X = 2
134
+ r = X - sum(dist) # leftover=0 => terminal immediately
135
+ from squid_game import get_expected_value
 
 
136
  get_expected_value.cache_clear()
137
+ ev = get_expected_value(dist, r, tier_map_example)
138
+ # With bracket => 2 => 2*1=2 => each zero pays 2 => 2 losers => winner gets 2*2=4
139
+ # payoff=( -2, -2, 4 )
140
+ assert nearly_equal(ev[0], -2)
141
+ assert nearly_equal(ev[1], -2)
142
+ assert nearly_equal(ev[2], 4)
143
+ # sum = 0
144
+ assert nearly_equal(sum(ev), 0.0)