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>
This commit is contained in:
279
docs/v3/V3_12_DRAW_SOURCE_DISTINCTION.md
Normal file
279
docs/v3/V3_12_DRAW_SOURCE_DISTINCTION.md
Normal file
@@ -0,0 +1,279 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user