Add final results modal, active rules display, and UI improvements
- Add big final results modal at game end with rankings and share button - Add active rules bar showing enabled variants during gameplay - Increase spacing between player cards and opponents row - Add Wolfpack bonus rule (2 pairs of Jacks = -5 pts) - Change joker options to radio buttons (None/Standard/Lucky Swing/Eagle-Eye) - Update Eagle-Eye jokers: +2 pts unpaired, -4 pts paired - Add card flip animation on discard pile - Redesign waiting room layout with side-by-side columns - Style card backs with red Bee-style diamond crosshatch pattern - Compact standings panel to show top 4 per category - Various CSS polish and responsive improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -116,7 +116,6 @@ Our implementation supports these optional rule variations:
|
||||
|--------|--------|
|
||||
| `lucky_swing` | Single Joker worth **-5** (instead of two -2 Jokers) |
|
||||
| `super_kings` | Kings worth **-2** (instead of 0) |
|
||||
| `lucky_sevens` | 7s worth **0** (instead of 7) |
|
||||
| `ten_penny` | 10s worth **1** (instead of 10) |
|
||||
|
||||
## Bonuses & Penalties
|
||||
@@ -128,13 +127,11 @@ Our implementation supports these optional rule variations:
|
||||
| `tied_shame` | Tying another player's score = **+5** penalty to both |
|
||||
| `blackjack` | Exact score of 21 becomes **0** |
|
||||
|
||||
## Gameplay Twists
|
||||
## Special Rules
|
||||
|
||||
| Option | Effect |
|
||||
|--------|--------|
|
||||
| `queens_wild` | Queens match any rank for column pairing |
|
||||
| `four_of_a_kind` | 4 cards of same rank in grid = all 4 score 0 |
|
||||
| `eagle_eye` | Paired Jokers score **-8** (instead of canceling to 0) |
|
||||
| `eagle_eye` | Jokers worth **+2 unpaired**, **-4 paired** (spot the pair!) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
212
server/ai.py
212
server/ai.py
@@ -18,14 +18,9 @@ def get_ai_card_value(card: Card, options: GameOptions) -> int:
|
||||
return get_card_value(card, options)
|
||||
|
||||
|
||||
def can_make_pair(card1: Card, card2: Card, options: GameOptions) -> bool:
|
||||
"""Check if two cards can form a pair (with Queens Wild support)."""
|
||||
if card1.rank == card2.rank:
|
||||
return True
|
||||
if options.queens_wild:
|
||||
if card1.rank == Rank.QUEEN or card2.rank == Rank.QUEEN:
|
||||
return True
|
||||
return False
|
||||
def can_make_pair(card1: Card, card2: Card) -> bool:
|
||||
"""Check if two cards can form a pair."""
|
||||
return card1.rank == card2.rank
|
||||
|
||||
|
||||
def estimate_opponent_min_score(player: Player, game: Game) -> int:
|
||||
@@ -41,11 +36,119 @@ def estimate_opponent_min_score(player: Player, game: Game) -> int:
|
||||
return min_est
|
||||
|
||||
|
||||
def get_end_game_pressure(player: Player, game: Game) -> float:
|
||||
"""
|
||||
Calculate pressure level based on how close opponents are to going out.
|
||||
Returns 0.0-1.0 where higher means more pressure to improve hand NOW.
|
||||
|
||||
Pressure increases when:
|
||||
- Opponents have few hidden cards (close to going out)
|
||||
- We have many hidden cards (stuck with unknown values)
|
||||
"""
|
||||
my_hidden = sum(1 for c in player.cards if not c.face_up)
|
||||
|
||||
# Find the opponent closest to going out
|
||||
min_opponent_hidden = 6
|
||||
for p in game.players:
|
||||
if p.id == player.id:
|
||||
continue
|
||||
opponent_hidden = sum(1 for c in p.cards if not c.face_up)
|
||||
min_opponent_hidden = min(min_opponent_hidden, opponent_hidden)
|
||||
|
||||
# No pressure if opponents have lots of hidden cards
|
||||
if min_opponent_hidden >= 4:
|
||||
return 0.0
|
||||
|
||||
# Pressure scales based on how close opponent is to finishing
|
||||
# 3 hidden = mild pressure (0.4), 2 hidden = medium (0.7), 1 hidden = high (0.9), 0 = max (1.0)
|
||||
base_pressure = {0: 1.0, 1: 0.9, 2: 0.7, 3: 0.4}.get(min_opponent_hidden, 0.0)
|
||||
|
||||
# Increase pressure further if WE have many hidden cards (more unknowns to worry about)
|
||||
hidden_risk_bonus = (my_hidden - 2) * 0.05 # +0.05 per hidden card above 2
|
||||
hidden_risk_bonus = max(0, hidden_risk_bonus)
|
||||
|
||||
return min(1.0, base_pressure + hidden_risk_bonus)
|
||||
|
||||
|
||||
def count_rank_in_hand(player: Player, rank: Rank) -> int:
|
||||
"""Count how many cards of a given rank the player has visible."""
|
||||
return sum(1 for c in player.cards if c.face_up and c.rank == rank)
|
||||
|
||||
|
||||
def count_visible_cards_by_rank(game: Game) -> dict[Rank, int]:
|
||||
"""
|
||||
Count all visible cards of each rank across the entire table.
|
||||
Includes: all face-up player cards + top of discard pile.
|
||||
|
||||
Note: Buried discard cards are NOT counted because they reshuffle
|
||||
back into the deck when it empties.
|
||||
"""
|
||||
counts: dict[Rank, int] = {rank: 0 for rank in Rank}
|
||||
|
||||
# Count all face-up cards in all players' hands
|
||||
for player in game.players:
|
||||
for card in player.cards:
|
||||
if card.face_up:
|
||||
counts[card.rank] += 1
|
||||
|
||||
# Count top of discard pile (the only visible discard)
|
||||
discard_top = game.discard_top()
|
||||
if discard_top:
|
||||
counts[discard_top.rank] += 1
|
||||
|
||||
return counts
|
||||
|
||||
|
||||
def get_pair_viability(rank: Rank, game: Game, exclude_discard_top: bool = False) -> float:
|
||||
"""
|
||||
Calculate how viable it is to pair a card of this rank.
|
||||
Returns 0.0-1.0 where higher means better odds of finding a pair.
|
||||
|
||||
In a standard deck: 4 of each rank (2 Jokers).
|
||||
If you can see N cards of that rank, only (4-N) remain.
|
||||
|
||||
Args:
|
||||
rank: The rank we want to pair
|
||||
exclude_discard_top: If True, don't count discard top (useful when
|
||||
evaluating taking that card - it won't be visible after)
|
||||
"""
|
||||
counts = count_visible_cards_by_rank(game)
|
||||
visible = counts.get(rank, 0)
|
||||
|
||||
# Adjust if we're evaluating the discard top card itself
|
||||
if exclude_discard_top:
|
||||
discard_top = game.discard_top()
|
||||
if discard_top and discard_top.rank == rank:
|
||||
visible = max(0, visible - 1)
|
||||
|
||||
# Cards in deck for this rank
|
||||
max_copies = 2 if rank == Rank.JOKER else 4
|
||||
remaining = max(0, max_copies - visible)
|
||||
|
||||
# Viability scales with remaining copies
|
||||
# 4 remaining = 1.0, 3 = 0.75, 2 = 0.5, 1 = 0.25, 0 = 0.0
|
||||
return remaining / max_copies
|
||||
|
||||
|
||||
def get_game_phase(game: Game) -> str:
|
||||
"""
|
||||
Determine current game phase based on average hidden cards.
|
||||
Returns: 'early', 'mid', or 'late'
|
||||
"""
|
||||
total_hidden = sum(
|
||||
sum(1 for c in p.cards if not c.face_up)
|
||||
for p in game.players
|
||||
)
|
||||
avg_hidden = total_hidden / len(game.players) if game.players else 6
|
||||
|
||||
if avg_hidden >= 4.5:
|
||||
return 'early'
|
||||
elif avg_hidden >= 2.5:
|
||||
return 'mid'
|
||||
else:
|
||||
return 'late'
|
||||
|
||||
|
||||
def has_worse_visible_card(player: Player, card_value: int, options: GameOptions) -> bool:
|
||||
"""Check if player has a visible card worse than the given value.
|
||||
|
||||
@@ -255,29 +358,10 @@ class GolfAI:
|
||||
if discard_card.rank == Rank.KING:
|
||||
return True
|
||||
|
||||
# Auto-take 7s when lucky_sevens enabled (they're worth 0)
|
||||
if discard_card.rank == Rank.SEVEN and options.lucky_sevens:
|
||||
return True
|
||||
|
||||
# Auto-take 10s when ten_penny enabled (they're worth 0)
|
||||
# Auto-take 10s when ten_penny enabled (they're worth 1)
|
||||
if discard_card.rank == Rank.TEN and options.ten_penny:
|
||||
return True
|
||||
|
||||
# Queens Wild: Queen can complete ANY pair
|
||||
if options.queens_wild and discard_card.rank == Rank.QUEEN:
|
||||
for i, card in enumerate(player.cards):
|
||||
if card.face_up:
|
||||
pair_pos = (i + 3) % 6 if i < 3 else i - 3
|
||||
if not player.cards[pair_pos].face_up:
|
||||
# We have an incomplete column - Queen could pair it
|
||||
return True
|
||||
|
||||
# Four of a Kind: If we have 2+ of this rank, consider taking
|
||||
if options.four_of_a_kind:
|
||||
rank_count = count_rank_in_hand(player, discard_card.rank)
|
||||
if rank_count >= 2:
|
||||
return True
|
||||
|
||||
# Take card if it could make a column pair (but NOT for negative value cards)
|
||||
# Pairing negative cards is bad - you lose the negative benefit
|
||||
if discard_value > 0:
|
||||
@@ -289,15 +373,30 @@ class GolfAI:
|
||||
if card.face_up and card.rank == discard_card.rank and not pair_card.face_up:
|
||||
return True
|
||||
|
||||
# Queens Wild: check if we can pair with Queen
|
||||
if options.queens_wild:
|
||||
if card.face_up and can_make_pair(card, discard_card, options) and not pair_card.face_up:
|
||||
return True
|
||||
|
||||
# Take low cards (using house rule adjusted values)
|
||||
if discard_value <= 2:
|
||||
# Threshold adjusts by game phase - early game be picky, late game less so
|
||||
phase = get_game_phase(game)
|
||||
base_threshold = {'early': 2, 'mid': 3, 'late': 4}.get(phase, 2)
|
||||
|
||||
if discard_value <= base_threshold:
|
||||
return True
|
||||
|
||||
# Calculate end-game pressure from opponents close to going out
|
||||
pressure = get_end_game_pressure(player, game)
|
||||
|
||||
# Under pressure, expand what we consider "worth taking"
|
||||
# When opponents are close to going out, take decent cards to avoid
|
||||
# getting stuck with unknown bad cards when the round ends
|
||||
if pressure > 0.2:
|
||||
# Scale threshold: at pressure 0.2 take 4s, at 0.5+ take 6s
|
||||
pressure_threshold = 3 + int(pressure * 6) # 4 to 9 based on pressure
|
||||
pressure_threshold = min(pressure_threshold, 7) # Cap at 7
|
||||
if discard_value <= pressure_threshold:
|
||||
# Only take if we have hidden cards that could be worse
|
||||
my_hidden = sum(1 for c in player.cards if not c.face_up)
|
||||
if my_hidden > 0:
|
||||
return True
|
||||
|
||||
# Check if we have cards worse than the discard
|
||||
worst_visible = -999
|
||||
for card in player.cards:
|
||||
@@ -338,15 +437,6 @@ class GolfAI:
|
||||
if not player.cards[pair_pos].face_up:
|
||||
return pair_pos
|
||||
|
||||
# Four of a Kind: If we have 3 of this rank and draw the 4th, prioritize keeping
|
||||
if options.four_of_a_kind:
|
||||
rank_count = count_rank_in_hand(player, drawn_card.rank)
|
||||
if rank_count >= 3:
|
||||
# We'd have 4 - swap into any face-down spot
|
||||
face_down = [i for i, c in enumerate(player.cards) if not c.face_up]
|
||||
if face_down:
|
||||
return random.choice(face_down)
|
||||
|
||||
# Check for column pair opportunity first
|
||||
# But DON'T pair negative value cards (2s, Jokers) - keeping them unpaired is better!
|
||||
# Exception: Eagle Eye makes pairing Jokers GOOD (doubled negative)
|
||||
@@ -366,13 +456,6 @@ class GolfAI:
|
||||
if pair_card.face_up and pair_card.rank == drawn_card.rank and not card.face_up:
|
||||
return i
|
||||
|
||||
# Queens Wild: Queen can pair with anything
|
||||
if options.queens_wild:
|
||||
if card.face_up and can_make_pair(card, drawn_card, options) and not pair_card.face_up:
|
||||
return pair_pos
|
||||
if pair_card.face_up and can_make_pair(pair_card, drawn_card, options) and not card.face_up:
|
||||
return i
|
||||
|
||||
# Find best swap among face-up cards that are BAD (positive value)
|
||||
# Don't swap good cards (Kings, 2s, etc.) just for marginal gains -
|
||||
# we want to keep good cards and put new good cards into face-down positions
|
||||
@@ -409,27 +492,44 @@ class GolfAI:
|
||||
return i
|
||||
|
||||
# Consider swapping with face-down cards for very good cards (negative or zero value)
|
||||
# 7s (lucky_sevens) and 10s (ten_penny) become "excellent" cards worth keeping
|
||||
# 10s (ten_penny) become "excellent" cards worth keeping
|
||||
is_excellent = (drawn_value <= 0 or
|
||||
drawn_card.rank == Rank.ACE or
|
||||
(options.lucky_sevens and drawn_card.rank == Rank.SEVEN) or
|
||||
(options.ten_penny and drawn_card.rank == Rank.TEN))
|
||||
|
||||
# Calculate pair viability and game phase for smarter decisions
|
||||
pair_viability = get_pair_viability(drawn_card.rank, game)
|
||||
phase = get_game_phase(game)
|
||||
pressure = get_end_game_pressure(player, game)
|
||||
|
||||
if is_excellent:
|
||||
face_down = [i for i, c in enumerate(player.cards) if not c.face_up]
|
||||
if face_down:
|
||||
# Pair hunters might hold out hoping for matches
|
||||
if profile.pair_hope > 0.6 and random.random() < profile.pair_hope:
|
||||
# BUT: reduce hope if pair is unlikely or late game pressure
|
||||
effective_hope = profile.pair_hope * pair_viability
|
||||
if phase == 'late' or pressure > 0.5:
|
||||
effective_hope *= 0.3 # Much less willing to gamble late game
|
||||
if effective_hope > 0.6 and random.random() < effective_hope:
|
||||
return None
|
||||
return random.choice(face_down)
|
||||
|
||||
# For medium cards, swap threshold based on profile
|
||||
if drawn_value <= profile.swap_threshold:
|
||||
# Late game: be more willing to swap in medium cards
|
||||
effective_threshold = profile.swap_threshold
|
||||
if phase == 'late' or pressure > 0.5:
|
||||
effective_threshold += 2 # Accept higher value cards under pressure
|
||||
|
||||
if drawn_value <= effective_threshold:
|
||||
face_down = [i for i, c in enumerate(player.cards) if not c.face_up]
|
||||
if face_down:
|
||||
# Pair hunters hold high cards hoping for matches
|
||||
if profile.pair_hope > 0.5 and drawn_value >= 6:
|
||||
if random.random() < profile.pair_hope:
|
||||
# BUT: check if pairing is actually viable
|
||||
effective_hope = profile.pair_hope * pair_viability
|
||||
if phase == 'late' or pressure > 0.5:
|
||||
effective_hope *= 0.3 # Don't gamble late game
|
||||
if effective_hope > 0.5 and drawn_value >= 6:
|
||||
if random.random() < effective_hope:
|
||||
return None
|
||||
return random.choice(face_down)
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ from enum import Enum
|
||||
from constants import (
|
||||
DEFAULT_CARD_VALUES,
|
||||
SUPER_KINGS_VALUE,
|
||||
LUCKY_SEVENS_VALUE,
|
||||
TEN_PENNY_VALUE,
|
||||
LUCKY_SWING_JOKER_VALUE,
|
||||
)
|
||||
@@ -58,11 +57,11 @@ def get_card_value(card: "Card", options: Optional["GameOptions"] = None) -> int
|
||||
"""
|
||||
if options:
|
||||
if card.rank == Rank.JOKER:
|
||||
if options.eagle_eye:
|
||||
return 2 # Eagle-eyed: jokers worth +2 unpaired, -4 when paired (handled in calculate_score)
|
||||
return LUCKY_SWING_JOKER_VALUE if options.lucky_swing else RANK_VALUES[Rank.JOKER]
|
||||
if card.rank == Rank.KING and options.super_kings:
|
||||
return SUPER_KINGS_VALUE
|
||||
if card.rank == Rank.SEVEN and options.lucky_sevens:
|
||||
return LUCKY_SEVENS_VALUE
|
||||
if card.rank == Rank.TEN and options.ten_penny:
|
||||
return TEN_PENNY_VALUE
|
||||
return RANK_VALUES[card.rank]
|
||||
@@ -148,58 +147,39 @@ class Player:
|
||||
if len(self.cards) != 6:
|
||||
return 0
|
||||
|
||||
def cards_match(card1: Card, card2: Card) -> bool:
|
||||
"""Check if two cards match for pairing (with Queens Wild support)."""
|
||||
if card1.rank == card2.rank:
|
||||
return True
|
||||
if options and options.queens_wild:
|
||||
if card1.rank == Rank.QUEEN or card2.rank == Rank.QUEEN:
|
||||
return True
|
||||
return False
|
||||
|
||||
total = 0
|
||||
jack_pairs = 0 # Track paired Jacks for Wolfpack bonus
|
||||
|
||||
# Cards are arranged in 2 rows x 3 columns
|
||||
# Position mapping: [0, 1, 2] (top row)
|
||||
# [3, 4, 5] (bottom row)
|
||||
# Columns: (0,3), (1,4), (2,5)
|
||||
|
||||
# Check for Four of a Kind first (4 cards same rank = all score 0)
|
||||
four_of_kind_positions: set[int] = set()
|
||||
if options and options.four_of_a_kind:
|
||||
from collections import Counter
|
||||
rank_positions: dict[Rank, list[int]] = {}
|
||||
for i, card in enumerate(self.cards):
|
||||
if card.rank not in rank_positions:
|
||||
rank_positions[card.rank] = []
|
||||
rank_positions[card.rank].append(i)
|
||||
for rank, positions in rank_positions.items():
|
||||
if len(positions) >= 4:
|
||||
four_of_kind_positions.update(positions)
|
||||
|
||||
for col in range(3):
|
||||
top_idx = col
|
||||
bottom_idx = col + 3
|
||||
top_card = self.cards[top_idx]
|
||||
bottom_card = self.cards[bottom_idx]
|
||||
|
||||
# Skip if part of four of a kind
|
||||
if top_idx in four_of_kind_positions and bottom_idx in four_of_kind_positions:
|
||||
continue
|
||||
|
||||
# Check if column pair matches (same rank or Queens Wild)
|
||||
if cards_match(top_card, bottom_card):
|
||||
# Eagle Eye: paired jokers score -8 (2³) instead of canceling
|
||||
# Check if column pair matches (same rank)
|
||||
if top_card.rank == bottom_card.rank:
|
||||
# Track Jack pairs for Wolfpack
|
||||
if top_card.rank == Rank.JACK:
|
||||
jack_pairs += 1
|
||||
# Eagle Eye: paired jokers score -4 (reward for spotting the pair)
|
||||
if (options and options.eagle_eye and
|
||||
top_card.rank == Rank.JOKER and bottom_card.rank == Rank.JOKER):
|
||||
total -= 8
|
||||
total -= 4
|
||||
continue
|
||||
# Normal matching pair scores 0
|
||||
continue
|
||||
else:
|
||||
if top_idx not in four_of_kind_positions:
|
||||
total += get_card_value(top_card, options)
|
||||
if bottom_idx not in four_of_kind_positions:
|
||||
total += get_card_value(bottom_card, options)
|
||||
total += get_card_value(top_card, options)
|
||||
total += get_card_value(bottom_card, options)
|
||||
|
||||
# Wolfpack bonus: 2 pairs of Jacks = -5 pts
|
||||
if options and options.wolfpack and jack_pairs >= 2:
|
||||
total -= 5
|
||||
|
||||
self.score = total
|
||||
return total
|
||||
@@ -228,7 +208,6 @@ class GameOptions:
|
||||
# House Rules - Point Modifiers
|
||||
lucky_swing: bool = False # Single joker worth -5 instead of two -2 jokers
|
||||
super_kings: bool = False # Kings worth -2 instead of 0
|
||||
lucky_sevens: bool = False # 7s worth 0 instead of 7
|
||||
ten_penny: bool = False # 10s worth 1 (like Ace) instead of 10
|
||||
|
||||
# House Rules - Bonuses/Penalties
|
||||
@@ -236,10 +215,9 @@ class GameOptions:
|
||||
underdog_bonus: bool = False # Lowest score player gets -3 each hole
|
||||
tied_shame: bool = False # Tie with someone's score = +5 penalty to both
|
||||
blackjack: bool = False # Hole score of exactly 21 becomes 0
|
||||
wolfpack: bool = False # 2 pairs of Jacks = -5 bonus
|
||||
|
||||
# House Rules - Gameplay Twists
|
||||
queens_wild: bool = False # Queens count as any rank for pairing
|
||||
four_of_a_kind: bool = False # 4 cards of same rank in grid = all 4 score 0
|
||||
# House Rules - Special
|
||||
eagle_eye: bool = False # Paired jokers double instead of cancel (-4 or -10)
|
||||
|
||||
|
||||
@@ -271,12 +249,12 @@ class Game:
|
||||
# Apply house rule modifications
|
||||
if self.options.super_kings:
|
||||
values['K'] = SUPER_KINGS_VALUE
|
||||
if self.options.lucky_sevens:
|
||||
values['7'] = LUCKY_SEVENS_VALUE
|
||||
if self.options.ten_penny:
|
||||
values['10'] = TEN_PENNY_VALUE
|
||||
if self.options.lucky_swing:
|
||||
values['★'] = LUCKY_SWING_JOKER_VALUE
|
||||
elif self.options.eagle_eye:
|
||||
values['★'] = 2 # Eagle-eyed: +2 unpaired, -4 paired
|
||||
|
||||
return values
|
||||
|
||||
@@ -613,6 +591,34 @@ class Game:
|
||||
|
||||
discard_top = self.discard_top()
|
||||
|
||||
# Build active rules list for display
|
||||
active_rules = []
|
||||
if self.options:
|
||||
if self.options.flip_on_discard:
|
||||
active_rules.append("Flip on Discard")
|
||||
if self.options.knock_penalty:
|
||||
active_rules.append("Knock Penalty")
|
||||
if self.options.use_jokers and not self.options.lucky_swing and not self.options.eagle_eye:
|
||||
active_rules.append("Jokers")
|
||||
if self.options.lucky_swing:
|
||||
active_rules.append("Lucky Swing")
|
||||
if self.options.eagle_eye:
|
||||
active_rules.append("Eagle-Eye")
|
||||
if self.options.super_kings:
|
||||
active_rules.append("Super Kings")
|
||||
if self.options.ten_penny:
|
||||
active_rules.append("Ten Penny")
|
||||
if self.options.knock_bonus:
|
||||
active_rules.append("Knock Bonus")
|
||||
if self.options.underdog_bonus:
|
||||
active_rules.append("Underdog")
|
||||
if self.options.tied_shame:
|
||||
active_rules.append("Tied Shame")
|
||||
if self.options.blackjack:
|
||||
active_rules.append("Blackjack")
|
||||
if self.options.wolfpack:
|
||||
active_rules.append("Wolfpack")
|
||||
|
||||
return {
|
||||
"phase": self.phase.value,
|
||||
"players": players_data,
|
||||
@@ -630,4 +636,5 @@ class Game:
|
||||
"initial_flips": self.options.initial_flips,
|
||||
"flip_on_discard": self.flip_on_discard,
|
||||
"card_values": self.get_card_values(),
|
||||
"active_rules": active_rules,
|
||||
}
|
||||
|
||||
@@ -184,17 +184,14 @@ async def websocket_endpoint(websocket: WebSocket):
|
||||
# House Rules - Point Modifiers
|
||||
lucky_swing=data.get("lucky_swing", False),
|
||||
super_kings=data.get("super_kings", False),
|
||||
lucky_sevens=data.get("lucky_sevens", False),
|
||||
ten_penny=data.get("ten_penny", False),
|
||||
# House Rules - Bonuses/Penalties
|
||||
knock_bonus=data.get("knock_bonus", False),
|
||||
underdog_bonus=data.get("underdog_bonus", False),
|
||||
tied_shame=data.get("tied_shame", False),
|
||||
blackjack=data.get("blackjack", False),
|
||||
# House Rules - Gameplay Twists
|
||||
queens_wild=data.get("queens_wild", False),
|
||||
four_of_a_kind=data.get("four_of_a_kind", False),
|
||||
eagle_eye=data.get("eagle_eye", False),
|
||||
wolfpack=data.get("wolfpack", False),
|
||||
)
|
||||
|
||||
# Validate settings
|
||||
@@ -331,6 +328,37 @@ async def websocket_endpoint(websocket: WebSocket):
|
||||
await handle_player_leave(current_room, player_id)
|
||||
current_room = None
|
||||
|
||||
elif msg_type == "leave_game":
|
||||
# Player leaves during an active game
|
||||
if current_room:
|
||||
await handle_player_leave(current_room, player_id)
|
||||
current_room = None
|
||||
|
||||
elif msg_type == "end_game":
|
||||
# Host ends the game for everyone
|
||||
if not current_room:
|
||||
continue
|
||||
|
||||
room_player = current_room.get_player(player_id)
|
||||
if not room_player or not room_player.is_host:
|
||||
await websocket.send_json({
|
||||
"type": "error",
|
||||
"message": "Only the host can end the game",
|
||||
})
|
||||
continue
|
||||
|
||||
# Notify all players that the game has ended
|
||||
await current_room.broadcast({
|
||||
"type": "game_ended",
|
||||
"reason": "Host ended the game",
|
||||
})
|
||||
|
||||
# Clean up the room
|
||||
for cpu in list(current_room.get_cpu_players()):
|
||||
current_room.remove_player(cpu.id)
|
||||
room_manager.remove_room(current_room.code)
|
||||
current_room = None
|
||||
|
||||
except WebSocketDisconnect:
|
||||
if current_room:
|
||||
await handle_player_leave(current_room, player_id)
|
||||
|
||||
@@ -235,11 +235,6 @@ def get_test_configs() -> list[tuple[str, GameOptions]]:
|
||||
super_kings=True,
|
||||
)))
|
||||
|
||||
configs.append(("lucky_sevens", GameOptions(
|
||||
initial_flips=2,
|
||||
lucky_sevens=True,
|
||||
)))
|
||||
|
||||
configs.append(("ten_penny", GameOptions(
|
||||
initial_flips=2,
|
||||
ten_penny=True,
|
||||
@@ -267,17 +262,7 @@ def get_test_configs() -> list[tuple[str, GameOptions]]:
|
||||
blackjack=True,
|
||||
)))
|
||||
|
||||
# === Gameplay Twists ===
|
||||
|
||||
configs.append(("queens_wild", GameOptions(
|
||||
initial_flips=2,
|
||||
queens_wild=True,
|
||||
)))
|
||||
|
||||
configs.append(("four_of_a_kind", GameOptions(
|
||||
initial_flips=2,
|
||||
four_of_a_kind=True,
|
||||
)))
|
||||
# === Special Rules ===
|
||||
|
||||
configs.append(("eagle_eye", GameOptions(
|
||||
initial_flips=2,
|
||||
@@ -292,7 +277,6 @@ def get_test_configs() -> list[tuple[str, GameOptions]]:
|
||||
use_jokers=True,
|
||||
lucky_swing=True,
|
||||
super_kings=True,
|
||||
lucky_sevens=True,
|
||||
ten_penny=True,
|
||||
)))
|
||||
|
||||
@@ -311,8 +295,6 @@ def get_test_configs() -> list[tuple[str, GameOptions]]:
|
||||
configs.append(("WILD CARDS", GameOptions(
|
||||
initial_flips=2,
|
||||
use_jokers=True,
|
||||
queens_wild=True,
|
||||
four_of_a_kind=True,
|
||||
eagle_eye=True,
|
||||
)))
|
||||
|
||||
@@ -329,14 +311,11 @@ def get_test_configs() -> list[tuple[str, GameOptions]]:
|
||||
use_jokers=True,
|
||||
lucky_swing=True,
|
||||
super_kings=True,
|
||||
lucky_sevens=True,
|
||||
ten_penny=True,
|
||||
knock_bonus=True,
|
||||
underdog_bonus=True,
|
||||
tied_shame=True,
|
||||
blackjack=True,
|
||||
queens_wild=True,
|
||||
four_of_a_kind=True,
|
||||
eagle_eye=True,
|
||||
)))
|
||||
|
||||
@@ -457,15 +436,6 @@ def print_expected_effects(results: list[RuleTestResult]):
|
||||
status = "✓" if diff < 0 else "✗"
|
||||
checks.append((r.name, expected, f"{actual} ({diff:+.1f})", status))
|
||||
|
||||
# lucky_sevens should lower scores (7s worth 0 instead of 7)
|
||||
r = find("lucky_sevens")
|
||||
if r and r.scores:
|
||||
diff = r.mean_score - baseline.mean_score
|
||||
expected = "LOWER scores"
|
||||
actual = "lower" if diff < -1 else "higher" if diff > 1 else "similar"
|
||||
status = "✓" if diff < 0 else "✗"
|
||||
checks.append((r.name, expected, f"{actual} ({diff:+.1f})", status))
|
||||
|
||||
# ten_penny should lower scores (10s worth 1 instead of 10)
|
||||
r = find("ten_penny")
|
||||
if r and r.scores:
|
||||
|
||||
@@ -145,7 +145,7 @@ class TestMayaBugFix:
|
||||
When forced to swap (drew from discard), the AI should use
|
||||
get_ai_card_value() to find the worst card, not raw value().
|
||||
|
||||
This matters for house rules like super_kings, lucky_sevens, etc.
|
||||
This matters for house rules like super_kings, ten_penny, etc.
|
||||
"""
|
||||
game = create_test_game()
|
||||
game.options = GameOptions(super_kings=True) # Kings now worth -2
|
||||
|
||||
Reference in New Issue
Block a user