Files
golfgame/CLAUDE.md
adlee-was-taken 121959ba27
All checks were successful
Build & Deploy Staging / build-and-deploy (release) Successful in 29s
docs(CLAUDE.md): staging deploy verification checklist
Encodes the lessons from the v3.3.5 → v3.3.5.1 hotfix cascade: CI green
is necessary but not sufficient. Walk the chain: clean worktree → tag
sha matches → container up recently → new code introspectable → env vars
present → DB state correct → end-to-end smoke.

Each step calls out a specific failure mode we just hit, so future-me
doesn't assume the next deploy will 'just work' when the primitives
underneath (git fetch tag-cache, compose env wiring, image reuse) can
silently skip changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:06:37 -04:00

14 KiB

Golf Card Game - Project Context

A real-time multiplayer 6-card Golf card game with CPU opponents and smooth anime.js animations.

Quick Start

# Install dependencies
pip install -r server/requirements.txt

# Run the server
python server/main.py

# Visit http://localhost:8000

For full installation (Docker, PostgreSQL, Redis, production), see INSTALL.md.

Architecture

golfgame/
├── server/                        # Python FastAPI backend
│   ├── main.py                    # HTTP routes, WebSocket server, lifespan
│   ├── game.py                    # Core game logic, state machine
│   ├── ai.py                      # CPU opponent AI with timing/personality
│   ├── handlers.py                # WebSocket message handlers
│   ├── room.py                    # Room/lobby management
│   ├── config.py                  # Environment configuration (pydantic)
│   ├── constants.py               # Card values, game constants
│   ├── auth.py                    # Authentication (JWT, passwords)
│   ├── logging_config.py          # Structured logging setup
│   ├── simulate.py                # AI simulation runner with stats
│   ├── game_analyzer.py           # Query tools for game analysis
│   ├── score_analysis.py          # Score distribution analysis
│   ├── routers/                   # FastAPI route modules
│   │   ├── auth.py                # Login, signup, verify endpoints
│   │   ├── admin.py               # Admin management endpoints
│   │   ├── stats.py               # Statistics & leaderboard endpoints
│   │   ├── replay.py              # Game replay endpoints
│   │   └── health.py              # Health check endpoints
│   ├── services/                  # Business logic layer
│   │   ├── auth_service.py        # User authentication
│   │   ├── admin_service.py       # Admin tools
│   │   ├── stats_service.py       # Player statistics & leaderboards
│   │   ├── replay_service.py      # Game replay functionality
│   │   ├── game_logger.py         # PostgreSQL game move logging
│   │   ├── spectator.py           # Spectator mode
│   │   ├── email_service.py       # Email notifications (Resend)
│   │   ├── recovery_service.py    # Account recovery
│   │   └── ratelimit.py           # Rate limiting
│   ├── stores/                    # Data persistence layer
│   │   ├── event_store.py         # PostgreSQL event sourcing
│   │   ├── user_store.py          # User persistence
│   │   ├── state_cache.py         # Redis state caching
│   │   └── pubsub.py              # Pub/sub messaging
│   ├── models/                    # Data models
│   │   ├── events.py              # Event types for event sourcing
│   │   ├── game_state.py          # Game state representation
│   │   └── user.py                # User data model
│   └── middleware/                 # Request middleware
│       ├── security.py            # CORS, CSP, security headers
│       ├── request_id.py          # Request ID tracking
│       └── ratelimit.py           # Rate limiting middleware
│
├── client/                        # Vanilla JS frontend
│   ├── index.html                 # Main game page
│   ├── app.js                     # Main game controller
│   ├── card-animations.js         # Unified anime.js animation system
│   ├── card-manager.js            # DOM management for cards
│   ├── animation-queue.js         # Animation sequencing
│   ├── timing-config.js           # Centralized timing configuration
│   ├── state-differ.js            # Diff game state for animations
│   ├── style.css                  # Styles (NO card transitions)
│   ├── admin.html                 # Admin panel
│   ├── admin.js                   # Admin panel interface
│   ├── admin.css                  # Admin panel styles
│   ├── replay.js                  # Game replay viewer
│   ├── leaderboard.js             # Leaderboard display
│   └── ANIMATIONS.md              # Animation system documentation
│
├── docs/
│   ├── ANIMATION-FLOWS.md         # Animation flow diagrams
│   ├── v2/                        # V2 architecture docs (event sourcing, auth, etc.)
│   └── v3/                        # V3 feature & refactoring docs
│
├── scripts/                       # Helper scripts
│   ├── install.sh                 # Interactive installer
│   ├── dev-server.sh              # Development server launcher
│   └── docker-build.sh            # Docker image builder
│
└── tests/e2e/                     # End-to-end tests (Playwright)

