- 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>
7.7 KiB
V3-12: Draw Source Distinction
Overview
Drawing from the deck (face-down, unknown) vs discard (face-up, known) should feel different. Currently both animations are similar. This feature enhances the visual distinction.
Dependencies: None Dependents: None
Goals
- Deck draw: Card emerges face-down, then flips
- Discard draw: Card lifts straight up (already visible)
- Different sound for each source
- Visual hint about the strategic difference
- Help new players understand the two options
Current State
From card-animations.js (CardAnimations class):
// Deck draw: suspenseful pause + flip reveal
animateDrawDeck(cardData, onComplete) {
// Pulse deck, lift card face-down, move to holding, suspense pause, flip
timeline.add({ targets: inner, rotateY: 0, duration: 245 });
}
// Discard draw: quick decisive grab
animateDrawDiscard(cardData, onComplete) {
// Pulse discard, quick lift, direct move to holding (no flip needed)
timeline.add({ targets: animCard, translateY: -12, scale: 1.05, duration: 42 });
}
The distinction exists and is already fairly pronounced. This feature enhances it further with:
- More distinct sounds for each source
- Visual "shuffleDeckVisual" effect when drawing from deck
- Better timing contrast
Design
Deck Draw (Unknown)
1. Deck "shuffles" slightly (optional)
2. Top card lifts off deck
3. Card floats to holding position (face-down)
4. Brief suspense pause
5. Card flips to reveal
6. Sound: "mysterious" flip sound
Discard Draw (Known)
1. Card lifts directly (quick)
2. No flip needed - already visible
3. Moves to holding position
4. "Picked up" visual on discard pile
5. Sound: quick "pick" sound
Visual Distinction
| Aspect | Deck Draw | Discard Draw |
|---|---|---|
| Card state | Face-down → Face-up | Face-up entire time |
| Motion | Float + flip | Direct lift |
| Sound | Suspenseful flip | Quick pick |
| Duration | Longer (suspense) | Shorter (decisive) |
| Deck visual | Cards shuffle | N/A |
| Discard visual | N/A | "Picked up" state |
Implementation
Enhanced Deck Draw
The existing animateDrawDeck() in card-animations.js already has most of this functionality. Enhancements to add:
// In card-animations.js - enhance existing animateDrawDeck
// The current implementation already:
// - Pulses deck before drawing (startDrawPulse)
// - Lifts card with wobble
// - Adds suspense pause before flip
// - Flips to reveal with sound
// Add distinct sound for deck draws:
animateDrawDeck(cardData, onComplete) {
// ... existing code ...
// Change sound from 'card' to 'draw-deck' for more mysterious feel
this.playSound('draw-deck'); // Instead of 'card'
// ... rest of existing code ...
}
// The shuffleDeckVisual already exists as startDrawPulse:
startDrawPulse(element) {
if (!element) return;
element.classList.add('draw-pulse');
setTimeout(() => {
element.classList.remove('draw-pulse');
}, 450);
}
Key existing features:
startDrawPulse()- gold ring pulse effect- Suspense pause of 200ms before flip
- Flip duration 245ms with
easeInOutQuadeasing
Enhanced Discard Draw
The existing animateDrawDiscard() in card-animations.js already has quick, decisive animation:
// Current implementation already does:
// - Pulses discard before picking up (startDrawPulse)
// - Quick lift (42ms) with scale
// - Direct move (126ms) - much faster than deck draw
// - No flip needed (card already face-up)
// Enhancement: Add distinct sound for discard draws
_animateDrawDiscardCard(cardData, discardRect, holdingRect, onComplete) {
// ... existing code ...
// Change sound from 'card' to 'draw-discard' for decisive feel
this.playSound('draw-discard'); // Instead of 'card'
// ... rest of existing code ...
}
Current timing comparison (already implemented):
| Phase | Deck Draw | Discard Draw |
|---|---|---|
| Pulse delay | 250ms | 200ms |
| Lift | 105ms | 42ms |
| Travel | 175ms | 126ms |
| Suspense | 200ms | 0ms |
| Flip | 245ms | 0ms |
| Settle | 150ms | 80ms |
| Total | ~1125ms | ~448ms |
The distinction is already pronounced - discard draw is ~2.5x faster.
Deck Visual Effects
The draw-pulse class already exists with a CSS animation (gold ring expanding). For additional deck depth effect, use CSS only:
/* Deck "depth" visual - multiple card shadows */
#deck {
box-shadow:
1px 1px 0 0 rgba(0, 0, 0, 0.1),
2px 2px 0 0 rgba(0, 0, 0, 0.1),
3px 3px 0 0 rgba(0, 0, 0, 0.1),
4px 4px 8px rgba(0, 0, 0, 0.3);
}
/* Existing draw-pulse animation handles the visual feedback */
.draw-pulse {
/* Already defined in style.css */
}
Distinct Sounds
// In playSound() method
} else if (type === 'draw-deck') {
// Mysterious "what's this?" sound
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.type = 'triangle';
osc.frequency.setValueAtTime(300, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(500, ctx.currentTime + 0.1);
osc.frequency.exponentialRampToValueAtTime(350, ctx.currentTime + 0.15);
gain.gain.setValueAtTime(0.08, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.2);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.2);
} else if (type === 'draw-discard') {
// Quick decisive "grab" sound
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.type = 'square';
osc.frequency.setValueAtTime(600, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(300, ctx.currentTime + 0.05);
gain.gain.setValueAtTime(0.08, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.06);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.06);
}
Timing Comparison
| Phase | Deck Draw | Discard Draw |
|---|---|---|
| Lift | 150ms | 80ms |
| Travel | 250ms | 200ms |
| Suspense | 200ms | 0ms |
| Flip | 350ms | 0ms |
| Settle | 150ms | 80ms |
| Total | ~1100ms | ~360ms |
Deck draw is intentionally longer to build suspense.
Test Scenarios
- Draw from deck - Longer animation with flip
- Draw from discard - Quick decisive grab
- Rapid alternating draws - Animations don't conflict
- CPU draws - Same visual distinction
Acceptance Criteria
- Deck draw has suspenseful pause before flip
- Discard draw is quick and direct
- Different sounds for each source
- Deck shows visual "dealing" effect
- Timing difference is noticeable but not tedious
- Both animations complete cleanly
- Works for both local player and opponents
Implementation Order
- Add distinct sounds to
playSound() - Enhance
animateDrawDeck()with suspense - Enhance
animateDrawDiscard()for quick grab - Add deck visual effects (CSS)
- Add
shuffleDeckVisual()method - Test both draw types
- Tune timing for feel
Notes for Agent
- Most of this is already implemented in
card-animations.js - Main enhancement is adding distinct sounds (
draw-deckvsdraw-discard) - The existing timing difference (1125ms vs 448ms) is already significant
- Deck draw suspense shouldn't be annoying, just noticeable
- Discard draw being faster reflects the strategic advantage (you know what you're getting)
- Consider: Show deck count visual changing? (Nice to have)
- Sound design matters here - different tones communicate different meanings
- Mobile performance should still be smooth