All checks were successful
Build & Deploy Staging / build-and-deploy (release) Successful in 29s
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>
337 lines
14 KiB
Markdown
337 lines
14 KiB
Markdown
# Golf Card Game - Project Context
|
|
|
|
A real-time multiplayer 6-card Golf card game with CPU opponents and smooth anime.js animations.
|
|
|
|
## Quick Start
|
|
|
|
```bash
|
|
# 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](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:**
|
|
```bash
|
|
# 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.**
|
|
```bash
|
|
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.**
|
|
```bash
|
|
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).**
|
|
```bash
|
|
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:
|
|
```bash
|
|
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.
|
|
```bash
|
|
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:
|
|
```bash
|
|
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:
|
|
```bash
|
|
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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```bash
|
|
# 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:
|
|
```javascript
|
|
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
|