Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- .gitignore +69 -0
- squid_game.py +12 -3
- squid_game_core.py +107 -84
- 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
|
|
|
|
|
|
|
| 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 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 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
|
| 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 |
-
|
| 33 |
-
|
| 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 |
-
|
| 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 |
-
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 87 |
|
|
|
|
| 88 |
if m == 0:
|
| 89 |
-
|
|
|
|
| 90 |
elif m == 1:
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
else:
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
@lru_cache(None)
|
| 100 |
def get_expected_value(distribution, remaining, tier_map_tuple):
|
| 101 |
"""
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 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 |
-
|
| 120 |
-
return tuple(
|
| 121 |
-
|
| 122 |
-
# 2. Otherwise, average over all possible ways to distribute the remaining squids
|
| 123 |
-
n = len(distribution)
|
| 124 |
|
| 125 |
-
|
| 126 |
-
|
| 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 |
-
|
| 142 |
-
sub_ev = get_expected_value(new_dist, remaining - 1, tier_map_tuple)
|
| 143 |
for i in range(n):
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
# Probability = 1/n for each winner
|
| 147 |
for i in range(n):
|
| 148 |
-
|
|
|
|
| 149 |
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 8 |
-
from
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
@pytest.fixture
|
| 11 |
-
def
|
| 12 |
"""
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
3-100:2 => 3 or more => total=k*2
|
| 18 |
"""
|
|
|
|
|
|
|
| 19 |
return (
|
| 20 |
(0,0,0.0),
|
| 21 |
-
(1,
|
| 22 |
-
(
|
| 23 |
-
(3,100,2.0)
|
| 24 |
)
|
| 25 |
|
| 26 |
-
def
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
def
|
| 30 |
"""
|
| 31 |
-
|
| 32 |
-
|
| 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 = (
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 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
|
| 47 |
-
"""
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 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
|
| 64 |
-
"""
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
assert
|
| 74 |
-
assert
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
-
def
|
| 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 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 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 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
|
|
|
| 95 |
"""
|
| 96 |
dist = (0,1,1)
|
| 97 |
-
X =
|
| 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 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
r = X - sum(dist)
|
| 113 |
get_expected_value.cache_clear()
|
| 114 |
-
ev = get_expected_value(dist, r,
|
| 115 |
-
assert
|
| 116 |
-
|
| 117 |
-
|
|
|
|
| 118 |
|
| 119 |
-
|
| 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
|
| 133 |
"""
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 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 =
|
| 157 |
-
r = X - sum(dist)
|
| 158 |
-
|
| 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,
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
assert nearly_equal(ev[
|
| 167 |
-
assert nearly_equal(ev[
|
| 168 |
-
|
|
|
|
|
|
| 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)
|