Key Technical Decisions

Animation System

When to use anime.js vs CSS:

  • Anime.js (CardAnimations): Card movements, flips, swaps, draws - anything involving card elements
  • CSS keyframes/transitions: Simple UI feedback (button hover, badge entrance, status message fades) - non-card elements

General rule: If it moves a card, use anime.js. If it's UI chrome, CSS is fine.

  • See client/ANIMATIONS.md for full documentation
  • See docs/ANIMATION-FLOWS.md for flow diagrams
  • CardAnimations class in card-animations.js handles everything
  • Timing configured in timing-config.js

State Management

  • Server is source of truth
  • Client receives full game state on each update
  • state-differ.js computes diffs to trigger appropriate animations

Animation Race Condition Flags

Several flags in app.js prevent renderGame() from updating the discard pile during animations:

Flag Purpose
isDrawAnimating Local or opponent draw animation in progress
localDiscardAnimating Local player discarding drawn card
opponentDiscardAnimating Opponent discarding without swap
opponentSwapAnimation Opponent swap animation in progress
dealAnimationInProgress Deal animation running (suppresses flip prompts)

Critical: These flags must be cleared in ALL code paths (success, error, fallback). Failure to clear causes UI to freeze.

Clear flags when:

  • Animation completes (callback)
  • New animation starts (clear stale flags)
  • your_turn message received (safety clear)
  • Error/fallback paths

CPU Players

  • AI logic in server/ai.py
  • Configurable timing delays for natural feel
  • Multiple personality types affect decision-making (pair hunters, aggressive, conservative, etc.)

AI Decision Safety Checks:

  • Never swap high cards (8+) into unknown positions (expected value ~4.5)
  • Unpredictability has value threshold (7) to prevent obviously bad random plays
  • Comeback bonus only applies to cards < 8
  • Denial logic skips hidden positions for 8+ cards

Testing AI with simulations:

# Run 500 games and check dumb move rate
python server/simulate.py 500

# Detailed single game output
python server/simulate.py 1 --detailed

# Compare rule presets
python server/simulate.py 100 --compare

Server Architecture

  • Routers (server/routers/): FastAPI route modules for auth, admin, stats, replay, health
  • Services (server/services/): Business logic layer (auth, admin, stats, replay, email, rate limiting)
  • Stores (server/stores/): Data persistence (PostgreSQL event store, user store, Redis state cache, pub/sub)
  • Models (server/models/): Data models (events, game state, user)
  • Middleware (server/middleware/): Security headers, request ID tracking, rate limiting
  • Handlers (server/handlers.py): WebSocket message dispatch (extracted from main.py)

Staging Deploy Verification Checklist

After any release that triggers a staging deploy (via .gitea/workflows/deploy-staging.yml), do NOT trust "CI went green" — walk the full chain end-to-end. A green CI run does not prove the deploy did what you intended: git fetch won't update already-cached tags, compose yaml and .env can drift out of sync, and container images can cache without visible signal. The v3.3.5 → v3.3.5.1 saga cost us two releases because each of these bit in turn.

