Skip to content

Commit bf58fa2

Browse files
hankhsu1996claude
andcommitted
Add late surrender support and rename soft table
- Add late_surrender config option with EV calculation (-0.5) - Generate 1,152 strategy files (576 sur + 576 nosur) - Add surrender (R) action with purple color in legend - Rename "Soft Totals" to "Soft Hands" for accuracy - Run MC simulation for all surrender configs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9e17762 commit bf58fa2

13 files changed

Lines changed: 4034 additions & 1117 deletions

File tree

docs/usage.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ Columns: Dealer's upcard (2-A)
114114

115115
A "hard" hand has no ace counted as 11, or would bust if the ace were 11.
116116

117-
### Soft Totals
117+
### Soft Hands
118118

119119
Rows: Player's soft hand (A2-A9)
120120
Columns: Dealer's upcard (2-A)

scripts/generate_strategies.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ def config_to_filename(config: GameConfig) -> str:
1919
msh = f"sp{config.max_split_hands}" # sp2, sp3, sp4
2020
peek = "peek" if config.dealer_peeks else "nopeek"
2121
bj = "32" if config.blackjack_pays == 1.5 else "65"
22-
return f"{decks}-{s17}-{das}-{rsa}-{msh}-{peek}-{bj}.json"
22+
sur = "sur" if config.late_surrender else "nosur"
23+
return f"{decks}-{s17}-{das}-{rsa}-{msh}-{peek}-{bj}-{sur}.json"
2324

2425

2526
def get_table_data(tables: StrategyTables) -> dict:
@@ -53,7 +54,7 @@ def generate_for_base_config(base_params: tuple) -> list[dict]:
5354
Strategy tables are the same for both payouts and max_split_hands,
5455
only house edge differs.
5556
"""
56-
decks, h17, das, rsa, peek = base_params
57+
decks, h17, das, rsa, peek, surrender = base_params
5758
output_dir = Path("web/public/strategies")
5859

5960
# Create base config for strategy tables (these don't affect strategy)
@@ -65,6 +66,7 @@ def generate_for_base_config(base_params: tuple) -> list[dict]:
6566
max_split_hands=4, # Doesn't affect strategy tables
6667
dealer_peeks=peek,
6768
blackjack_pays=1.5, # Doesn't affect strategy tables
69+
late_surrender=surrender,
6870
)
6971

7072
# Generate strategy tables once (same for all payouts and max_split_hands)
@@ -84,6 +86,7 @@ def generate_for_base_config(base_params: tuple) -> list[dict]:
8486
max_split_hands=max_hands,
8587
dealer_peeks=peek,
8688
blackjack_pays=bj_pays,
89+
late_surrender=surrender,
8790
)
8891

8992
filename = config_to_filename(config)
@@ -97,6 +100,7 @@ def generate_for_base_config(base_params: tuple) -> list[dict]:
97100
"max_split_hands": config.max_split_hands,
98101
"dealer_peeks": config.dealer_peeks,
99102
"blackjack_pays": config.blackjack_pays,
103+
"late_surrender": config.late_surrender,
100104
"description": str(config),
101105
},
102106
**table_data,
@@ -132,6 +136,7 @@ def main():
132136
bool_options, # double_after_split
133137
bool_options, # resplit_aces
134138
bool_options, # dealer_peeks
139+
bool_options, # late_surrender
135140
)
136141
)
137142

src/blackjack/config.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@ class GameConfig:
1515
max_split_hands: Maximum hands from splitting (2-4, default 4)
1616
blackjack_pays: Payout ratio for blackjack (1.5 for 3:2, 1.2 for 6:5)
1717
dealer_peeks: True if dealer peeks for blackjack with 10/A showing
18-
19-
Note:
20-
Surrender is not modeled.
18+
late_surrender: True if late surrender is allowed (after dealer peeks)
2119
"""
2220

2321
num_decks: int = 6
@@ -27,6 +25,7 @@ class GameConfig:
2725
max_split_hands: int = 4 # Can resplit up to 4 hands
2826
blackjack_pays: float = 1.5 # 3:2
2927
dealer_peeks: bool = True
28+
late_surrender: bool = False
3029

3130
@classmethod
3231
def default(cls) -> "GameConfig":

src/blackjack/evaluator.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@ def get_all_evs(
194194
if can_split and len(player_cards) == 2 and player_cards[0] == player_cards[1]:
195195
evs["split"] = self.ev_split(player_cards[0], dealer_upcard)
196196

197+
if self.config.late_surrender and len(player_cards) == 2:
198+
evs["surrender"] = -0.5
199+
197200
return evs
198201

199202
def _get_all_evs_composition(
@@ -233,6 +236,9 @@ def _get_all_evs_composition(
233236
player_cards[0], dealer_upcard, adj_probs, adj_dealer_outcomes, removed
234237
)
235238

239+
if self.config.late_surrender and len(player_cards) == 2:
240+
evs["surrender"] = -0.5
241+
236242
return evs
237243

238244
def _get_dealer_outcomes_adjusted(

src/blackjack/renderers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@ class StrategyData:
2121
soft_table: TableData
2222
pair_table: TableData
2323
legend: str = field(
24-
default="S=Stand, H=Hit, Dh=Double/Hit, Ds=Double/Stand, P=Split"
24+
default="S=Stand, H=Hit, Dh=Double/Hit, Ds=Double/Stand, P=Split, R=Surrender"
2525
)

src/blackjack/strategy.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
ACTION_DOUBLE_OR_STAND = "Ds" # Double if allowed, otherwise stand
1212
ACTION_SPLIT = "P"
1313
ACTION_SPLIT_OR_HIT = "Ph" # Split if DAS, otherwise hit
14+
ACTION_SURRENDER = "R" # Surrender (give up half bet)
1415

1516

1617
class BasicStrategy:
@@ -36,7 +37,7 @@ def get_action(
3637
can_double: Whether doubling is allowed
3738
3839
Returns:
39-
Action code (S, H, D, Dh, Ds, P)
40+
Action code (S, H, D, Dh, Ds, P, R)
4041
"""
4142
evs = self.ev_calc.get_all_evs(
4243
player_cards, dealer_upcard, can_split, can_double
@@ -58,6 +59,9 @@ def get_action(
5859
if best_action == "split":
5960
return ACTION_SPLIT
6061

62+
if best_action == "surrender":
63+
return ACTION_SURRENDER
64+
6165
if best_action == "stand":
6266
return ACTION_STAND
6367

src/blackjack/tables.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def get_strategy_data(self) -> StrategyData:
3838
rows=self._build_hard_rows(hard),
3939
),
4040
soft_table=TableData(
41-
title="Soft Totals",
41+
title="Soft Hands",
4242
headers=self.DEALER_HEADERS,
4343
rows=self._build_soft_rows(soft),
4444
),

0 commit comments

Comments
 (0)