golfgame/docs/v3/V3_14_ACTIVE_RULES_CONTEXT.md
adlee-was-taken 9fc6b83bba v3.0.0: V3 features, server refactoring, and documentation overhaul
- 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>
2026-02-14 10:03:45 -05:00

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

  1. Highlight relevant rules during specific actions
  2. Brief explanatory tooltip when rule affects play
  3. Help players learn how rules work
  4. Don't clutter the interface
  5. 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

  1. Speed Golf mode - "Must flip" highlighted when discarding
  2. Knock with penalty - Warning shown
  3. Draw Lucky Swing Joker - "-5" highlighted
  4. Blackjack score - Celebration when 21 → 0
  5. No active rules - No highlights
  6. 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

  1. Add data-rule attributes to rule tags
  2. Implement getRuleKey() mapping
  3. Implement highlightRule() method
  4. Add CSS for highlight animation
  5. Add trigger points for each major rule
  6. Test with various rule combinations
  7. 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)