diff --git a/client/app.js b/client/app.js
index 9919bf2..c178a08 100644
--- a/client/app.js
+++ b/client/app.js
@@ -139,7 +139,6 @@ class GolfGame {
this.currentRoundSpan = document.getElementById('current-round');
this.totalRoundsSpan = document.getElementById('total-rounds');
this.turnInfo = document.getElementById('turn-info');
- this.leaderInfo = document.getElementById('leader-info');
this.yourScore = document.getElementById('your-score');
this.muteBtn = document.getElementById('mute-btn');
this.opponentsRow = document.getElementById('opponents-row');
@@ -154,6 +153,7 @@ class GolfGame {
this.toast = document.getElementById('toast');
this.scoreboard = document.getElementById('scoreboard');
this.scoreTable = document.getElementById('score-table').querySelector('tbody');
+ this.standingsList = document.getElementById('standings-list');
this.gameButtons = document.getElementById('game-buttons');
this.nextRoundBtn = document.getElementById('next-round-btn');
this.newGameBtn = document.getElementById('new-game-btn');
@@ -739,6 +739,43 @@ class GolfGame {
return suit === 'hearts' || suit === 'diamonds';
}
+ calculateShowingScore(cards) {
+ if (!cards || cards.length !== 6) return 0;
+
+ // Use card values from server (includes house rules) or defaults
+ const cardValues = this.gameState?.card_values || {
+ 'A': 1, '2': -2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7,
+ '8': 8, '9': 9, '10': 10, 'J': 10, 'Q': 10, 'K': 0, '★': -2
+ };
+
+ const getCardValue = (card) => {
+ if (!card.face_up) return 0;
+ return cardValues[card.rank] ?? 0;
+ };
+
+ // Check for column pairs (cards in same column cancel out if matching)
+ let total = 0;
+ for (let col = 0; col < 3; col++) {
+ const topCard = cards[col];
+ const bottomCard = cards[col + 3];
+
+ const topUp = topCard.face_up;
+ const bottomUp = bottomCard.face_up;
+
+ // If both face up and matching rank, they cancel (score 0)
+ if (topUp && bottomUp && topCard.rank === bottomCard.rank) {
+ // Matching pair = 0 points for both
+ continue;
+ }
+
+ // Otherwise add individual values
+ total += getCardValue(topCard);
+ total += getCardValue(bottomCard);
+ }
+
+ return total;
+ }
+
getSuitSymbol(suit) {
const symbols = {
hearts: '♥',
@@ -777,19 +814,12 @@ class GolfGame {
}
}
- // Update leader info (by total score)
- const sortedByTotal = [...this.gameState.players].sort((a, b) => a.total_score - b.total_score);
- const leader = sortedByTotal[0];
- if (leader && this.gameState.current_round > 1) {
- this.leaderInfo.textContent = `Leader: ${leader.name} (${leader.total_score})`;
- } else {
- this.leaderInfo.textContent = "";
- }
-
- // Update your score
+ // Update your score (points currently showing on your cards)
const me = this.gameState.players.find(p => p.id === this.playerId);
if (me) {
- this.yourScore.textContent = me.round_score ?? 0;
+ // Calculate visible score from face-up cards
+ const showingScore = this.calculateShowingScore(me.cards);
+ this.yourScore.textContent = showingScore;
}
// Update discard pile
@@ -833,7 +863,7 @@ class GolfGame {
div.classList.add('current-turn');
}
- const displayName = player.name.length > 8 ? player.name.substring(0, 7) + '…' : player.name;
+ const displayName = player.name.length > 12 ? player.name.substring(0, 11) + '…' : player.name;
div.innerHTML = `
${displayName}${player.all_face_up ? ' ✓' : ''}
@@ -895,6 +925,10 @@ class GolfGame {
updateScorePanel() {
if (!this.gameState) return;
+ // Update standings (left panel)
+ this.updateStandings();
+
+ // Update score table (right panel)
this.scoreTable.innerHTML = '';
this.gameState.players.forEach(player => {
@@ -906,8 +940,8 @@ class GolfGame {
}
// Truncate long names
- const displayName = player.name.length > 10
- ? player.name.substring(0, 9) + '…'
+ const displayName = player.name.length > 12
+ ? player.name.substring(0, 11) + '…'
: player.name;
const roundScore = player.score !== null ? player.score : '-';
@@ -923,6 +957,39 @@ class GolfGame {
});
}
+ updateStandings() {
+ if (!this.gameState || !this.standingsList) return;
+
+ // Sort players by total score (lowest is best in golf)
+ const sorted = [...this.gameState.players].sort((a, b) => a.total_score - b.total_score);
+
+ this.standingsList.innerHTML = '';
+
+ sorted.forEach((player, index) => {
+ const div = document.createElement('div');
+ div.className = 'standing-row';
+
+ if (index === 0 && player.total_score < sorted[sorted.length - 1]?.total_score) {
+ div.classList.add('leader');
+ }
+ if (player.id === this.playerId) {
+ div.classList.add('you');
+ }
+
+ const displayName = player.name.length > 12
+ ? player.name.substring(0, 11) + '…'
+ : player.name;
+
+ div.innerHTML = `
+ ${index + 1}.
+ ${displayName}
+ ${player.total_score} pts
+ `;
+
+ this.standingsList.appendChild(div);
+ });
+ }
+
renderCard(card, clickable, selected) {
let classes = 'card';
let content = '';
@@ -959,8 +1026,8 @@ class GolfGame {
const roundsWon = score.rounds_won || 0;
// Truncate long names
- const displayName = score.name.length > 10
- ? score.name.substring(0, 9) + '…'
+ const displayName = score.name.length > 12
+ ? score.name.substring(0, 11) + '…'
: score.name;
if (total === minScore) {
@@ -1023,7 +1090,7 @@ class GolfGame {
prevPoints = p.total;
}
const medal = pointsRank === 0 ? '🥇' : pointsRank === 1 ? '🥈' : pointsRank === 2 ? '🥉' : `${pointsRank + 1}.`;
- const name = p.name.length > 8 ? p.name.substring(0, 7) + '…' : p.name;
+ const name = p.name.length > 12 ? p.name.substring(0, 11) + '…' : p.name;
return `${medal}${name}${p.total}pt
`;
}).join('');
@@ -1038,7 +1105,7 @@ class GolfGame {
// No medal for 0 wins
const medal = p.rounds_won === 0 ? '-' :
holesRank === 0 ? '🥇' : holesRank === 1 ? '🥈' : holesRank === 2 ? '🥉' : `${holesRank + 1}.`;
- const name = p.name.length > 8 ? p.name.substring(0, 7) + '…' : p.name;
+ const name = p.name.length > 12 ? p.name.substring(0, 11) + '…' : p.name;
return `${medal}${name}${p.rounds_won}W
`;
}).join('');
diff --git a/client/index.html b/client/index.html
index e6f87f1..dfa8300 100644
--- a/client/index.html
+++ b/client/index.html
@@ -15,7 +15,7 @@
-
+
@@ -192,8 +192,7 @@
@@ -227,7 +226,14 @@
-
+
+
+
+
+
Scores
diff --git a/client/style.css b/client/style.css
index 8fdfb9b..8f2573e 100644
--- a/client/style.css
+++ b/client/style.css
@@ -280,29 +280,35 @@ input::placeholder {
/* Game Screen */
.game-header {
- display: flex;
- justify-content: center;
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr 1fr;
align-items: center;
- gap: clamp(15px, 3vw, 40px);
- padding: 6px 15px;
- background: rgba(0,0,0,0.2);
- border-radius: 6px;
- font-size: 0.85rem;
- width: calc(100vw - 250px);
- margin-left: 10px;
+ padding: 10px 25px;
+ background: rgba(0,0,0,0.35);
+ border-radius: 0;
+ font-size: 0.9rem;
+ width: 100vw;
+ margin-left: calc(-50vw + 50%);
+ box-sizing: border-box;
+}
+
+.game-header .round-info {
+ justify-self: start;
+ font-weight: 600;
}
.game-header .turn-info {
+ justify-self: center;
font-weight: 600;
color: #f4a460;
}
-.game-header .leader-info {
- opacity: 0.9;
+.game-header .score-info {
+ justify-self: center;
}
-.game-header .score-info {
- opacity: 0.9;
+.game-header .mute-btn {
+ justify-self: end;
}
.mute-btn {
@@ -323,13 +329,13 @@ input::placeholder {
/* Card Styles */
.card {
- width: clamp(60px, 6vw, 80px);
- height: clamp(84px, 8.4vw, 112px);
+ width: clamp(65px, 5.5vw, 100px);
+ height: clamp(91px, 7.7vw, 140px);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
- font-size: clamp(1.2rem, 1.4vw, 1.7rem);
+ font-size: clamp(2rem, 2.5vw, 3.2rem);
font-weight: bold;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
@@ -386,8 +392,8 @@ input::placeholder {
/* Card Grid */
.card-grid {
display: grid;
- grid-template-columns: repeat(3, clamp(60px, 6vw, 80px));
- gap: clamp(6px, 0.8vw, 12px);
+ grid-template-columns: repeat(3, clamp(65px, 5.5vw, 100px));
+ gap: clamp(8px, 0.8vw, 14px);
justify-content: center;
}
@@ -415,11 +421,10 @@ input::placeholder {
flex-wrap: nowrap;
justify-content: center;
align-items: flex-end;
- gap: clamp(10px, 2vw, 30px);
- min-height: clamp(100px, 12vw, 150px);
- padding: 0 10px;
- width: calc(100vw - 240px);
- margin-left: 10px;
+ gap: clamp(12px, 1.8vw, 35px);
+ min-height: clamp(120px, 14vw, 200px);
+ padding: 0 20px;
+ width: 100%;
}
/* Arch layout - middle items higher, edges lower with rotation for "around the table" feel */
@@ -566,15 +571,15 @@ input::placeholder {
/* Opponent Areas */
.opponent-area {
background: rgba(0,0,0,0.2);
- border-radius: 6px;
- padding: clamp(3px, 0.4vw, 6px) clamp(5px, 0.6vw, 10px) clamp(5px, 0.6vw, 10px);
+ border-radius: 8px;
+ padding: clamp(4px, 0.5vw, 10px) clamp(6px, 0.7vw, 14px) clamp(6px, 0.7vw, 14px);
text-align: center;
}
.opponent-area h4 {
- font-size: clamp(0.65rem, 0.75vw, 0.85rem);
- margin: 0 0 4px 0;
- padding: clamp(2px, 0.3vw, 4px) clamp(6px, 0.8vw, 10px);
+ font-size: clamp(0.7rem, 0.8vw, 1rem);
+ margin: 0 0 6px 0;
+ padding: clamp(3px, 0.35vw, 6px) clamp(8px, 0.9vw, 14px);
background: rgba(244, 164, 96, 0.5);
border-radius: 4px;
white-space: nowrap;
@@ -584,15 +589,15 @@ input::placeholder {
.opponent-area .card-grid {
display: grid;
- grid-template-columns: repeat(3, clamp(40px, 4.5vw, 60px));
- gap: clamp(3px, 0.4vw, 6px);
+ grid-template-columns: repeat(3, clamp(45px, 4vw, 75px));
+ gap: clamp(4px, 0.4vw, 8px);
}
.opponent-area .card {
- width: clamp(40px, 4.5vw, 60px);
- height: clamp(56px, 6.3vw, 84px);
- font-size: clamp(0.75rem, 0.9vw, 1.1rem);
- border-radius: 4px;
+ width: clamp(45px, 4vw, 75px);
+ height: clamp(63px, 5.6vw, 105px);
+ font-size: clamp(1.3rem, 1.5vw, 2.2rem);
+ border-radius: 5px;
}
.opponent-area.current-turn {
@@ -647,13 +652,14 @@ input::placeholder {
#game-screen.active {
display: flex;
flex-direction: column;
- align-items: flex-start;
+ align-items: center;
position: relative;
+ padding: 10px;
}
.game-layout {
display: flex;
- justify-content: flex-start;
+ justify-content: center;
width: 100%;
align-items: flex-start;
}
@@ -664,86 +670,136 @@ input::placeholder {
flex-direction: column;
align-items: center;
gap: 8px;
- width: calc(100vw - 230px);
+ width: 100%;
}
-/* Scoreboard Panel - positioned as overlay with cool styling */
-.scoreboard-panel {
+/* Side Panels - positioned in bottom corners */
+.side-panel {
position: fixed;
- top: 15px;
- right: 15px;
- background: linear-gradient(145deg, rgba(20, 60, 40, 0.95) 0%, rgba(10, 35, 25, 0.98) 100%);
- border-radius: 12px;
- padding: 12px;
- width: 210px;
- flex-shrink: 0;
- overflow: hidden;
+ bottom: 20px;
+ background: linear-gradient(145deg, rgba(15, 50, 35, 0.92) 0%, rgba(8, 30, 20, 0.95) 100%);
+ border-radius: 14px;
+ padding: 16px 18px;
+ width: 235px;
z-index: 100;
- backdrop-filter: blur(8px);
- border: 1px solid rgba(244, 164, 96, 0.3);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(244, 164, 96, 0.25);
box-shadow:
- 0 4px 20px rgba(0, 0, 0, 0.4),
- 0 0 30px rgba(244, 164, 96, 0.1),
- inset 0 1px 0 rgba(255, 255, 255, 0.1);
+ 0 4px 24px rgba(0, 0, 0, 0.5),
+ 0 0 40px rgba(244, 164, 96, 0.08),
+ inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
-.scoreboard-panel > h4 {
+.side-panel.left-panel {
+ left: 20px;
+}
+
+.side-panel.right-panel {
+ right: 20px;
+}
+
+.side-panel > h4 {
font-size: 0.9rem;
text-align: center;
- margin-bottom: 10px;
+ margin-bottom: 12px;
color: #f4a460;
text-transform: uppercase;
- letter-spacing: 0.15em;
+ letter-spacing: 0.2em;
font-weight: 700;
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
+ text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
+ border-bottom: 1px solid rgba(244, 164, 96, 0.2);
+ padding-bottom: 10px;
}
-.scoreboard-panel table {
+/* Standings list */
+.standings-list {
+ font-size: 0.95rem;
+}
+
+.standings-list .standing-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 6px 8px;
+ border-radius: 5px;
+ margin-bottom: 4px;
+}
+
+.standings-list .standing-row.leader {
+ background: rgba(244, 164, 96, 0.2);
+ border-left: 3px solid #f4a460;
+}
+
+.standings-list .standing-row.you {
+ background: rgba(255, 255, 255, 0.1);
+}
+
+.standings-list .standing-pos {
+ font-weight: 700;
+ color: #f4a460;
+ width: 20px;
+}
+
+.standings-list .standing-name {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.standings-list .standing-score {
+ font-weight: 600;
+ opacity: 0.9;
+}
+
+/* Score table */
+.side-panel table {
width: 100%;
border-collapse: collapse;
- font-size: 0.8rem;
+ font-size: 0.9rem;
}
-.scoreboard-panel th,
-.scoreboard-panel td {
- padding: 6px 5px;
+.side-panel th,
+.side-panel td {
+ padding: 7px 5px;
text-align: center;
- border-bottom: 1px solid rgba(255,255,255,0.1);
+ border-bottom: 1px solid rgba(255,255,255,0.08);
}
-.scoreboard-panel th {
+.side-panel th {
font-weight: 600;
- background: rgba(0,0,0,0.3);
- font-size: 0.7rem;
+ background: rgba(0,0,0,0.25);
+ font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
- color: rgba(255, 255, 255, 0.7);
+ color: rgba(255, 255, 255, 0.6);
}
-.scoreboard-panel td:first-child {
+.side-panel td:first-child {
text-align: left;
font-weight: 500;
}
-.scoreboard-panel tr.winner {
- background: linear-gradient(90deg, rgba(244, 164, 96, 0.4) 0%, rgba(244, 164, 96, 0.2) 100%);
+.side-panel tr.winner {
+ background: linear-gradient(90deg, rgba(244, 164, 96, 0.35) 0%, rgba(244, 164, 96, 0.15) 100%);
}
-.scoreboard-panel tr.current-player {
- background: rgba(244, 164, 96, 0.2);
+.side-panel tr.current-player {
+ background: rgba(244, 164, 96, 0.15);
box-shadow: inset 3px 0 0 #f4a460;
}
.game-buttons {
- margin-top: 8px;
+ margin-top: 10px;
display: flex;
flex-direction: column;
- gap: 5px;
+ gap: 6px;
}
.game-buttons .btn {
- font-size: 0.75rem;
- padding: 6px 10px;
+ font-size: 0.7rem;
+ padding: 8px 10px;
+ width: 100%;
}
/* Rankings Announcement */
diff --git a/server/ai.py b/server/ai.py
index 46cca44..3a1c5e8 100644
--- a/server/ai.py
+++ b/server/ai.py
@@ -6,20 +6,16 @@ from dataclasses import dataclass
from typing import Optional
from enum import Enum
-from game import Card, Player, Game, GamePhase, GameOptions, RANK_VALUES, Rank
+from game import Card, Player, Game, GamePhase, GameOptions, RANK_VALUES, Rank, get_card_value
+# Alias for backwards compatibility - use the centralized function from game.py
def get_ai_card_value(card: Card, options: GameOptions) -> int:
- """Get card value with house rules applied for AI decisions."""
- if card.rank == Rank.JOKER:
- return -5 if options.lucky_swing else -2
- if card.rank == Rank.KING and options.super_kings:
- return -2
- if card.rank == Rank.SEVEN and options.lucky_sevens:
- return 0
- if card.rank == Rank.TEN and options.ten_penny:
- return 0
- return card.value()
+ """Get card value with house rules applied for AI decisions.
+
+ This is an alias for game.get_card_value() for backwards compatibility.
+ """
+ return get_card_value(card, options)
def can_make_pair(card1: Card, card2: Card, options: GameOptions) -> bool:
diff --git a/server/constants.py b/server/constants.py
new file mode 100644
index 0000000..8501b79
--- /dev/null
+++ b/server/constants.py
@@ -0,0 +1,53 @@
+# Card values - Single source of truth for all card scoring
+# Per RULES.md: A=1, 2=-2, 3-10=face, J/Q=10, K=0, Joker=-2
+DEFAULT_CARD_VALUES = {
+ 'A': 1,
+ '2': -2,
+ '3': 3,
+ '4': 4,
+ '5': 5,
+ '6': 6,
+ '7': 7,
+ '8': 8,
+ '9': 9,
+ '10': 10,
+ 'J': 10,
+ 'Q': 10,
+ 'K': 0,
+ '★': -2, # Joker (standard)
+}
+
+# House rule modifications (per RULES.md House Rules section)
+SUPER_KINGS_VALUE = -2 # K worth -2 instead of 0
+LUCKY_SEVENS_VALUE = 0 # 7 worth 0 instead of 7
+TEN_PENNY_VALUE = 1 # 10 worth 1 instead of 10
+LUCKY_SWING_JOKER_VALUE = -5 # Joker worth -5 in Lucky Swing mode
+
+
+def get_card_value_for_rank(rank_str: str, options: dict | None = None) -> int:
+ """
+ Get point value for a card rank string, with house rules applied.
+
+ This is the single source of truth for card value calculations.
+ Use this for string-based rank lookups (e.g., from JSON/logs).
+
+ Args:
+ rank_str: Card rank as string ('A', '2', ..., 'K', '★')
+ options: Optional dict with house rule flags (lucky_swing, super_kings, etc.)
+
+ Returns:
+ Point value for the card
+ """
+ value = DEFAULT_CARD_VALUES.get(rank_str, 0)
+
+ if options:
+ if rank_str == '★' and options.get('lucky_swing'):
+ value = LUCKY_SWING_JOKER_VALUE
+ elif rank_str == 'K' and options.get('super_kings'):
+ value = SUPER_KINGS_VALUE
+ elif rank_str == '7' and options.get('lucky_sevens'):
+ value = LUCKY_SEVENS_VALUE
+ elif rank_str == '10' and options.get('ten_penny'):
+ value = TEN_PENNY_VALUE
+
+ return value
diff --git a/server/game.py b/server/game.py
index e0b7214..19edbc6 100644
--- a/server/game.py
+++ b/server/game.py
@@ -5,6 +5,14 @@ from dataclasses import dataclass, field
from typing import Optional
from enum import Enum
+from constants import (
+ DEFAULT_CARD_VALUES,
+ SUPER_KINGS_VALUE,
+ LUCKY_SEVENS_VALUE,
+ TEN_PENNY_VALUE,
+ LUCKY_SWING_JOKER_VALUE,
+)
+
class Suit(Enum):
HEARTS = "hearts"
@@ -30,22 +38,34 @@ class Rank(Enum):
JOKER = "★"
-RANK_VALUES = {
- Rank.ACE: 1,
- Rank.TWO: -2,
- Rank.THREE: 3,
- Rank.FOUR: 4,
- Rank.FIVE: 5,
- Rank.SIX: 6,
- Rank.SEVEN: 7,
- Rank.EIGHT: 8,
- Rank.NINE: 9,
- Rank.TEN: 10,
- Rank.JACK: 10,
- Rank.QUEEN: 10,
- Rank.KING: 0,
- Rank.JOKER: -2,
-}
+# Derive RANK_VALUES from DEFAULT_CARD_VALUES (single source of truth in constants.py)
+RANK_VALUES = {rank: DEFAULT_CARD_VALUES[rank.value] for rank in Rank}
+
+
+def get_card_value(card: "Card", options: Optional["GameOptions"] = None) -> int:
+ """
+ Get point value for a card, with house rules applied.
+
+ This is the single source of truth for Card object value calculations.
+ Use this instead of card.value() when house rules need to be considered.
+
+ Args:
+ card: Card object to evaluate
+ options: Optional GameOptions with house rule flags
+
+ Returns:
+ Point value for the card
+ """
+ if options:
+ if card.rank == Rank.JOKER:
+ 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]
@dataclass
@@ -128,19 +148,6 @@ class Player:
if len(self.cards) != 6:
return 0
- def get_card_value(card: Card) -> int:
- """Get card value with house rules applied."""
- if options:
- if card.rank == Rank.JOKER:
- return -5 if options.lucky_swing else -2
- if card.rank == Rank.KING and options.super_kings:
- return -2
- if card.rank == Rank.SEVEN and options.lucky_sevens:
- return 0
- if card.rank == Rank.TEN and options.ten_penny:
- return 1
- return card.value()
-
def cards_match(card1: Card, card2: Card) -> bool:
"""Check if two cards match for pairing (with Queens Wild support)."""
if card1.rank == card2.rank:
@@ -190,9 +197,9 @@ class Player:
continue
else:
if top_idx not in four_of_kind_positions:
- total += get_card_value(top_card)
+ total += get_card_value(top_card, options)
if bottom_idx not in four_of_kind_positions:
- total += get_card_value(bottom_card)
+ total += get_card_value(bottom_card, options)
self.score = total
return total
@@ -257,6 +264,22 @@ class Game:
def flip_on_discard(self) -> bool:
return self.options.flip_on_discard
+ def get_card_values(self) -> dict:
+ """Get current card values with house rules applied."""
+ values = DEFAULT_CARD_VALUES.copy()
+
+ # 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
+
+ return values
+
def add_player(self, player: Player) -> bool:
if len(self.players) >= 6:
return False
@@ -606,4 +629,5 @@ class Game:
),
"initial_flips": self.options.initial_flips,
"flip_on_discard": self.flip_on_discard,
+ "card_values": self.get_card_values(),
}
diff --git a/server/game_analyzer.py b/server/game_analyzer.py
index 8ce5139..a375a60 100644
--- a/server/game_analyzer.py
+++ b/server/game_analyzer.py
@@ -14,6 +14,7 @@ from typing import Optional
from enum import Enum
from game import Rank, RANK_VALUES, GameOptions
+from constants import get_card_value_for_rank
# =============================================================================
@@ -21,25 +22,12 @@ from game import Rank, RANK_VALUES, GameOptions
# =============================================================================
def get_card_value(rank: str, options: Optional[dict] = None) -> int:
- """Get point value for a card rank string."""
- rank_map = {
- 'A': 1, '2': -2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7,
- '8': 8, '9': 9, '10': 10, 'J': 10, 'Q': 10, 'K': 0, '★': -2
- }
- value = rank_map.get(rank, 0)
+ """Get point value for a card rank string.
- # Apply house rules if provided
- if options:
- if rank == '★' and options.get('lucky_swing'):
- value = -5
- if rank == 'K' and options.get('super_kings'):
- value = -2
- if rank == '7' and options.get('lucky_sevens'):
- value = 0
- if rank == '10' and options.get('ten_penny'):
- value = 1
-
- return value
+ This is a wrapper around constants.get_card_value_for_rank() for
+ backwards compatibility with existing analyzer code.
+ """
+ return get_card_value_for_rank(rank, options)
def rank_quality(rank: str, options: Optional[dict] = None) -> str: