diff --git a/client/app.js b/client/app.js index 25fdc1b..acb9a5d 100644 --- a/client/app.js +++ b/client/app.js @@ -163,6 +163,11 @@ class GolfGame { this.tiedShameCheckbox = document.getElementById('tied-shame'); this.blackjackCheckbox = document.getElementById('blackjack'); this.wolfpackCheckbox = document.getElementById('wolfpack'); + // House Rules - New Variants + this.flipAsActionCheckbox = document.getElementById('flip-as-action'); + this.fourOfAKindCheckbox = document.getElementById('four-of-a-kind'); + this.negativePairsCheckbox = document.getElementById('negative-pairs-keep-value'); + this.oneEyedJacksCheckbox = document.getElementById('one-eyed-jacks'); this.startGameBtn = document.getElementById('start-game-btn'); this.leaveRoomBtn = document.getElementById('leave-room-btn'); this.addCpuBtn = document.getElementById('add-cpu-btn'); @@ -397,7 +402,11 @@ class GolfGame { break; case 'your_turn': - this.showToast('Your turn! Draw a card', 'your-turn'); + if (this.gameState && this.gameState.flip_as_action) { + this.showToast('Your turn! Draw or flip a card', 'your-turn'); + } else { + this.showToast('Your turn! Draw a card', 'your-turn'); + } break; case 'card_drawn': @@ -516,6 +525,12 @@ class GolfGame { const blackjack = this.blackjackCheckbox.checked; const wolfpack = this.wolfpackCheckbox.checked; + // House Rules - New Variants + const flip_as_action = this.flipAsActionCheckbox.checked; + const four_of_a_kind = this.fourOfAKindCheckbox.checked; + const negative_pairs_keep_value = this.negativePairsCheckbox.checked; + const one_eyed_jacks = this.oneEyedJacksCheckbox.checked; + this.send({ type: 'start_game', decks, @@ -532,7 +547,11 @@ class GolfGame { tied_shame, blackjack, eagle_eye, - wolfpack + wolfpack, + flip_as_action, + four_of_a_kind, + negative_pairs_keep_value, + one_eyed_jacks }); } @@ -1192,6 +1211,21 @@ class GolfGame { const card = myData.cards[position]; + // Check for flip-as-action: can flip face-down card instead of drawing + const canFlipAsAction = this.gameState.flip_as_action && + this.isMyTurn() && + !this.drawnCard && + !this.gameState.has_drawn_card && + !card.face_up && + !this.gameState.waiting_for_initial_flip; + if (canFlipAsAction) { + this.playSound('flip'); + this.fireLocalFlipAnimation(position, card); + this.send({ type: 'flip_as_action', position }); + this.hideToast(); + return; + } + // Check if action is allowed - if not, play reject sound const canAct = this.gameState.waiting_for_initial_flip || this.drawnCard || @@ -1449,7 +1483,11 @@ class GolfGame { if (currentPlayer && currentPlayer.id !== this.playerId) { this.setStatus(`${currentPlayer.name}'s turn`); } else if (this.isMyTurn()) { - this.setStatus('Your turn - draw a card', 'your-turn'); + if (this.gameState.flip_as_action && !this.drawnCard && !this.gameState.has_drawn_card) { + this.setStatus('Your turn - draw a card or flip one', 'your-turn'); + } else { + this.setStatus('Your turn - draw a card', 'your-turn'); + } } else { this.setStatus(''); } diff --git a/client/index.html b/client/index.html index 20a6e04..804ed92 100644 --- a/client/index.html +++ b/client/index.html @@ -104,7 +104,7 @@ What happens when you draw from deck and discard @@ -186,7 +186,33 @@ + + + +
+

New Variants

+
+ + + +
@@ -296,8 +322,47 @@
- -

Game Rules

+ + +
+

Golf Rules

+

6-Card Golf Card Game - Complete Guide

+
+ + +

Basic Rules

@@ -331,24 +396,24 @@

Column Pairing (IMPORTANT!)

This is the most important rule to understand:

-

If both cards in a vertical column have the same rank (like two Kings, or two 7s), that entire column scores 0 points - regardless of what the cards are worth individually!

+

If both cards in a vertical column have the same rank (like two 8s or two Jacks), that entire column scores 0 points - regardless of what the cards are worth individually!

Example:

 Your 6-card grid:
   Col1  Col2  Col3
- [K]   [5]   [7]   ← Top row
- [K]   [3]   [9]   ← Bottom row
+ [8]   [5]   [7]   ← Top row
+ [8]   [3]   [9]   ← Bottom row
 
-Column 1: K + K = PAIR! = 0 points (not 0+0)
+Column 1: 8 + 8 = PAIR! = 0 points (not 16!)
 Column 2: 5 + 3 = 8 points
 Column 3: 7 + 9 = 16 points
 
 TOTAL: 0 + 8 + 16 = 24 points
-

IMPORTANT: When you pair cards, you get 0 points for that column - even if the cards have negative values! Two 2s paired = 0 points (not -4). Two Jokers paired = 0 points (not -4).

+

IMPORTANT: When you pair cards, you get 0 points for that column - even if the cards have negative values! Two 2s paired = 0 points (not -4). Two Jokers paired = 0 points (not -4). Exception: The "Negative Pairs Keep Value" house rule changes this - paired negative cards keep their -4 value!

@@ -386,55 +451,129 @@ TOTAL: 0 + 8 + 16 = 24 points

Standard Mode (No Flip)

Default setting. Discarding ends your turn immediately.

How it works: When you draw from the deck and decide not to use it, you simply discard it and your turn is over. Nothing else happens.

-

Best for: Traditional gameplay, longer games, maximum hidden information.

+

Strategic impact: Information is precious. You only learn what's in your hand by actively swapping cards, so there's more gambling on face-down cards. Rewards good memory and tracking what opponents discard.

+

Best for: Traditional gameplay, longer games, players who enjoy mystery and risk.

Speed Golf Mode (Must Flip)

Every discard reveals one of your hidden cards.

How it works: When you draw from the deck and discard, you MUST also flip over one of your face-down cards. This is mandatory - you cannot skip it.

-

Why use it: Games go much faster because more cards get revealed every turn. More information for everyone = more strategic decisions.

-

Best for: Quick games, players who like faster-paced action.

+

Strategic impact: Even "bad" draws give you information. Reduces the luck factor since everyone makes more informed decisions. Games naturally end faster with less hidden information.

+

Best for: Quick games, players who prefer skill over luck.

-

Endgame Mode (Flip When Close to Finishing)

-

Flip activates when any player has only 1 hidden card remaining.

+

Endgame Mode (Catch-Up Flip)

+

Optional flip activates when any player has only 1 hidden card left.

How it works:

-

Why use it: Creates dramatic tension near the end of rounds. Do you reveal more to try to improve your score, or keep cards hidden to maintain mystery?

-

Best for: Players who enjoy dramatic finishes and tough end-game decisions.

+

Strategic impact: This is a catch-up mechanic. When someone is about to go out, trailing players can accelerate their information gathering to find pairs or swap out bad cards. The leader (who triggered this) doesn't benefit since they have no hidden cards left. Reduces the "runaway leader" problem and keeps games competitive.

+

Best for: Competitive play where you want trailing players to have a fighting chance.

House Rules (Optional Variants)

-

Point Modifiers

- +
+

Point Modifiers

+
+

Super Kings

+

Kings are worth -2 points instead of 0.

+

Strategic impact: Kings become valuable to keep unpaired, not just pairing fodder. Creates interesting decisions - do you pair Kings for 0, or keep them separate for -4 total?

+
+
+

Ten Penny

+

10s are worth 1 point instead of 10.

+

Strategic impact: Removes the "10 disaster" - drawing a 10 is no longer a crisis. Queens and Jacks become the only truly bad cards. Makes the game more forgiving.

+
+
-

Joker Variants

- +
+

Joker Variants

+
+

Standard Jokers

+

2 Jokers per deck, each worth -2 points.

+

Strategic impact: Jokers are great to find but pairing them is wasteful (0 points instead of -4). Best kept in different columns. Adds 2 premium cards to hunt for.

+
+
+

Lucky Swing

+

Only 1 Joker in the entire deck, worth -5 points.

+

Strategic impact: High variance. Whoever finds this rare card gets a significant advantage. Increases the luck factor - sometimes you get it, sometimes your opponent does.

+
+
+

Eagle Eye

+

Jokers are worth +2 unpaired, but -4 when paired.

+

Strategic impact: Risk/reward Jokers. Finding one actually hurts you (+2) until you commit to finding the second. Rewards aggressive searching and creates tense decisions about whether to keep hunting or cut your losses.

+
+
-

Bonuses & Penalties

- +
+

Going Out Rules

+
+

Knock Penalty

+

+10 points if you go out but don't have the lowest score.

+

Strategic impact: Discourages reckless rushing. You need to be confident you're winning before going out. Rewards patience and reading your opponents' likely scores. Can backfire spectacularly if you misjudge.

+
+
+

Knock Bonus

+

-5 points for going out first (regardless of who wins).

+

Strategic impact: Encourages racing to finish, even with a mediocre hand. The 5-point bonus might make up for a slightly worse score. Speeds up gameplay.

+
+

Combining Knock Penalty + Knock Bonus creates high-stakes "going out" decisions: -5 if you win, +10 if you lose!

+
+ +
+

Scoring Bonuses

+
+

Underdog Bonus

+

Round winner gets -3 points extra.

+

Strategic impact: Amplifies winning - the best player each round pulls further ahead. Can lead to snowballing leads over multiple holes. Rewards consistency.

+
+
+

Tied Shame

+

If you tie another player's score, both get +5 penalty.

+

Strategic impact: Punishes playing it safe. If you suspect a tie, you need to take risks to differentiate your score. Creates interesting late-round decisions.

+
+
+

Blackjack

+

Score of exactly 21 becomes 0.

+

Strategic impact: A "hail mary" comeback. If you're stuck at 21, you're suddenly in great shape. Mostly luck, but adds exciting moments when it happens.

+
+
+

Wolfpack

+

Having all 4 Jacks (2 pairs) gives -20 bonus.

+

Strategic impact: Extremely rare but now a significant reward! Turns a potential disaster (40 points of Jacks) into a triumph. The huge bonus makes it worth celebrating when achieved, though still not worth actively pursuing.

+
+
+ +
+

New Variants

+
+

Flip as Action

+

Use your turn to flip one of your face-down cards without drawing. Ends your turn immediately.

+

Strategic impact: Lets you gather information without risking a bad deck draw. Conservative players can learn their hand safely. However, you miss the chance to actively improve your hand - you're just learning what you have.

+
+
+

Four of a Kind

+

Having 4 cards of the same rank across two columns scores -20 bonus.

+

Strategic impact: Rewards collecting matching cards beyond just column pairs. Changes whether you should take a third or fourth copy of a rank. If you already have two pairs of 8s, that's -20 extra! Stacks with Wolfpack: four Jacks = -40 total.

+
+
+

Negative Pairs Keep Value

+

When you pair 2s or Jokers in a column, they keep their combined -4 points instead of becoming 0.

+

Strategic impact: Major change! Pairing your best cards is now beneficial. Two 2s paired = -4 points, not 0. This encourages hunting for duplicate negative cards and fundamentally changes how you value 2s and Jokers.

+
+
+

One-Eyed Jacks

+

The Jack of Hearts (J♥) and Jack of Spades (J♠) - the "one-eyed" Jacks - are worth 0 points instead of 10.

+

Strategic impact: Two of the four Jacks become safe cards, comparable to Kings. J♥ and J♠ are now good cards to keep! Only J♣ and J♦ remain dangerous. Reduces the "Jack disaster" probability by half.

+
+
@@ -481,8 +620,13 @@ TOTAL: 0 + 8 + 16 = 24 points
-

Q: Why would I NOT flip in Endgame mode?

-

A: Maybe you have a hidden card you hope is good, and you don't want to reveal a potential disaster. Or maybe you want to keep your opponents guessing about your score. It's a strategic choice!

+

Q: How does Endgame mode help trailing players?

+

A: When someone is close to going out, they've likely optimized their hand already. The optional flip lets everyone else accelerate their information gathering - flipping cards to find pairs or identify which cards to swap out. The leader doesn't benefit (they have no hidden cards left), so it's purely a catch-up mechanic.

+
+ +
+

Q: Why would I skip the flip in Endgame mode?

+

A: If you're already winning or your remaining hidden cards are statistically likely to be good, you might prefer not to risk revealing a disaster. It's a calculated gamble!

diff --git a/server/ai.py b/server/ai.py index 83daa86..a0a1298 100644 --- a/server/ai.py +++ b/server/ai.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from typing import Optional from enum import Enum -from game import Card, Player, Game, GamePhase, GameOptions, RANK_VALUES, Rank, get_card_value +from game import Card, Player, Game, GamePhase, GameOptions, RANK_VALUES, Rank, Suit, get_card_value # Debug logging configuration @@ -226,6 +226,10 @@ def filter_bad_pair_positions( if options.eagle_eye and drawn_card.rank == Rank.JOKER: return positions + # Exception: Negative Pairs Keep Value makes pairing negative cards GOOD + if options.negative_pairs_keep_value: + return positions + filtered = [] for pos in positions: partner_pos = get_column_partner_position(pos) @@ -475,6 +479,12 @@ class GolfAI: ai_log(f" >> TAKE: King (always take)") return True + # One-eyed Jacks: J♥ and J♠ are worth 0, always take them + if options.one_eyed_jacks: + if discard_card.rank == Rank.JACK and discard_card.suit in (Suit.HEARTS, Suit.SPADES): + ai_log(f" >> TAKE: One-eyed Jack (worth 0)") + return True + # Auto-take 10s when ten_penny enabled (they're worth 1) if discard_card.rank == Rank.TEN and options.ten_penny: ai_log(f" >> TAKE: 10 (ten_penny rule)") @@ -578,11 +588,17 @@ class GolfAI: pair_bonus = drawn_value + partner_value score += pair_bonus * pair_weight # Pair hunters value this more else: - # Pairing negative cards - usually bad + # Pairing negative cards if options.eagle_eye and drawn_card.rank == Rank.JOKER: - score += 8 * pair_weight # Eagle Eye Joker pairs + score += 8 * pair_weight # Eagle Eye Joker pairs = -4 + elif options.negative_pairs_keep_value: + # Negative Pairs Keep Value: pairing 2s/Jokers is NOW good! + # Pair of 2s = -4, pair of Jokers = -4 (instead of 0) + pair_benefit = abs(drawn_value + partner_value) + score += pair_benefit * pair_weight + ai_log(f" Negative pair keep value bonus: +{pair_benefit * pair_weight:.1f}") else: - # Penalty, but pair hunters might still do it + # Standard rules: penalty for wasting negative cards penalty = abs(drawn_value) * 2 * (2.0 - profile.pair_hope) score -= penalty @@ -632,6 +648,20 @@ class GolfAI: pair_viability = get_pair_viability(drawn_card.rank, game) score += pair_viability * pair_weight * 0.5 + # 4b. FOUR OF A KIND PURSUIT + # When four_of_a_kind rule is enabled, boost score for collecting 3rd/4th card + if options.four_of_a_kind: + # Count how many of this rank player already has visible (excluding current position) + rank_count = sum( + 1 for i, c in enumerate(player.cards) + if c.face_up and c.rank == drawn_card.rank and i != pos + ) + if rank_count >= 2: + # Already have 2+ of this rank, getting more is great for 4-of-a-kind + four_kind_bonus = rank_count * 4 # 8 for 2 cards, 12 for 3 cards + score += four_kind_bonus + ai_log(f" Four-of-a-kind pursuit bonus: +{four_kind_bonus}") + # 5. GO-OUT SAFETY - Penalty for going out with bad score face_down_positions = [i for i, c in enumerate(player.cards) if not c.face_up] if len(face_down_positions) == 1 and pos == face_down_positions[0]: @@ -862,6 +892,62 @@ class GolfAI: ai_log(f" >> FLIP: choosing to reveal for information") return False + @staticmethod + def should_use_flip_action(game: Game, player: Player, profile: CPUProfile) -> Optional[int]: + """ + Decide whether to use flip-as-action instead of drawing. + + Returns card index to flip, or None to draw normally. + + Only available when flip_as_action house rule is enabled. + Conservative players may prefer this to avoid risky deck draws. + """ + if not game.options.flip_as_action: + return None + + # Find face-down cards + face_down = [(i, c) for i, c in enumerate(player.cards) if not c.face_up] + if not face_down: + return None # No cards to flip + + # Check if discard has a good card we want - if so, don't use flip action + discard_top = game.discard_top() + if discard_top: + discard_value = get_ai_card_value(discard_top, game.options) + if discard_value <= 2: # Good card available + ai_log(f" Flip-as-action: skipping, good discard available ({discard_value})") + return None + + # Aggressive players prefer drawing (more action, chance to improve) + if profile.aggression > 0.6: + ai_log(f" Flip-as-action: skipping, too aggressive ({profile.aggression:.2f})") + return None + + # Consider flip action with probability based on personality + # Conservative players (low aggression) are more likely to use it + flip_chance = (1.0 - profile.aggression) * 0.25 # Max 25% for most conservative + + # Increase chance if we have many hidden cards (info is valuable) + if len(face_down) >= 4: + flip_chance *= 1.5 + + if random.random() > flip_chance: + return None + + ai_log(f" Flip-as-action: choosing to flip instead of draw") + + # Prioritize positions where column partner is visible (pair info) + for idx, card in face_down: + partner_idx = idx + 3 if idx < 3 else idx - 3 + if player.cards[partner_idx].face_up: + ai_log(f" Flipping position {idx} (partner visible)") + return idx + + # Random face-down card + choice = random.choice(face_down)[0] + ai_log(f" Flipping position {choice} (random)") + return choice + @staticmethod def should_go_out_early(player: Player, game: Game, profile: CPUProfile) -> bool: """ @@ -943,6 +1029,26 @@ async def process_cpu_turn( # Check if we should try to go out early GolfAI.should_go_out_early(cpu_player, game, profile) + # Check if we should use flip-as-action instead of drawing + flip_action_pos = GolfAI.should_use_flip_action(game, cpu_player, profile) + if flip_action_pos is not None: + if game.flip_card_as_action(cpu_player.id, flip_action_pos): + # Log flip-as-action + if logger and game_id: + flipped_card = cpu_player.cards[flip_action_pos] + logger.log_move( + game_id=game_id, + player=cpu_player, + is_cpu=True, + action="flip_as_action", + card=flipped_card, + position=flip_action_pos, + game=game, + decision_reason=f"used flip-as-action to reveal position {flip_action_pos}", + ) + await broadcast_callback() + return # Turn is over + # Decide whether to draw from discard or deck discard_top = game.discard_top() take_discard = GolfAI.should_take_discard(discard_top, cpu_player, profile, game) diff --git a/server/constants.py b/server/constants.py index 3031bcd..b2ee879 100644 --- a/server/constants.py +++ b/server/constants.py @@ -59,6 +59,14 @@ else: LUCKY_SWING_JOKER_VALUE: int = -5 # Single joker worth -5 +# ============================================================================= +# Bonus/Penalty Constants +# ============================================================================= + +WOLFPACK_BONUS: int = -20 # All 4 Jacks (2 pairs) bonus (was -5, buffed) +FOUR_OF_A_KIND_BONUS: int = -20 # Four equal cards in two columns bonus + + # ============================================================================= # Game Constants # ============================================================================= diff --git a/server/game.py b/server/game.py index ae36900..7d060d3 100644 --- a/server/game.py +++ b/server/game.py @@ -29,6 +29,8 @@ from constants import ( SUPER_KINGS_VALUE, TEN_PENNY_VALUE, LUCKY_SWING_JOKER_VALUE, + WOLFPACK_BONUS, + FOUR_OF_A_KIND_BONUS, ) @@ -110,6 +112,10 @@ def get_card_value(card: "Card", options: Optional["GameOptions"] = None) -> int return SUPER_KINGS_VALUE if card.rank == Rank.TEN and options.ten_penny: return TEN_PENNY_VALUE + # One-eyed Jacks: J♥ and J♠ are worth 0 instead of 10 + if options.one_eyed_jacks: + if card.rank == Rank.JACK and card.suit in (Suit.HEARTS, Suit.SPADES): + return 0 return RANK_VALUES[card.rank] @@ -301,6 +307,7 @@ class Player: total = 0 jack_pairs = 0 # Track paired Jacks for Wolfpack bonus + paired_ranks: list[Rank] = [] # Track all paired ranks for four-of-a-kind for col in range(3): top_idx = col @@ -310,6 +317,8 @@ class Player: # Check if column pair matches (same rank cancels out) if top_card.rank == bottom_card.rank: + paired_ranks.append(top_card.rank) + # Track Jack pairs for Wolfpack bonus if top_card.rank == Rank.JACK: jack_pairs += 1 @@ -320,6 +329,15 @@ class Player: total -= 4 continue + # Negative Pairs Keep Value: paired 2s/Jokers keep their negative value + if options and options.negative_pairs_keep_value: + top_val = get_card_value(top_card, options) + bottom_val = get_card_value(bottom_card, options) + if top_val < 0 or bottom_val < 0: + # Keep negative value instead of 0 + total += top_val + bottom_val + continue + # Normal matching pair: scores 0 (skip adding values) continue @@ -327,9 +345,18 @@ class Player: total += get_card_value(top_card, options) total += get_card_value(bottom_card, options) - # Wolfpack bonus: 2+ pairs of Jacks = -5 pts + # Wolfpack bonus: 2+ pairs of Jacks if options and options.wolfpack and jack_pairs >= 2: - total -= 5 + total += WOLFPACK_BONUS # -20 + + # Four of a Kind bonus: same rank appears twice in paired_ranks + # (meaning 4 cards of that rank across 2 columns) + if options and options.four_of_a_kind: + rank_counts = Counter(paired_ranks) + for rank, count in rank_counts.items(): + if count >= 2: + # Four of a kind! Apply bonus + total += FOUR_OF_A_KIND_BONUS # -20 self.score = total return total @@ -415,6 +442,19 @@ class GameOptions: eagle_eye: bool = False """Jokers worth +2 unpaired, -4 when paired (instead of -2/0).""" + # --- House Rules: New Variants (all OFF by default for classic gameplay) --- + flip_as_action: bool = False + """Allow using turn to flip a face-down card without drawing.""" + + four_of_a_kind: bool = False + """Four equal cards in two columns scores -20 points bonus.""" + + negative_pairs_keep_value: bool = False + """Paired 2s and Jokers keep their negative value (-4) instead of becoming 0.""" + + one_eyed_jacks: bool = False + """One-eyed Jacks (J♥ and J♠) are worth 0 points instead of 10.""" + @dataclass class Game: @@ -878,6 +918,45 @@ class Game: self._check_end_turn(player) return True + def flip_card_as_action(self, player_id: str, card_index: int) -> bool: + """ + Use turn to flip a face-down card without drawing. + + Only valid if flip_as_action house rule is enabled. + This is an alternative to drawing - player flips one of their + face-down cards to see what it is, then their turn ends. + + Args: + player_id: ID of the player using this action. + card_index: Index 0-5 of the card to flip. + + Returns: + True if action was valid and turn ended, False otherwise. + """ + if not self.options.flip_as_action: + return False + + player = self.current_player() + if not player or player.id != player_id: + return False + + if self.phase not in (GamePhase.PLAYING, GamePhase.FINAL_TURN): + return False + + # Can't use this action if already drawn a card + if self.drawn_card is not None: + return False + + if not (0 <= card_index < len(player.cards)): + return False + + if player.cards[card_index].face_up: + return False # Already face-up, can't flip + + player.cards[card_index].face_up = True + self._check_end_turn(player) + return True + # ------------------------------------------------------------------------- # Turn & Round Flow (Internal) # ------------------------------------------------------------------------- @@ -1089,6 +1168,15 @@ class Game: active_rules.append("Blackjack") if self.options.wolfpack: active_rules.append("Wolfpack") + # New house rules + if self.options.flip_as_action: + active_rules.append("Flip as Action") + if self.options.four_of_a_kind: + active_rules.append("Four of a Kind") + if self.options.negative_pairs_keep_value: + active_rules.append("Negative Pairs Keep Value") + if self.options.one_eyed_jacks: + active_rules.append("One-Eyed Jacks") return { "phase": self.phase.value, @@ -1108,6 +1196,7 @@ class Game: "flip_on_discard": self.flip_on_discard, "flip_mode": self.options.flip_mode, "flip_is_optional": self.flip_is_optional, + "flip_as_action": self.options.flip_as_action, "card_values": self.get_card_values(), "active_rules": active_rules, } diff --git a/server/main.py b/server/main.py index 681a025..7e23a0d 100644 --- a/server/main.py +++ b/server/main.py @@ -575,6 +575,11 @@ async def websocket_endpoint(websocket: WebSocket): blackjack=data.get("blackjack", False), eagle_eye=data.get("eagle_eye", False), wolfpack=data.get("wolfpack", False), + # House Rules - New Variants + flip_as_action=data.get("flip_as_action", False), + four_of_a_kind=data.get("four_of_a_kind", False), + negative_pairs_keep_value=data.get("negative_pairs_keep_value", False), + one_eyed_jacks=data.get("one_eyed_jacks", False), ) # Validate settings @@ -688,6 +693,15 @@ async def websocket_endpoint(websocket: WebSocket): await broadcast_game_state(current_room) await check_and_run_cpu_turn(current_room) + elif msg_type == "flip_as_action": + if not current_room: + continue + + position = data.get("position", 0) + if current_room.game.flip_card_as_action(player_id, position): + await broadcast_game_state(current_room) + await check_and_run_cpu_turn(current_room) + elif msg_type == "next_round": if not current_room: continue