golfgame/docs/v3/V3_13_CARD_VALUE_TOOLTIPS.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

9.5 KiB

V3-13: Card Value Tooltips

Overview

New players often forget card values, especially special cards (2=-2, K=0, Joker=-2). This feature adds tooltips showing card point values on long-press or hover.

Dependencies: None Dependents: None


Goals

  1. Show card point value on long-press (mobile) or hover (desktop)
  2. Especially helpful for special value cards
  3. Show house rule modified values if active
  4. Don't interfere with normal gameplay
  5. Optional: disable for experienced players

Current State

No card value tooltips exist. Players must remember:

  • Standard values: A=1, 2-10=face, J/Q=10, K=0
  • Special values: 2=-2, Joker=-2
  • House rules: super_kings=-2, ten_penny=1, etc.

Design

Tooltip Content

┌─────────┐
│    K    │  ← Normal card display
│    ♠    │
└─────────┘
     │
     ▼
 ┌───────┐
 │ 0 pts │  ← Tooltip on hover/long-press
 └───────┘

For special cards:

 ┌────────────┐
 │ -2 pts     │
 │ (negative!)│
 └────────────┘

Activation

  • Desktop: Hover for 500ms (not instant to avoid cluttering)
  • Mobile: Long-press (300ms threshold)
  • Dismiss: Mouse leave / touch release

Implementation

JavaScript

// Card tooltip system

initCardTooltips() {
    this.tooltip = document.createElement('div');
    this.tooltip.className = 'card-value-tooltip hidden';
    document.body.appendChild(this.tooltip);

    this.tooltipTimeout = null;
    this.currentTooltipTarget = null;
}

bindCardTooltipEvents(cardElement, cardData) {
    // Desktop hover
    cardElement.addEventListener('mouseenter', () => {
        this.scheduleTooltip(cardElement, cardData);
    });

    cardElement.addEventListener('mouseleave', () => {
        this.hideCardTooltip();
    });

    // Mobile long-press
    let pressTimer = null;

    cardElement.addEventListener('touchstart', (e) => {
        pressTimer = setTimeout(() => {
            this.showCardTooltip(cardElement, cardData);
            // Prevent triggering card click
            e.preventDefault();
        }, 300);
    });

    cardElement.addEventListener('touchend', () => {
        clearTimeout(pressTimer);
        this.hideCardTooltip();
    });

    cardElement.addEventListener('touchmove', () => {
        clearTimeout(pressTimer);
        this.hideCardTooltip();
    });
}

scheduleTooltip(cardElement, cardData) {
    this.hideCardTooltip();

    if (!cardData?.face_up || !cardData?.rank) return;

    this.tooltipTimeout = setTimeout(() => {
        this.showCardTooltip(cardElement, cardData);
    }, 500); // 500ms delay on desktop
}

showCardTooltip(cardElement, cardData) {
    if (!cardData?.face_up || !cardData?.rank) return;

    const value = this.getCardPointValue(cardData);
    const special = this.getCardSpecialNote(cardData);

    // Build tooltip content
    let content = `<span class="tooltip-value ${value < 0 ? 'negative' : ''}">${value} pts</span>`;
    if (special) {
        content += `<span class="tooltip-note">${special}</span>`;
    }

    this.tooltip.innerHTML = content;

    // Position tooltip
    const rect = cardElement.getBoundingClientRect();
    const tooltipRect = this.tooltip.getBoundingClientRect();

    let left = rect.left + rect.width / 2;
    let top = rect.bottom + 8;

    // Keep on screen
    if (left + tooltipRect.width / 2 > window.innerWidth) {
        left = window.innerWidth - tooltipRect.width / 2 - 10;
    }
    if (left - tooltipRect.width / 2 < 0) {
        left = tooltipRect.width / 2 + 10;
    }
    if (top + tooltipRect.height > window.innerHeight) {
        top = rect.top - tooltipRect.height - 8;
    }

    this.tooltip.style.left = `${left}px`;
    this.tooltip.style.top = `${top}px`;
    this.tooltip.classList.remove('hidden');

    this.currentTooltipTarget = cardElement;
}

hideCardTooltip() {
    clearTimeout(this.tooltipTimeout);
    this.tooltip.classList.add('hidden');
    this.currentTooltipTarget = null;
}

getCardPointValue(cardData) {
    const values = 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
    };

    return values[cardData.rank] ?? 0;
}

getCardSpecialNote(cardData) {
    const rank = cardData.rank;
    const value = this.getCardPointValue(cardData);

    // Special notes for notable cards
    if (value < 0) {
        return 'Negative - keep it!';
    }
    if (rank === 'K' && value === 0) {
        return 'Safe card';
    }
    if (rank === 'K' && value === -2) {
        return 'Super King!';
    }
    if (rank === '10' && value === 1) {
        return 'Ten Penny rule';
    }
    if (rank === 'J' || rank === 'Q') {
        return 'High - replace if possible';
    }

    return null;
}

