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:
adlee-was-taken
2026-02-14 10:03:45 -05:00
parent 13ab5b9017
commit 9fc6b83bba
60 changed files with 11791 additions and 1639 deletions

View File

@@ -52,7 +52,7 @@ class CardManager {
card.innerHTML = `
<div class="card-inner">
<div class="card-face card-face-front"></div>
<div class="card-face card-face-back"><span>?</span></div>
<div class="card-face card-face-back"></div>
</div>
`;
@@ -64,10 +64,22 @@ class CardManager {
updateCardAppearance(cardEl, cardData) {
const inner = cardEl.querySelector('.card-inner');
const front = cardEl.querySelector('.card-face-front');
const back = cardEl.querySelector('.card-face-back');
// Reset front classes
front.className = 'card-face card-face-front';
// Apply deck color to card back
if (back) {
// Remove any existing deck color classes
back.className = back.className.replace(/\bdeck-\w+/g, '').trim();
back.className = 'card-face card-face-back';
const deckColor = this.getDeckColorClass(cardData);
if (deckColor) {
back.classList.add(deckColor);
}
}
if (!cardData || !cardData.face_up || !cardData.rank) {
// Face down or no data
inner.classList.add('flipped');
@@ -88,6 +100,17 @@ class CardManager {
}
}
// Get the deck color class for a card based on its deck_id
getDeckColorClass(cardData) {
if (!cardData || cardData.deck_id === undefined || cardData.deck_id === null) {
return null;
}
// Get deck colors from game state (set by app.js)
const deckColors = window.currentDeckColors || ['red', 'blue', 'gold'];
const colorName = deckColors[cardData.deck_id] || deckColors[0] || 'red';
return `deck-${colorName}`;
}
getSuitSymbol(suit) {
return { hearts: '♥', diamonds: '♦', clubs: '♣', spades: '♠' }[suit] || '';
}
@@ -104,7 +127,8 @@ class CardManager {
cardEl.style.height = `${rect.height}px`;
if (animate) {
setTimeout(() => cardEl.classList.remove('moving'), 350);
const moveDuration = window.TIMING?.card?.moving || 350;
setTimeout(() => cardEl.classList.remove('moving'), moveDuration);
}
}
@@ -130,7 +154,11 @@ class CardManager {
}
// Animate a card flip
async flipCard(playerId, position, newCardData, duration = 400) {
async flipCard(playerId, position, newCardData, duration = null) {
// Use centralized timing if not specified
if (duration === null) {
duration = window.TIMING?.cardManager?.flipDuration || 400;
}
const cardInfo = this.getHandCard(playerId, position);
if (!cardInfo) return;
@@ -158,7 +186,11 @@ class CardManager {
}
// Animate a swap: hand card goes to discard, new card comes to hand
async animateSwap(playerId, position, oldCardData, newCardData, getSlotRect, getDiscardRect, duration = 300) {
async animateSwap(playerId, position, oldCardData, newCardData, getSlotRect, getDiscardRect, duration = null) {
// Use centralized timing if not specified
if (duration === null) {
duration = window.TIMING?.cardManager?.moveDuration || 250;
}
const cardInfo = this.getHandCard(playerId, position);
if (!cardInfo) return;
@@ -192,7 +224,8 @@ class CardManager {
}
inner.classList.remove('flipped');
await this.delay(400);
const flipDuration = window.TIMING?.cardManager?.flipDuration || 400;
await this.delay(flipDuration);
}
// Step 2: Move card to discard
@@ -202,7 +235,8 @@ class CardManager {
cardEl.classList.remove('moving');
// Pause to show the discarded card
await this.delay(250);
const pauseDuration = window.TIMING?.cardManager?.moveDuration || 250;
await this.delay(pauseDuration);
// Step 3: Update card to show new card and move back to hand
front.className = 'card-face card-face-front';