Run through every step before declaring a staging deploy successful:

  1. Worktree is clean on staging BEFORE cutting the release.

    ssh root@staging.golfcards.club 'cd /opt/golfgame && git status --short'
    

    Must be empty. Dirty files or untracked files will abort git checkout $TAG mid-pipeline. If you ever scp files to staging for hot-patching, land those changes on main + commit before the release, OR git reset --hard HEAD && git clean -fd on staging first.

  2. Staging is actually at the new tag, not a stale cached position.

    ssh root@staging.golfcards.club 'cd /opt/golfgame && git rev-parse HEAD && git log --oneline -1'
    

    Compare the sha to git rev-parse v3.x.y locally. If they differ, a moved tag was force-pushed and the runner used stale cache. Workflows now git fetch --tags --force, but verify.

  3. Container is running the new image (not a recycled old one).

    ssh root@staging.golfcards.club 'docker ps --format "{{.Names}} {{.Status}}"; docker inspect golfgame-app-1 --format "{{.Created}}"'
    

    Up X seconds/minutes with a recent .Created time; not Up 13 hours. A compose restart picks up the current :latest image — confirm that image was built by this release, not a prior one.

  4. New code is actually in the container. Introspect a signature/attribute that changed in this release:

    ssh root@staging.golfcards.club 'docker exec golfgame-app-1 python -c "
    import sys, inspect; sys.path.insert(0, \"/app/server\")
    from services.game_logger import GameLogger
    print(inspect.signature(GameLogger.log_game_start_async).parameters.keys())
    "'
    
  5. Container env has every var the code reads. Any config added to server/config.py this release needs TWO edits to flow through: .env on the host AND - FOO=${FOO:-default} in the compose yaml's environment: block. Setting only .env silently does nothing.

    ssh root@staging.golfcards.club 'docker exec golfgame-app-1 printenv | grep -iE "YOUR_NEW_VAR"'
    
  6. DB schema + invariants hold. Sample the tables this release touches and confirm the new columns/values look right:

    ssh root@staging.golfcards.club 'docker exec golfgame-postgres-1 psql -U golf -d golf -c "SELECT status, COUNT(*) FROM games_v2 GROUP BY status;"'
    
  7. End-to-end smoke. For a feature visible through the API, curl it and verify the response shape and content match expectations:

    curl -s 'https://staging.golfcards.club/api/stats/leaderboard?metric=wins' | python3 -m json.tool
    

    For features that only fire on specific game events (GAME_OVER, abandonment, etc.), run a soak game or manual repro and re-check the DB — don't assume "code is deployed" = "code has executed."

If any step fails, stop and diagnose before running the next release. Cascading hotfixes amplify the problem — each force-moved tag is another chance for the runner's cache to lie to you.

Common Development Tasks

Adjusting Animation Speed

Edit timing-config.js - all timings are centralized there.

Adding New Animations

  1. Add method to CardAnimations class in card-animations.js
  2. Use anime.js, not CSS transitions
  3. Track in activeAnimations Map for cancellation support
  4. Add timing config to timing-config.js if needed

Debugging Animations

// Check what's animating
console.log(window.cardAnimations.activeAnimations);

// Force cleanup
window.cardAnimations.cancelAll();

// Check timing config
console.log(window.TIMING);

Testing CPU Behavior

Adjust delays in server/ai.py CPU_TIMING dict.

Running Tests

# All server tests
cd server && pytest -v

# AI simulation
python server/simulate.py 500

Important Patterns

No CSS Transitions on Cards

Cards animate via anime.js only. The following should NOT have transition (especially on transform):

  • .card, .card-inner
  • .real-card, .swap-card
  • .held-card-floating

Card hover effects are handled by CardAnimations.hoverIn()/hoverOut() methods. CSS may still use box-shadow transitions for hover glow effects.

State Differ Logic (triggerAnimationsForStateChange)

The state differ in app.js detects what changed between game states:

STEP 1: Draw Detection

  • Detects when drawn_card goes from null to something
  • Triggers draw animation (from deck or discard)
  • Sets isDrawAnimating flag

STEP 2: Discard/Swap Detection

  • Detects when discard_top changes and it was another player's turn
  • Triggers swap or discard animation
  • Important: Skip STEP 2 if STEP 1 detected a draw from discard (the discard change was from REMOVING a card, not adding one)

Animation Overlays

Complex animations create temporary overlay elements:

  1. Create .draw-anim-card positioned over source
  2. Hide original card (or set opacity: 0 on discard pile during draw-from-discard)
  3. Animate overlay
  4. Remove overlay, reveal updated card, restore visibility

Fire-and-Forget for Opponents

Opponent animations don't block - no callbacks needed:

cardAnimations.animateOpponentFlip(cardElement, cardData);

Common Animation Pitfalls

Card position before append: Always set left/top styles BEFORE appending overlay cards to body, otherwise they flash at (0,0).

Deal animation source: Use getDeckRect() for deal animations, not getDealerRect(). The dealer rect returns the whole player area, causing cards to animate at wrong size.

Element rects during hidden: visibility: hidden still allows getBoundingClientRect() to work. display: none does not.

Dependencies

Server

  • FastAPI + uvicorn (web framework & ASGI server)
  • websockets (WebSocket support)
  • asyncpg (PostgreSQL async driver)
  • redis (state caching, pub/sub)
  • bcrypt (password hashing)
  • resend (email service)
  • python-dotenv (environment management)
  • sentry-sdk (error tracking, optional)

Client

  • anime.js (animations)
  • No other frameworks

Infrastructure

  • PostgreSQL (event sourcing, auth, stats, game logs)
  • Redis (state caching, pub/sub)

Game Rules Reference

  • 6 cards per player in 2x3 grid
  • Lower score wins
  • Matching columns cancel out (0 points)
  • Jokers are -2 points
  • Kings are 0 points
  • Game ends when a player flips all cards