CSS

/* Card value tooltip */
.card-value-tooltip {
    position: fixed;
    transform: translateX(-50%);
    background: rgba(26, 26, 46, 0.95);
    color: white;
    padding: 6px 12px;
    border-radius: 8px;
    font-size: 0.85em;
    text-align: center;
    z-index: 500;
    pointer-events: none;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
    transition: opacity 0.15s;
}

.card-value-tooltip.hidden {
    opacity: 0;
    pointer-events: none;
}

.card-value-tooltip::before {
    content: '';
    position: absolute;
    top: -6px;
    left: 50%;
    transform: translateX(-50%);
    border: 6px solid transparent;
    border-bottom-color: rgba(26, 26, 46, 0.95);
}

.tooltip-value {
    display: block;
    font-size: 1.2em;
    font-weight: bold;
}

.tooltip-value.negative {
    color: #27ae60;
}

.tooltip-note {
    display: block;
    font-size: 0.85em;
    color: rgba(255, 255, 255, 0.7);
    margin-top: 2px;
}

/* Visual indicator that tooltip is available */
.card[data-has-tooltip]:hover {
    cursor: help;
}

Integration with renderGame

// In renderGame, after creating card elements
renderPlayerCards() {
    // ... existing card rendering ...

    const cards = this.playerCards.querySelectorAll('.card');
    const myData = this.getMyPlayerData();

    cards.forEach((cardEl, i) => {
        const cardData = myData?.cards[i];
        if (cardData?.face_up) {
            cardEl.dataset.hasTooltip = 'true';
            this.bindCardTooltipEvents(cardEl, cardData);
        }
    });
}

// Similar for opponent cards
renderOpponentCards(player, container) {
    // ... existing card rendering ...

    const cards = container.querySelectorAll('.card');
    player.cards.forEach((cardData, i) => {
        if (cardData?.face_up && cards[i]) {
            cards[i].dataset.hasTooltip = 'true';
            this.bindCardTooltipEvents(cards[i], cardData);
        }
    });
}

House Rule Awareness

Tooltip values should reflect active house rules:

getCardPointValue(cardData) {
    // Use server-provided values which include house rules
    if (this.gameState?.card_values) {
        return this.gameState.card_values[cardData.rank] ?? 0;
    }

    // Fallback to defaults
    return DEFAULT_CARD_VALUES[cardData.rank] ?? 0;
}

The server already provides card_values in game state that accounts for:

  • super_kings (K = -2)
  • ten_penny (10 = 1)
  • lucky_swing (Joker = -5)
  • etc.

Performance Considerations

  • Only bind tooltip events to face-up cards
  • Remove tooltip events when cards re-render
  • Use event delegation if performance becomes an issue
// Event delegation approach
this.playerCards.addEventListener('mouseenter', (e) => {
    const card = e.target.closest('.card');
    if (card && card.dataset.hasTooltip) {
        const cardData = this.getCardDataForElement(card);
        this.scheduleTooltip(card, cardData);
    }
}, true);

Settings Option (Optional)

Let players disable tooltips:

// In settings
this.showCardTooltips = localStorage.getItem('showCardTooltips') !== 'false';

// Check before showing
showCardTooltip(cardElement, cardData) {
    if (!this.showCardTooltips) return;
    // ... rest of method
}

Test Scenarios

  1. Hover on face-up card - Tooltip appears after delay
  2. Long-press on mobile - Tooltip appears
  3. Move mouse away - Tooltip disappears
  4. Face-down card - No tooltip
  5. Special cards (K, 2, Joker) - Show special note
  6. House rules active - Modified values shown
  7. Rapid card changes - No stale tooltips

Acceptance Criteria

  • Hover (500ms delay) shows tooltip on desktop
  • Long-press (300ms) shows tooltip on mobile
  • Tooltip shows point value
  • Negative values highlighted green
  • Special notes for notable cards
  • House rule modified values displayed
  • Tooltips don't interfere with gameplay
  • Tooltips position correctly (stay on screen)
  • Face-down cards have no tooltip

Implementation Order

  1. Create tooltip element and basic CSS
  2. Implement showCardTooltip() method
  3. Implement hideCardTooltip() method
  4. Add desktop hover events
  5. Add mobile long-press events
  6. Integrate with renderGame()
  7. Add house rule awareness
  8. Test on mobile and desktop
  9. Optional: Add settings toggle

Notes for Agent

  • CSS vs anime.js: CSS is appropriate for tooltip show/hide transitions (simple UI)
  • The 500ms delay prevents tooltips appearing during normal play
  • Mobile long-press should be discoverable but not intrusive
  • Use server-provided card_values for house rule accuracy
  • Consider: Quick reference card in rules screen? (Separate feature)
  • Don't show tooltip during swap animation