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

10 KiB

V3-06: Opponent Thinking Phase

Overview

In physical card games, you watch opponents pick up a card, consider it, and decide. Currently, CPU turns happen quickly with minimal visual indication that they're "thinking." This feature adds visible consideration time.

Dependencies: None Dependents: None


Goals

  1. Show when an opponent is considering their move
  2. Highlight which pile they're considering (deck vs discard)
  3. Add brief thinking pause before CPU actions
  4. Make CPU feel more like a real player
  5. Human opponents should also show consideration state

Current State

From app.js and card-animations.js:

// In app.js
updateCpuConsideringState() {
    const currentPlayer = this.gameState.players.find(
        p => p.id === this.gameState.current_player_id
    );
    const isCpuTurn = currentPlayer && currentPlayer.is_cpu;
    const hasNotDrawn = !this.gameState.has_drawn_card;

    if (isCpuTurn && hasNotDrawn) {
        this.discard.classList.add('cpu-considering');
    } else {
        this.discard.classList.remove('cpu-considering');
    }
}

// CardAnimations already has CPU thinking glow:
startCpuThinking(element) {
    anime({
        targets: element,
        boxShadow: [
            '0 4px 12px rgba(0,0,0,0.3)',
            '0 4px 12px rgba(0,0,0,0.3), 0 0 18px rgba(59, 130, 246, 0.5)',
            '0 4px 12px rgba(0,0,0,0.3)'
        ],
        duration: 1500,
        easing: 'easeInOutSine',
        loop: true
    });
}

The existing startCpuThinking() method in CardAnimations provides a looping glow animation. This feature enhances visibility further.


Design

Enhanced Consideration Display

  1. Opponent area highlight - Active player's area glows
  2. "Thinking" indicator - Small animation near their name
  3. Deck/discard highlight - Show which pile they're eyeing
  4. Held card consideration - After draw, show they're deciding

States

1. WAITING_TO_DRAW
   - Player area highlighted
   - Deck and discard both subtly available
   - Brief pause before action (CPU)

2. CONSIDERING_DISCARD
   - Player looks at discard pile
   - Discard pile pulses brighter
   - "Eye" indicator on discard

3. DREW_CARD
   - Held card visible (existing)
   - Player area still highlighted

4. CONSIDERING_SWAP
   - Player deciding which card to swap
   - Their hand cards subtly indicate options

Timing (CPU only)

// In timing-config.js
cpuThinking: {
    beforeDraw: 800,        // Pause before CPU draws
    discardConsider: 400,   // Extra pause when looking at discard
    beforeSwap: 500,        // Pause before CPU swaps
    beforeDiscard: 300,     // Pause before CPU discards drawn card
}

Human players don't need artificial pauses - their actual thinking provides the delay.


Implementation

Thinking Indicator

Add a small animated indicator near the current player's name:

<!-- In opponent area -->
<div class="opponent-area" data-player-id="...">
    <h4>
        <span class="thinking-indicator hidden">🤔</span>
        <span class="opponent-name">Sofia</span>
        ...
    </h4>
</div>

CSS and Animations

Most animations should use anime.js via CardAnimations class for consistency:

// In CardAnimations class - the startCpuThinking method already exists
// Add similar methods for other thinking states:

startOpponentThinking(opponentArea) {
    const id = `opponentThinking-${opponentArea.dataset.playerId}`;
    this.stopOpponentThinking(opponentArea);

    anime({
        targets: opponentArea,
        boxShadow: [
            '0 0 15px rgba(244, 164, 96, 0.4)',
            '0 0 25px rgba(244, 164, 96, 0.6)',
            '0 0 15px rgba(244, 164, 96, 0.4)'
        ],
        duration: 1500,
        easing: 'easeInOutSine',
        loop: true
    });
}

stopOpponentThinking(opponentArea) {
    anime.remove(opponentArea);
    opponentArea.style.boxShadow = '';
}

Minimal CSS for layout only:

/* Thinking indicator - simple show/hide */
.thinking-indicator {
    display: inline-block;
    margin-right: 4px;
}

.thinking-indicator.hidden {
    display: none;
}

/* Current turn highlight base (animation handled by anime.js) */
.opponent-area.current-turn {
    border-color: #f4a460;
}

/* Eye indicator positioning */
.pile-eye-indicator {
    position: absolute;
    top: -15px;
    right: -10px;
    font-size: 1.2em;
}

For the thinking indicator bobbing, use anime.js:

// Animate emoji indicator
startThinkingIndicator(element) {
    anime({
        targets: element,
        translateY: [0, -3, 0],
        duration: 800,
        easing: 'easeInOutSine',
        loop: true
    });
}

JavaScript Updates

// Enhanced consideration state management

