- 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>
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
- Show when an opponent is considering their move
- Highlight which pile they're considering (deck vs discard)
- Add brief thinking pause before CPU actions
- Make CPU feel more like a real player
- 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
- Opponent area highlight - Active player's area glows
- "Thinking" indicator - Small animation near their name
- Deck/discard highlight - Show which pile they're eyeing
- 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
- CPU turn starts - Area highlights, thinking indicator shows
- CPU considering discard - Discard pile glows if valuable card
- CPU draws - Thinking indicator changes to held card state
- CPU swaps - Brief consideration before swap
- Human opponent turn - Area highlights but no thinking indicator
- 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
- Add thinking indicator element to opponent areas
- Add CSS for thinking animations
- Implement
updateConsiderationState()method - Implement
clearConsiderationState()method - Add pile consideration highlighting
- Integrate CPU thinking delays (server or client)
- Test with various CPU profiles
- 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
activeAnimationsfor proper cleanup