Additional house rules to accomodate more common game variants.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee 2026-01-26 20:13:30 -05:00
parent 23657f6b0c
commit 33e3f124ed
6 changed files with 447 additions and 48 deletions

View File

@ -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('');
}

View File

@ -104,7 +104,7 @@
<select id="flip-mode">
<option value="never">Standard - No flip after discarding</option>
<option value="always">Speed Golf - MUST flip a card after discarding</option>
<option value="endgame">Endgame - Flip after discard if a player has 1 hidden card left</option>
<option value="endgame">Endgame - Optional flip to help trailing players catch up</option>
</select>
<span class="rule-desc">What happens when you draw from deck and discard</span>
</div>
@ -186,7 +186,33 @@
<label class="checkbox-label inline">
<input type="checkbox" id="wolfpack">
<span>Wolfpack</span>
<span class="rule-desc">2 pairs of Jacks = -5 pts</span>
<span class="rule-desc">All 4 Jacks = -20 pts</span>
</label>
</div>
</div>
<div class="options-category">
<h4>New Variants</h4>
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" id="flip-as-action">
<span>Flip as Action</span>
<span class="rule-desc">Use turn to flip a card without drawing</span>
</label>
<label class="checkbox-label">
<input type="checkbox" id="four-of-a-kind">
<span>Four of a Kind</span>
<span class="rule-desc">4 matching cards in 2 columns = -20 pts</span>
</label>
<label class="checkbox-label">
<input type="checkbox" id="negative-pairs-keep-value">
<span>Negative Pairs Keep Value</span>
<span class="rule-desc">Paired 2s/Jokers stay at -4 (not 0)</span>
</label>
<label class="checkbox-label">
<input type="checkbox" id="one-eyed-jacks">
<span>One-Eyed Jacks</span>
<span class="rule-desc">J♥ and J♠ worth 0 pts</span>
</label>
</div>
</div>
@ -296,8 +322,47 @@
<!-- Rules Screen -->
<div id="rules-screen" class="screen">
<div class="rules-container">
<button id="rules-back-btn" class="btn btn-secondary back-btn">← Back</button>
<h1>Game Rules</h1>
<button id="rules-back-btn" class="btn rules-back-btn">« Back</button>
<div class="rules-header">
<h1><span class="golfer-logo">🏌️</span> <span class="golf-title">Golf Rules</span></h1>
<p class="rules-subtitle">6-Card Golf Card Game - Complete Guide</p>
</div>
<!-- Table of Contents -->
<nav class="rules-toc">
<div class="toc-title">Quick Navigation</div>
<div class="toc-links">
<a href="#rules-basic" class="toc-link">
<span class="toc-icon">🎯</span>
<span class="toc-text">Basic Rules</span>
</a>
<a href="#rules-card-values" class="toc-link">
<span class="toc-icon">🃏</span>
<span class="toc-text">Card Values</span>
</a>
<a href="#rules-pairing" class="toc-link">
<span class="toc-icon">👯</span>
<span class="toc-text">Column Pairing</span>
</a>
<a href="#rules-turn" class="toc-link">
<span class="toc-icon">🔄</span>
<span class="toc-text">Turn Structure</span>
</a>
<a href="#rules-flip-mode" class="toc-link">
<span class="toc-icon">🔃</span>
<span class="toc-text">Flip Modes</span>
</a>
<a href="#rules-house-rules" class="toc-link">
<span class="toc-icon">🏠</span>
<span class="toc-text">House Rules</span>
</a>
<a href="#rules-faq" class="toc-link">
<span class="toc-icon"></span>
<span class="toc-text">FAQ</span>
</a>
</div>
</nav>
<section id="rules-basic" class="rules-section">
<h2>Basic Rules</h2>
@ -331,24 +396,24 @@
<section id="rules-pairing" class="rules-section">
<h2>Column Pairing (IMPORTANT!)</h2>
<p><strong>This is the most important rule to understand:</strong></p>
<p>If both cards in a <strong>vertical column</strong> have the <strong>same rank</strong> (like two Kings, or two 7s), that entire column scores <strong>0 points</strong> - regardless of what the cards are worth individually!</p>
<p>If both cards in a <strong>vertical column</strong> have the <strong>same rank</strong> (like two 8s or two Jacks), that entire column scores <strong>0 points</strong> - regardless of what the cards are worth individually!</p>
<div class="rules-example">
<h4>Example:</h4>
<pre>
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</pre>
</div>
<p class="rules-warning"><strong>IMPORTANT:</strong> 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).</p>
<p class="rules-warning"><strong>IMPORTANT:</strong> 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). <em>Exception: The "Negative Pairs Keep Value" house rule changes this - paired negative cards keep their -4 value!</em></p>
</section>
<section id="rules-turn" class="rules-section">
@ -386,55 +451,129 @@ TOTAL: 0 + 8 + 16 = 24 points</pre>
<h3>Standard Mode (No Flip)</h3>
<p class="mode-summary">Default setting. Discarding ends your turn immediately.</p>
<p><strong>How it works:</strong> When you draw from the deck and decide not to use it, you simply discard it and your turn is over. Nothing else happens.</p>
<p><strong>Best for:</strong> Traditional gameplay, longer games, maximum hidden information.</p>
<p><strong>Strategic impact:</strong> 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.</p>
<p><strong>Best for:</strong> Traditional gameplay, longer games, players who enjoy mystery and risk.</p>
</div>
<div class="rules-mode">
<h3>Speed Golf Mode (Must Flip)</h3>
<p class="mode-summary">Every discard reveals one of your hidden cards.</p>
<p><strong>How it works:</strong> 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.</p>
<p><strong>Why use it:</strong> Games go much faster because more cards get revealed every turn. More information for everyone = more strategic decisions.</p>
<p><strong>Best for:</strong> Quick games, players who like faster-paced action.</p>
<p><strong>Strategic impact:</strong> Even "bad" draws give you information. Reduces the luck factor since everyone makes more informed decisions. Games naturally end faster with less hidden information.</p>
<p><strong>Best for:</strong> Quick games, players who prefer skill over luck.</p>
</div>
<div class="rules-mode">
<h3>Endgame Mode (Flip When Close to Finishing)</h3>
<p class="mode-summary">Flip activates when any player has only 1 hidden card remaining.</p>
<h3>Endgame Mode (Catch-Up Flip)</h3>
<p class="mode-summary">Optional flip activates when any player has only 1 hidden card left.</p>
<p><strong>How it works:</strong></p>
<ul>
<li>Early in the round: Discarding ends your turn (like Standard mode)</li>
<li><strong>When ANY player has 1 or fewer face-down cards:</strong> After discarding, you MAY choose to flip one of your hidden cards OR skip the flip</li>
<li><strong>When ANY player has 1 or fewer face-down cards:</strong> After discarding, you MAY choose to flip one of your hidden cards OR skip</li>
</ul>
<p><strong>Why use it:</strong> 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?</p>
<p><strong>Best for:</strong> Players who enjoy dramatic finishes and tough end-game decisions.</p>
<p><strong>Strategic impact:</strong> This is a <strong>catch-up mechanic</strong>. 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.</p>
<p><strong>Best for:</strong> Competitive play where you want trailing players to have a fighting chance.</p>
</div>
</section>
<section id="rules-house-rules" class="rules-section">
<h2>House Rules (Optional Variants)</h2>
<h3>Point Modifiers</h3>
<ul>
<li><strong>Super Kings:</strong> Kings are worth -2 points instead of 0 (makes them even better!)</li>
<li><strong>Ten Penny:</strong> 10s are worth only 1 point instead of 10 (makes 10s less scary)</li>
</ul>
<div class="rules-mode">
<h3>Point Modifiers</h3>
<div class="house-rule">
<h4>Super Kings</h4>
<p>Kings are worth <strong>-2 points</strong> instead of 0.</p>
<p class="strategic-impact"><strong>Strategic impact:</strong> 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?</p>
</div>
<div class="house-rule">
<h4>Ten Penny</h4>
<p>10s are worth <strong>1 point</strong> instead of 10.</p>
<p class="strategic-impact"><strong>Strategic impact:</strong> 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.</p>
</div>
</div>
<h3>Joker Variants</h3>
<ul>
<li><strong>Standard Jokers:</strong> 2 Jokers per deck, each worth -2 points (paired Jokers = 0 points)</li>
<li><strong>Lucky Swing:</strong> Only 1 Joker in the entire deck, but it's worth -5 points! (Rare and powerful)</li>
<li><strong>Eagle Eye:</strong> Jokers are worth +2 points unpaired, but -4 points when paired (rewards finding both Jokers)</li>
</ul>
<div class="rules-mode">
<h3>Joker Variants</h3>
<div class="house-rule">
<h4>Standard Jokers</h4>
<p>2 Jokers per deck, each worth <strong>-2 points</strong>.</p>
<p class="strategic-impact"><strong>Strategic impact:</strong> 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.</p>
</div>
<div class="house-rule">
<h4>Lucky Swing</h4>
<p>Only <strong>1 Joker</strong> in the entire deck, worth <strong>-5 points</strong>.</p>
<p class="strategic-impact"><strong>Strategic impact:</strong> High variance. Whoever finds this rare card gets a significant advantage. Increases the luck factor - sometimes you get it, sometimes your opponent does.</p>
</div>
<div class="house-rule">
<h4>Eagle Eye</h4>
<p>Jokers are worth <strong>+2 unpaired</strong>, but <strong>-4 when paired</strong>.</p>
<p class="strategic-impact"><strong>Strategic impact:</strong> 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.</p>
</div>
</div>
<h3>Bonuses & Penalties</h3>
<ul>
<li><strong>Knock Penalty:</strong> If you "go out" (reveal all cards first) but DON'T have the lowest score, you get +10 penalty points. Risk vs reward!</li>
<li><strong>Knock Bonus:</strong> Get -5 points (subtracted from your score) for going out first.</li>
<li><strong>Underdog Bonus:</strong> The player with the lowest score each hole gets -3 points.</li>
<li><strong>Tied Shame:</strong> If you tie with another player's score, both of you get +5 penalty points.</li>
<li><strong>Blackjack:</strong> If your exact score is 21, it becomes 0 instead!</li>
<li><strong>Wolfpack:</strong> If you have exactly 2 pairs of Jacks (all 4 Jacks), you get -5 bonus points.</li>
</ul>
<div class="rules-mode">
<h3>Going Out Rules</h3>
<div class="house-rule">
<h4>Knock Penalty</h4>
<p><strong>+10 points</strong> if you go out but don't have the lowest score.</p>
<p class="strategic-impact"><strong>Strategic impact:</strong> 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.</p>
</div>
<div class="house-rule">
<h4>Knock Bonus</h4>
<p><strong>-5 points</strong> for going out first (regardless of who wins).</p>
<p class="strategic-impact"><strong>Strategic impact:</strong> Encourages racing to finish, even with a mediocre hand. The 5-point bonus might make up for a slightly worse score. Speeds up gameplay.</p>
</div>
<p class="combo-note"><em>Combining Knock Penalty + Knock Bonus creates high-stakes "going out" decisions: -5 if you win, +10 if you lose!</em></p>
</div>
<div class="rules-mode">
<h3>Scoring Bonuses</h3>
<div class="house-rule">
<h4>Underdog Bonus</h4>
<p>Round winner gets <strong>-3 points</strong> extra.</p>
<p class="strategic-impact"><strong>Strategic impact:</strong> Amplifies winning - the best player each round pulls further ahead. Can lead to snowballing leads over multiple holes. Rewards consistency.</p>
</div>
<div class="house-rule">
<h4>Tied Shame</h4>
<p>If you tie another player's score, <strong>both get +5 penalty</strong>.</p>
<p class="strategic-impact"><strong>Strategic impact:</strong> Punishes playing it safe. If you suspect a tie, you need to take risks to differentiate your score. Creates interesting late-round decisions.</p>
</div>
<div class="house-rule">
<h4>Blackjack</h4>
<p>Score of exactly <strong>21 becomes 0</strong>.</p>
<p class="strategic-impact"><strong>Strategic impact:</strong> 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.</p>
</div>
<div class="house-rule">
<h4>Wolfpack</h4>
<p>Having <strong>all 4 Jacks</strong> (2 pairs) gives <strong>-20 bonus</strong>.</p>
<p class="strategic-impact"><strong>Strategic impact:</strong> 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.</p>
</div>
</div>
<div class="rules-mode">
<h3>New Variants</h3>
<div class="house-rule">
<h4>Flip as Action</h4>
<p>Use your turn to flip one of your face-down cards without drawing. Ends your turn immediately.</p>
<p class="strategic-impact"><strong>Strategic impact:</strong> 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.</p>
</div>
<div class="house-rule">
<h4>Four of a Kind</h4>
<p>Having 4 cards of the same rank across two columns scores <strong>-20 bonus</strong>.</p>
<p class="strategic-impact"><strong>Strategic impact:</strong> 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.</p>
</div>
<div class="house-rule">
<h4>Negative Pairs Keep Value</h4>
<p>When you pair 2s or Jokers in a column, they keep their combined <strong>-4 points</strong> instead of becoming 0.</p>
<p class="strategic-impact"><strong>Strategic impact:</strong> 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.</p>
</div>
<div class="house-rule">
<h4>One-Eyed Jacks</h4>
<p>The Jack of Hearts (J♥) and Jack of Spades (J♠) - the "one-eyed" Jacks - are worth <strong>0 points</strong> instead of 10.</p>
<p class="strategic-impact"><strong>Strategic impact:</strong> 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.</p>
</div>
</div>
</section>
<section id="rules-faq" class="rules-section">
@ -481,8 +620,13 @@ TOTAL: 0 + 8 + 16 = 24 points</pre>
</div>
<div class="faq-item">
<h4>Q: Why would I NOT flip in Endgame mode?</h4>
<p>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!</p>
<h4>Q: How does Endgame mode help trailing players?</h4>
<p>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.</p>
</div>
<div class="faq-item">
<h4>Q: Why would I skip the flip in Endgame mode?</h4>
<p>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!</p>
</div>
</section>
</div>

View File

@ -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)

View File

@ -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
# =============================================================================

View File

@ -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,
}

View File

@ -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