updateConsiderationState() {
    const currentPlayer = this.gameState?.players?.find(
        p => p.id === this.gameState.current_player_id
    );

    if (!currentPlayer || currentPlayer.id === this.playerId) {
        this.clearConsiderationState();
        return;
    }

    const hasDrawn = this.gameState.has_drawn_card;
    const isCpu = currentPlayer.is_cpu;

    // Find opponent area
    const area = this.opponentsRow.querySelector(
        `.opponent-area[data-player-id="${currentPlayer.id}"]`
    );

    if (!area) return;

    // Show thinking indicator for CPUs
    const indicator = area.querySelector('.thinking-indicator');
    if (indicator) {
        indicator.classList.toggle('hidden', !isCpu || hasDrawn);
    }

    // Add thinking class to area
    area.classList.toggle('thinking', !hasDrawn);

    // Show which pile they might be considering
    if (!hasDrawn && isCpu) {
        // CPU AI hint: check if discard is attractive
        const discardValue = this.getDiscardValue();
        if (discardValue !== null && discardValue <= 4) {
            this.discard.classList.add('being-considered');
            this.deck.classList.remove('being-considered');
        } else {
            this.deck.classList.add('being-considered');
            this.discard.classList.remove('being-considered');
        }
    } else {
        this.deck.classList.remove('being-considered');
        this.discard.classList.remove('being-considered');
    }
}

clearConsiderationState() {
    // Remove all consideration indicators
    this.opponentsRow.querySelectorAll('.thinking-indicator').forEach(el => {
        el.classList.add('hidden');
    });
    this.opponentsRow.querySelectorAll('.opponent-area').forEach(el => {
        el.classList.remove('thinking');
    });
    this.deck.classList.remove('being-considered');
    this.discard.classList.remove('being-considered');
}

getDiscardValue() {
    const card = this.gameState?.discard_top;
    if (!card) return null;

    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[card.rank] ?? 10;
}

Server-Side CPU Thinking Delay

The server should add pauses for CPU thinking (or the client can delay rendering):

# In ai.py or game.py, after CPU makes decision

async def cpu_take_turn(self, game, player_id):
    thinking_time = self.profile.get_thinking_time()  # 500-1500ms based on profile

    # Pre-draw consideration
    await asyncio.sleep(thinking_time * 0.5)

    # Make draw decision
    source = self.decide_draw_source(game, player_id)

    # Broadcast "considering" state
    await self.broadcast_cpu_considering(game, player_id, source)
    await asyncio.sleep(thinking_time * 0.3)

    # Execute draw
    game.draw_card(player_id, source)

    # Post-draw consideration
    await asyncio.sleep(thinking_time * 0.4)

    # Make swap/discard decision
    ...

Alternatively, handle all delays on the client side by adding pauses before rendering CPU actions.


CPU Personality Integration

Different AI profiles could have different thinking patterns:

// Thinking time variance by personality (from ai.py profiles)
const thinkingProfiles = {
    'Sofia': { baseTime: 1200, variance: 200 },     // Calculated & Patient
    'Maya': { baseTime: 600, variance: 100 },       // Aggressive Closer
    'Priya': { baseTime: 1000, variance: 300 },     // Pair Hunter (considers more)
    'Marcus': { baseTime: 800, variance: 150 },     // Steady Eddie
    'Kenji': { baseTime: 500, variance: 200 },      // Risk Taker (quick)
    'Diego': { baseTime: 700, variance: 400 },      // Chaotic Gambler (variable)
    'River': { baseTime: 900, variance: 250 },      // Adaptive Strategist
    'Sage': { baseTime: 1100, variance: 150 },      // Sneaky Finisher
};

Test Scenarios

  1. CPU turn starts - Area highlights, thinking indicator shows
  2. CPU considering discard - Discard pile glows if valuable card
  3. CPU draws - Thinking indicator changes to held card state
  4. CPU swaps - Brief consideration before swap
  5. Human opponent turn - Area highlights but no thinking indicator
  6. Local player turn - No consideration UI (they know what they're doing)

Acceptance Criteria

  • Current opponent's area highlights during their turn
  • CPU players show thinking indicator (emoji)
  • Deck/discard shows which pile CPU is considering
  • Brief pause before CPU actions (feels like thinking)
  • Different CPU personalities have different timing
  • Human opponents highlight without thinking indicator
  • All indicators clear when turn ends
  • Doesn't slow down the game significantly

Implementation Order

  1. Add thinking indicator element to opponent areas
  2. Add CSS for thinking animations
  3. Implement updateConsiderationState() method
  4. Implement clearConsiderationState() method
  5. Add pile consideration highlighting
  6. Integrate CPU thinking delays (server or client)
  7. Test with various CPU profiles
  8. Tune timing for natural feel

Notes for Agent

  • Use existing CardAnimations methods: startCpuThinking(), stopCpuThinking()
  • Add new methods to CardAnimations for opponent area glow
  • Use anime.js for all looping animations, not CSS keyframes
  • Keep thinking pauses short enough to not frustrate players
  • The goal is to make CPUs feel more human, not slow
  • Different profiles should feel distinct in their play speed
  • Human players don't need artificial delays
  • Consider: Option to speed up CPU thinking? (Future setting)
  • The "being considered" pile indicator is a subtle hint at AI logic
  • Track animations in activeAnimations for proper cleanup