- 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>
280 lines
7.7 KiB
Markdown
280 lines
7.7 KiB
Markdown
# 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
|
|
|
|
1. Deck draw: Card emerges face-down, then flips
|
|
2. Discard draw: Card lifts straight up (already visible)
|
|
3. Different sound for each source
|
|
4. Visual hint about the strategic difference
|
|
5. Help new players understand the two options
|
|
|
|
---
|
|
|
|
## Current State
|
|
|
|
From `card-animations.js` (CardAnimations class):
|
|
```javascript
|
|
// 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:
|
|
|
|
```javascript
|
|
// 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 `easeInOutQuad` easing
|
|
|
|
### Enhanced Discard Draw
|
|
|
|
The existing `animateDrawDiscard()` in `card-animations.js` already has quick, decisive animation:
|
|
|
|
```javascript
|
|
// 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:
|
|
|
|
```css
|
|
/* 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
1. **Draw from deck** - Longer animation with flip
|
|
2. **Draw from discard** - Quick decisive grab
|
|
3. **Rapid alternating draws** - Animations don't conflict
|
|
4. **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
|
|
|
|
1. Add distinct sounds to `playSound()`
|
|
2. Enhance `animateDrawDeck()` with suspense
|
|
3. Enhance `animateDrawDiscard()` for quick grab
|
|
4. Add deck visual effects (CSS)
|
|
5. Add `shuffleDeckVisual()` method
|
|
6. Test both draw types
|
|
7. Tune timing for feel
|
|
|
|
---
|
|
|
|
## Notes for Agent
|
|
|
|
- Most of this is already implemented in `card-animations.js`
|
|
- Main enhancement is adding distinct sounds (`draw-deck` vs `draw-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
|