- Extract WebSocket handlers from main.py into handlers.py - Add V3 feature docs (dealer rotation, dealing animation, round end reveal, column pair celebration, final turn urgency, opponent thinking, score tallying, card hover/selection, knock early drama, column pair indicator, swap animation improvements, draw source distinction, card value tooltips, active rules context, discard pile history, realistic card sounds) - Add V3 refactoring docs (ai.py, main.py/game.py, misc improvements) - Add installation guide with Docker, systemd, and nginx setup - Add helper scripts (install.sh, dev-server.sh, docker-build.sh) - Add animation flow diagrams documentation - Add test files for handlers, rooms, and V3 features - Add e2e test specs for V3 features - Update README with complete project structure and current tech stack - Update CLAUDE.md with full architecture tree and server layer descriptions - Update .env.example to reflect PostgreSQL (remove SQLite references) - Update .gitignore to exclude virtualenv files, .claude/, and .db files - Remove tracked virtualenv files (bin/, lib64, pyvenv.cfg) - Remove obsolete game_log.py (SQLite) and games.db Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
8.3 KiB
8.3 KiB
V3-14: Active Rules Context
Overview
The active rules bar shows which house rules are in effect, but doesn't highlight when a rule is relevant to the current action. This feature adds contextual highlighting to help players understand rule effects.
Dependencies: None Dependents: None
Goals
- Highlight relevant rules during specific actions
- Brief explanatory tooltip when rule affects play
- Help players learn how rules work
- Don't clutter the interface
- Fade after the moment passes
Current State
From app.js:
updateActiveRulesBar() {
const rules = this.gameState.active_rules || [];
if (rules.length === 0) {
this.activeRulesList.innerHTML = '<span class="rule-tag standard">Standard</span>';
} else {
// Show rule tags
}
}
Rules are listed but never highlighted contextually.
Design
Contextual Highlighting Moments
| Moment | Relevant Rule(s) | Highlight Text |
|---|---|---|
| Discard from deck | flip_mode: always | "Must flip a card!" |
| Player knocks | knock_penalty | "+10 if not lowest!" |
| Player knocks | knock_bonus | "-5 for going out first" |
| Pair negative cards | negative_pairs_keep_value | "Pairs keep -4!" |
| Draw Joker | lucky_swing | "Worth -5!" |
| Round end | underdog_bonus | "-3 for lowest score" |
| Score = 21 | blackjack | "Blackjack! Score → 0" |
| Four Jacks | wolfpack | "-20 Wolfpack bonus!" |
Visual Treatment
Normal: [Speed Golf] [Knock Penalty]
Highlighted: [Speed Golf ← Must flip!] [Knock Penalty]
↑
Pulsing, expanded
Implementation
Rule Highlight Method
highlightRule(ruleKey, message, duration = 3000) {
const ruleTag = this.activeRulesList.querySelector(
`[data-rule="${ruleKey}"]`
);
if (!ruleTag) return;
// Add highlight class
ruleTag.classList.add('rule-highlighted');
// Add message
const messageEl = document.createElement('span');
messageEl.className = 'rule-message';
messageEl.textContent = message;
ruleTag.appendChild(messageEl);
// Remove after duration
setTimeout(() => {
ruleTag.classList.remove('rule-highlighted');
messageEl.remove();
}, duration);
}
Integration Points
// In handleMessage or state change handlers
// 1. Speed Golf - must flip after discard
case 'can_flip':
if (!data.optional && this.gameState.flip_mode === 'always') {
this.highlightRule('flip_mode', 'Must flip a card!');
}
break;
// 2. Knock penalty warning
knockEarly() {
if (this.gameState.knock_penalty) {
this.highlightRule('knock_penalty', '+10 if not lowest!', 4000);
}
// ... rest of knock logic
}
// 3. Lucky swing Joker
case 'card_drawn':
if (data.card.rank === '★' && this.gameState.lucky_swing) {
this.highlightRule('lucky_swing', 'Worth -5!');
}
break;
// 4. Blackjack at round end
showScoreboard(scores, isFinal, rankings) {
// Check for blackjack
for (const [playerId, score] of Object.entries(scores)) {
if (score === 0 && this.wasOriginallyBlackjack(playerId)) {
this.highlightRule('blackjack', 'Blackjack! 21 → 0');
}
}
// ... rest of scoreboard logic
}
Update Rule Rendering
Add data attributes for targeting:
updateActiveRulesBar() {
const rules = this.gameState.active_rules || [];
if (rules.length === 0) {
this.activeRulesList.innerHTML = '<span class="rule-tag standard">Standard</span>';
return;
}
this.activeRulesList.innerHTML = rules
.map(rule => {
const key = this.getRuleKey(rule);
return `<span class="rule-tag" data-rule="${key}">${rule}</span>`;
})
.join('');
}
getRuleKey(ruleName) {
// Convert display name to key
const mapping = {
'Speed Golf': 'flip_mode',
'Endgame Flip': 'flip_mode',
'Knock Penalty': 'knock_penalty',
'Knock Bonus': 'knock_bonus',
'Super Kings': 'super_kings',
'Ten Penny': 'ten_penny',
'Lucky Swing': 'lucky_swing',
'Eagle Eye': 'eagle_eye',
'Underdog': 'underdog_bonus',
'Tied Shame': 'tied_shame',
'Blackjack': 'blackjack',
'Wolfpack': 'wolfpack',
'Flip Action': 'flip_as_action',
'4 of a Kind': 'four_of_a_kind',
'Negative Pairs': 'negative_pairs_keep_value',
'One-Eyed Jacks': 'one_eyed_jacks',
'Knock Early': 'knock_early',
};
return mapping[ruleName] || ruleName.toLowerCase().replace(/\s+/g, '_');
}
CSS
/* Rule tag base */
.rule-tag {
display: inline-flex;
align-items: center;
padding: 4px 10px;
background: rgba(255, 255, 255, 0.15);
border-radius: 12px;
font-size: 0.8em;
transition: all 0.3s ease;
}
/* Highlighted rule */
.rule-tag.rule-highlighted {
background: rgba(244, 164, 96, 0.3);
box-shadow: 0 0 10px rgba(244, 164, 96, 0.4);
animation: rule-pulse 0.5s ease-out;
}
@keyframes rule-pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
/* Message that appears */
.rule-message {
margin-left: 8px;
padding-left: 8px;
border-left: 1px solid rgba(255, 255, 255, 0.3);
font-weight: bold;
color: #f4a460;
animation: message-fade-in 0.3s ease-out;
}
@keyframes message-fade-in {
0% { opacity: 0; transform: translateX(-5px); }
100% { opacity: 1; transform: translateX(0); }
}
/* Ensure bar is visible when highlighted */
#active-rules-bar:has(.rule-highlighted) {
background: rgba(0, 0, 0, 0.4);
}
Rule-Specific Triggers
Flip Mode (Speed Golf/Endgame)
// When player must flip
if (this.waitingForFlip && !this.flipIsOptional) {
this.highlightRule('flip_mode', 'Flip a face-down card!');
}
Knock Penalty/Bonus
// When someone triggers final turn
if (newState.phase === 'final_turn' && oldState?.phase !== 'final_turn') {
if (this.gameState.knock_penalty) {
this.highlightRule('knock_penalty', '+10 if beaten!');
}
if (this.gameState.knock_bonus) {
this.highlightRule('knock_bonus', '-5 for going out!');
}
}
Negative Pairs
// When pair of 2s or Jokers is formed
checkForNewPairs(oldState, newState, playerId) {
// ... pair detection ...
if (nowPaired && this.gameState.negative_pairs_keep_value) {
const isNegativePair = cardRank === '2' || cardRank === '★';
if (isNegativePair) {
this.highlightRule('negative_pairs_keep_value', 'Keeps -4!');
}
}
}
Score Bonuses (Round End)
// In showScoreboard
if (this.gameState.underdog_bonus) {
const lowestPlayer = findLowest(scores);
this.highlightRule('underdog_bonus', `${lowestPlayer} gets -3!`);
}
if (this.gameState.tied_shame) {
const ties = findTies(scores);
if (ties.length > 0) {
this.highlightRule('tied_shame', '+5 for ties!');
}
}
Test Scenarios
- Speed Golf mode - "Must flip" highlighted when discarding
- Knock with penalty - Warning shown
- Draw Lucky Swing Joker - "-5" highlighted
- Blackjack score - Celebration when 21 → 0
- No active rules - No highlights
- Multiple rules trigger - All relevant ones highlight
Acceptance Criteria
- Rules have data attributes for targeting
- Relevant rule highlights during specific actions
- Highlight message explains the effect
- Highlight auto-fades after duration
- Multiple rules can highlight simultaneously
- Works for all major house rules
- Doesn't interfere with gameplay flow
Implementation Order
- Add
data-ruleattributes to rule tags - Implement
getRuleKey()mapping - Implement
highlightRule()method - Add CSS for highlight animation
- Add trigger points for each major rule
- Test with various rule combinations
- Tune timing and messaging
Notes for Agent
- CSS vs anime.js: CSS is appropriate for rule tag highlights (simple UI feedback)
- Keep highlight messages very short (3-5 words)
- Don't highlight on every single action, just key moments
- The goal is education, not distraction
- Consider: First-time highlight only? (Too complex for V3)
- Make sure the bar is visible when highlighting (expand if collapsed)