Files
open-claw-team/.openclaw/workspace/docs/superpowers/plans/2026-03-31-lotto-game-full-stack.md
T

581 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Production-Ready Lô Tô Game — Full Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build a full-stack multiplayer Lô Tô game (Node.js backend + React frontend) that is production-ready, deployable to Zalo Mini Apps, with robust testing, authentication, real-time gameplay, and full economy/social features.
**Architecture:** Server-authoritative WebSocket game server (Socket.io) with React SPA frontend, using PostgreSQL for persistence and Redis for real-time state/rate limiting. All game logic validated server-side; client uses optimistic UI.
**Tech Stack:**
- Backend: Node.js, Express, Socket.io, JSON Web Tokens (JWT), PostgreSQL (Supabase/Neon), Redis (Upstash)
- Frontend: React 18, Vite, Socket.io-client, Framer Motion, Zustand (state), Tailwind CSS (styling)
- Testing: Jest (unit), Cypress (E2E multi-player)
- Deployment: Docker, Vercel (frontend), Railway/Render (backend), GitHub Actions (CI)
- Auth: JWT, Zalo OAuth (Phase 2), anonymous temp users (Phase 1)
---
## Pre-Implementation Setup
Before any tasks, ensure environment:
- Create directory structure for frontend (`client/`) and backend (`server/`)
- Initialize Git repository (if not already)
- Create `.gitignore` (Node, React, VSCode,OS, env files)
- Set up root `docker-compose.yml` for local DB+Redis
- Create `.env.example` with all required env vars: DB URLs, JWT secret, Redis URL, etc.
- Set up root `package.json` workspaces or separate package.json server/client
- Configure ESLint + Prettier for consistency
---
## Phase 1: Foundation (Backend Core) — 2 weeks
### Task 1: Initialize Backend Project & Docker Compose
**Files:**
- Create: `server/package.json`
- Create: `server/.env.example`
- Create: `server/.eslintrc.js`
- Create: `server/README.md`
- Create: `docker-compose.yml` at repo root
- Create: `.gitignore`
- [ ] **Step 1: Write failing test** — not applicable (setup task)
- [ ] **Step 2: Create server/package.json** with dependencies: `express`, `socket.io`, `pg`, `ioredis`, `jsonwebtoken`, `uuid`, `dotenv`, `winston`, `cors`, `helmet`
- [ ] **Step 3: Create docker-compose.yml** with services: `postgres` (image: postgres:15), `redis` (image: redis:7)
- [ ] **Step 4: Create .env.example** listing all env vars (DATABASE_URL, REDIS_URL, JWT_SECRET, PORT=3001, NODE_ENV=development)
- [ ] **Step 5: Create server README** with setup instructions
- [ ] **Step 6: Commit** with message "feat: initialize backend project and docker compose"
### Task 2: Database Schema Migration
**Files:**
- Create: `server/db/migrations/001_initial_schema.sql`
- Create: `server/db/index.js` ( connects to PostgreSQL using `pg` or `supabase` client)
- Create: `server/db/redisClient.js` (connects to Redis)
- [ ] **Step 1: Write failing test** — create `server/db/schema.test.js` that imports the schema file and checks that all tables exist using raw SQL introspection (skip in CI if DB not up)
- [ ] **Step 2: Write SQL migration** with all tables and indexes exactly as spec (users, rooms, room_players, coin_transactions, game_history)
- [ ] **Step 3: Write db/index.js** that exports pool with `DATABASE_URL` connection; include `query` helper that logs slow queries (>100ms)
- [ ] **Step 4: Write db/redisClient.js** that creates Redis client from `REDIS_URL`, exports with `get`/`set`/`sadd`/`zadd` wrappers
- [ ] **Step 5: Run test** — ensure migration SQL can be applied (docker compose up -d; psql -f migration)
- [ ] **Step 6: Commit** "feat: database schema and clients"
### Task 3: JWT Auth Service (Anonymous)
**Files:**
- Create: `server/services/auth.js`
- Create: `server/middleware/auth.js`
- Create: `server/routes/auth.js`
- Modify: `server/index.js` (or `server/app.js`) to register auth routes
- [ ] **Step 1: Write failing test**`server/services/auth.test.js`:
- test `createAnonymousUser()` returns a JWT and creates user record with temp_id
- test `authenticateJWT()` validates token and returns user payload
- [ ] **Step 2: Implement auth.service**:
- `generateTempUserId()` — UUID v4
- `createAnonymousUser()` — INSERT into users (temp_id, coins=1000) RETURNING id, temp_id, coins; sign JWT with payload `{ userId, tempId, type: 'anonymous' }`
- `verifyToken(token)` — verify JWT secret, return payload
- [ ] **Step 3: Implement auth.middleware** that extracts Bearer token, calls verifyToken, attaches `req.user`
- [ ] **Step 4: Implement auth.route**:
- POST `/api/auth/anonymous` → creates anonymous account, returns `{ token, user: { id, coins } }`
- GET `/api/auth/me` → returns user from token
- [ ] **Step 5: Wire up server** to use routes and middleware
- [ ] **Step 6: Run tests** — pass
- [ ] **Step 7: Commit** "feat: anonymous JWT auth"
### Task 4: Socket.io Server Setup
**Files:**
- Create: `server/socket/socketServer.js`
- Create: `server/socket/handlers/roomHandlers.js`
- Create: `server/socket/handlers/gameHandlers.js`
- Modify: `server/index.js` to initialize Socket.io and attach handlers
- [ ] **Step 1: Write failing test**`server/socket/socketServer.test.js` using `socket.io-client` to connect, emit `create_room`, expect `room_created` event (mock DB)
- [ ] **Step 2: Implement socketServer.js**:
- `setupSocket(server)` attaches Socket.io to HTTP server
- uses `socket.use(authMiddleware)` to authenticate JWT
- registers namespaces (none, default)
- [ ] **Step 3: Implement roomHandlers.js**:
- `onCreateRoom(socket, data)` validates betAmount, maxPlayers, highlightSeconds; generates room code; creates room record; joins socket to room; emits `room_created`
- `onJoinRoom(socket, {roomCode})` validates code; adds player to room_players; joins socket room; emits `room_joined` with state
- `onReady(socket)` marks player ready; if all ready, transitions to `playing` after countdown
- [ ] **Step 4: Implement gameHandlers.js** (stubs):
- `onPlaceGrain` — validations (server-authoritative) will be added later
- `onCallKinh` — stub: push to Redis claims set
- `onChat` — broadcast to room
- [ ] **Step 5: Wire in socketServer to index.js**
- [ ] **Step 6: Run test** — pass
- [ ] **Step 7: Commit** "feat: socket.io server and room/game handlers"
### Task 5: Rate Limiting Middleware
**Files:**
- Create: `server/middleware/rateLimit.js`
- [ ] **Step 1: Write failing test**`server/middleware/rateLimit.test.js`:
- Simulate 10 requests from same user within second; first N allowed, rest blocked with 429
- TTL expiration allows after 1s
- [ ] **Step 2: Implement rateLimit middleware**:
- Uses Redis key `rate_limit:{userId}:{action}` with INCR and EXPIRE 1s
- Allows max 10 requests per second per action type (grain_placement, call_kinh, chat)
- Returns 429 with JSON `{ error: 'rate_limited' }` if exceeded
- Attachable to socket.io via `socket.use(...)`
- [ ] **Step 3: Apply rate limiting** to `place_grain`, `call_kinh`, `chat` events in socket handlers
- [ ] **Step 4: Run tests** — pass
- [ ] **Step 5: Commit** "feat: rate limiting per user action"
### Task 6: Balance Reservation at Join
**Files:**
- Modify: `server/socket/handlers/roomHandlers.js` (join_room)
- Create: `server/services/coinService.js`
- Create: `server/services/coinService.test.js`
- [ ] **Step 1: Write failing test** — test `reserveBet(userId, roomBet)` deducts from users.coins and logs coin_transaction type=BET_PLACED with negative amount; test failure when insufficient coins
- [ ] **Step 2: Implement coinService**:
- `reserveBet(userId, amount)` — atomic: UPDATE users SET coins = coins - amount WHERE id=$1 AND coins >= amount; if row count 0 throw insufficient; then INSERT coin_transaction (type=BET_PLACED, amount=-amount, balance_after = (SELECT coins FROM users WHERE id=$1))
- `refundBet(userId, amount)` — opposite (used on leave before start)
- `applyWin(userId, amount, roomId)` — add coins, log KINH_WON
- `applyPenalty(userId, amount, roomId)` — subtract coins, log KINH_PENALTY
- [ ] **Step 3: Wire join_room** to call `reserveBet` when player joins; if fails, emit error and reject join
- [ ] **Step 4: Run tests** — pass
- [ ] **Step 5: Commit** "feat: coin reservation on room join"
---
## Phase 2: Core Gameplay Engine — 2 weeks
### Task 7: Number Draw Engine
**Files:**
- Create: `server/services/drawEngine.js`
- Create: `server/services/drawEngine.test.js`
- Modify: `server/socket/handlers/gameHandlers.js` (add `startGame`)
- [ ] **Step 1: Write failing test** — test `shuffleNumbers()` returns array of 90 unique numbers; test `drawNext()` returns number not previously drawn
- [ ] **Step 2: Implement drawEngine**:
- `createDeck()` returns shuffled numbers 1..90 using Fisher-Yates
- `DrawSession` class: holds remaining deck, drawn array, current index. Method `drawNext()` returns next number, stores in drawn array with timestamp
- [ ] **Step 3: Implement `startGame(roomId)` handler**:
- Fetch room players; verify all ready or owner forced start
- Create DrawSession for room (store in Redis: `room:{roomId}:draw_state` = JSON with deck)
- Set room status = 'playing'; broadcast `game_start` with empty drawnNumbers
- Begin async loop: for each number, `setTimeout` highlightSeconds*1000, then emit `number_drawn` and `highlight_end`
- [ ] **Step 4: Run tests** — pass
- [ ] **Step 5: Commit** "feat: number draw engine with broadcast"
### Task 8: Rice Grain Placement Validation
**Files:**
- Create: `server/services/boardService.js`
- Create: `server/services/boardService.test.js`
- Modify: `server/socket/handlers/gameHandlers.js` (onPlaceGrain)
- [ ] **Step 1: Write failing test** — test `canPlaceGrain(userId, row, col, drawnNumbers)` returns false if column's number not in drawnNumbers; true if drawn; also check max 5 grains per row
- [ ] **Step 2: Implement boardService**:
- `getNumberForCell(row, col)` returns number (190) based on standard Lô Tô card layout (3 rows × 9 cols, 5 numbers per row distributed by decades)
- `isNumberDrawn(num, drawnNumbers)` — includes in array check
- `canPlaceGrain(userId, row, col, drawnNumbers, roomPlayers)` — ensures user has not exceeded 5 grains per row; cell not already occupied by that user; number drawn; bet reserved sufficient
- `placeGrain(userId, row, col)` — records grain placement (in Redis set `room:{roomId}:player:{userId}:grains` serialized) and broadcast to room
- [ ] **Step 3: in onPlaceGrain**, validate with boardService; if valid, update Redis and broadcast `grain_placed` event with position; if invalid, emit error
- [ ] **Step 4: Run tests** — pass
- [ ] **Step 5: Commit** "feat: rice grain placement validation"
### Task 9: Concurrent Kinh Handling
**Files:**
- Modify: `server/socket/handlers/gameHandlers.js` (onCallKinh)
- Create: `server/services/kinhService.js`
- Create: `server/services/kinhService.test.js`
- [ ] **Step 1: Write failing test** — test `validateKinh(userId, roomId)` returns true if all rows that have grains fully match drawnNumbers; false otherwise
- [ ] **Step 2: Implement `validateKinh`**:
- Get user's grains from Redis; compute rows where they have exactly 5 grains; check every grain's number is in drawnNumbers
- Returns object `{ valid: boolean, rowsCompleted: number[] }`
- [ ] **Step 3: Implement onCallKinh**:
- When received, add to Redis sorted set `room:{roomId}:kinh_claims` with score=Date.now()
- Broadcast `kinh_claim` to room so UI shows pending badge
- Do not respond immediately; wait for freeze window end
- [ ] **Step 4: Modify draw engine**:
- After `highlight_end` emitted, set a short freeze timeout (500ms)
- After freeze, collect all userIds from Redis sorted set (score within window); clear set; process all claims via `processKinhClaims(roomId, claims)`
- [ ] **Step 5: Implement processKinhClaims**:
- For each claimant, run `validateKinh`
- Separate into validWinners and invalidClaimants
- Compute totalPot = sum of all bets (roomBet * numPlayers)
- Compute reward per valid = totalPot / validCount (integer math; any remainder left in pot? Could add to next round or house. We'll do integer division, remainder stays with house for simplicity)
- For each invalid, penalty = betAmount to *every other player* in room (including other invalids). Implement loop: for each invalid `p`, for each other player `q` in room_players, transfer `betAmount` from p to q using coinService
- For each valid, reward = their share (already computed) added to balance via coinService
- Update room status to 'settling'; then 'settled' after DB updates
- Broadcast `game_settled` with full results (including pre/post balances)
- [ ] **Step 6: Run tests** — pass (use unit tests to validate math; integration will test end-to-end)
- [ ] **Step 7: Commit** "feat: concurrent Kinh verification and settlement"
---
## Phase 3: Frontend Core — 2 weeks
### Task 10: Initialize React Frontend with Vite
**Files:**
- Create: `client/package.json`
- Create: `client/vite.config.js`
- Create: `client/index.html`
- Create: `client/src/main.jsx`
- Create: `client/src/App.jsx`
- Create: `client/tailwind.config.js` (or use plain CSS per retro theme)
- Create: `client/.env.example`
- [ ] **Step 1: Create client/package.json** with deps: `react`, `react-dom`, `vite`, `socket.io-client`, `framer-motion`, `zustand`, `axios`, `tailwindcss`
- [ ] **Step 2: Create vite.config.js** for React, set base for Zalo Mini App later (`base: './'`), proxy API to backend for dev (`server.proxy`)
- [ ] **Step 3: Create index.html** simple root div
- [ ] **Step 4: Create main.jsx** ReactDOM.render into #root
- [ ] **Step 5: Create App.jsx** with basic routing (Lobby vs Game) using React state for now
- [ ] **Step 6: Create .env.example** with VITE_API_URL, VITE_WS_URL
- [ ] **Step 7: Commit** "feat: initialize React frontend with Vite"
### Task 11: Authentication Context & Login UI
**Files:**
- Create: `client/src/contexts/AuthContext.jsx`
- Create: `client/src/hooks/useAuth.js`
- Create: `client/src/components/Auth/AuthModal.jsx`
- Create: `client/src/components/Auth/LoginButton.jsx`
- Modify: `client/src/App.jsx` to use AuthContext and show login UI
- [ ] **Step 1: Write failing test**`client/src/contexts/AuthContext.test.jsx` with Jest Testing Library: test that after login, context has token and user; test localStorage persistence
- [ ] **Step 2: Implement AuthContext**:
- State: `user` (null or {id, coins}), `token` (string), `loading`
- Effect on mount: check localStorage for token; if present, call `/api/auth/me` to validate; else clear
- `loginAnonymous()` calls POST `/api/auth/anonymous`, saves token+user to localStorage, updates state
- `logout()` clears state and localStorage
- [ ] **Step 3: Implement LoginButton** that triggers `loginAnonymous`; also placeholder Zalo button (disabled behind flag)
- [ ] **Step 4: Implement AuthModal** that shows login options, closes after login
- [ ] **Step 5: Wire App.jsx**: if not authenticated, show login modal; else show main app (lobby placeholder)
- [ ] **Step 6: Run tests** — pass
- [ ] **Step 7: Commit** "feat: anonymous auth context and UI"
### Task 12: WebSocket Hook & Socket Context
**Files:**
- Create: `client/src/hooks/useWebSocket.js`
- Create: `client/src/contexts/GameContext.jsx`
- Create: `client/src/services/socketService.js`
- [ ] **Step 1: Write failing test**`client/src/hooks/useWebSocket.test.js`: mock socket.io-client; test that on connection, state is connected; on event, handler called
- [ ] **Step 2: Implement socketService.js**:
- Singleton that creates `socket` instance from `io()` with auth token
- Methods: `on(event, handler)`, `emit(event, payload)`, `disconnect()`
- Auto-reconnect logic
- [ ] **Step 3: Implement useWebSocket** hook that provides `socket` object via context
- [ ] **Step 4: Implement GameContext**:
- State: `room`, `players`, `gameState` ('lobby','playing','frozen','settled'), `currentNumber`, `myGrains`, etc.
- Socket event listeners: `room_joined`, `player_joined`, `number_drawn`, `highlight_end`, `kinh_claim`, `game_settled`, `balance_update`, `error`
- Methods: `createRoom`, `joinRoom`, `ready`, `placeGrain`, `callKinh`, `sendChat`, `leave`
- [ ] **Step 5: Run tests** — pass
- [ ] **Step 6: Commit** "feat: WebSocket hook and game context"
### Task 13: Lobby & Room Management UI
**Files:**
- Create: `client/src/components/Lobby/LobbyList.jsx`
- Create: `client/src/components/Lobby/RoomCard.jsx`
- Create: `client/src/components/Lobby/CreateRoomModal.jsx`
- Modify: `client/src/App.jsx` to show lobby when authenticated
- [ ] **Step 1: Write failing test**`client/src/components/Lobby/LobbyList.test.jsx`: test that room cards display bet amount, player count; test join button calls joinRoom
- [ ] **Step 2: Implement CreateRoomModal** with form fields: bet amount (input), max players (select 2-8), highlight seconds (select 1-5). Submits to `createRoom` from GameContext.
- [ ] **Step 3: Implement RoomCard** to show room code, bet, players count, status; join button disabled if full or started
- [ ] **Step 4: Implement LobbyList** that fetches public rooms from `/api/rooms/public` (backend stub needed later) or via socket event `public_rooms` (we'll add that to backend later). For now, mock data; later will wire to backend.
- [ ] **Step 5: Wire App.jsx** to show LobbyList after login; show CreateRoomModal button
- [ ] **Step 6: Run tests** — pass
- [ ] **Step 7: Commit** "feat: lobby UI and room creation"
---
## Phase 4: Core Gameplay Frontend — 2 weeks
### Task 14: Lotto Card & Board
**Files:**
- Create: `client/src/components/Game/LottoCard.jsx`
- Create: `client/src/utils/cardPositions.js`
- Modify: `client/src/components/Game/GameBoard.jsx` (to be created)
- [ ] **Step 1: Write failing test**`client/src/components/Game/LottoCard.test.jsx`: test that grid has 3 rows, 9 cols; test clicking cell triggers `placeGrain`; test grains display as placed
- [ ] **Step 2: Implement cardPositions.js** with `getNumberForCell(row, col)` (0-indexed rows 0-2, cols 0-8). Rules: Column 0 = numbers 1-9; column 1 = 10-19; ... column 8 = 81-90? Actually Lô Tô: 9 columns represent tens: 1-9,10-19,...,80-90. Each column has 3 rows, but only 5 numbers per row across columns. Standard layout: Each row contains 5 numbers placed in specific column positions based on decade. We need precise mapping. Quick reference: traditional Lô Tô card:
- Columns: 1 (1-9), 2 (10-19), 3 (20-29), 4 (30-39), 5 (40-49), 6 (50-59), 7 (60-69), 8 (70-79), 9 (80-90)
- Rows: top, middle, bottom. Each row has 5 numbers; the distribution across columns follows rules: each row includes exactly 5 numbers from 5 different columns. There are standard patterns but we can simplify: randomly assign 5 numbers (from the column's range) to each row, ensuring each column used at least once across rows? For simplicity, we can predefine a fixed pattern for all cards: each card identical layout. Let's define a static mapping array `CELL_NUMBERS[3][9]` where each cell has a number (1-90) or null. Only 15 cells are numbers; others empty visual. We'll create a deterministic layout (same for all players) to avoid needing dynamic generation. We'll hardcode a valid Lô Tô card pattern: e.g.,
Row1: 1,11,21,31,41 at columns 1,2,3,4,5; others empty.
Row2: 51,61,71,81,91? Wait 91 doesn't exist. Let's design properly: We need 5 numbers per row across 9 columns; each column appears at most once per row. Use this pattern (just an example):
Row1: col0=1, col1=11, col2=25, col3=33, col4=44, others null
Row2: col0=5, col1=17, col2=28, col4=49, col6=62
Row3: col1=20, col3=39, col5=58, col7=79, col8=90
We'll make a specific valid layout and hardcode it. It's not random; all players have same card pattern. That's fine for MVP.
Implement `getNumberForCell(row, col)` returns that hardcoded number or null.
- [ ] **Step 3: Implement LottoCard component**:
- Renders a 3x9 grid using CSS grid.
- For each cell: if number exists, clickable area; if null, empty placeholder with retro styling.
- Shows rice grain overlay if cell is in `grains` set (from GameContext)
- If cell's number is in `drawnNumbers`, highlight with CSS class
- onClick calls `placeGrain(row, col)`
- [ ] **Step 4: Implement GameBoard**:
- Contains LottoCard, NumberBag animation, current number display, Kinh button, players panel
- Uses Framer Motion for NumberBag (shake) when number being drawn
- [ ] **Step 5: Run tests** — pass
- [ ] **Step 6: Commit** "feat: Lotto card and game board UI"
### Task 15: Framer Motion Animations
**Files:**
- Modify: `client/src/components/Game/NumberBag.jsx` (new file)
- Modify: `client/src/components/Game/RiceGrain.jsx` (new file)
- Modify: `client/src/styles/retroTheme.js`
- [ ] **Step 1: Write failing test** (visual, manual) — skip unit; will verify visually
- [ ] **Step 2: Create NumberBag component** with animation using Framer Motion: while `isShaking` true, rotate and scale; while `drawing` emits number ball with motion
- [ ] **Step 3: Create RiceGrain component** — when placed, animate drop (y: -100 to 0 with bounce) using `motion.div`
- [ ] **Step 4: Create retroTheme.js** with color palette: nâu gỗ, vàng ố, đỏ đất, xanh lá cũ; fonts: handwriting retro; textures optional (use CSS patterns)
- [ ] **Step 5: Apply retro theme to components** (buttons, cards)
- [ ] **Step 6: Manual QA**: run app, verify animations smooth (≥30fps)
- [ ] **Step 7: Commit** "feat: animations and retro styling"
### Task 16: In-Game HUD & Controls
**Files:**
- Create: `client/src/components/Game/PlayersPanel.jsx`
- Create: `client/src/components/Game/KinhButton.jsx`
- Create: `client/src/components/Game/ChatPanel.jsx`
- Modify: `client/src/components/Game/GameBoard.jsx` to include these
- [ ] **Step 1: Write failing test**`client/src/components/Game/KinhButton.test.jsx`: test disabled when not playing; click calls `callKinh`
- [ ] **Step 2: Implement PlayersPanel**:
- Shows list: avatar, name, ready status, balance (pre-game), grains count
- Updates via GameContext
- [ ] **Step 3: Implement KinhButton**:
- Visible only during playing and after user has completed at least one row (check row completeness from grains)
- Clicking emits `callKinh`; button disabled after click to prevent spam
- Shows countdown timer if freeze window active? Could show "Verifying..." message
- [ ] **Step 4: Implement ChatPanel**:
- Simple input + send button; messages list scrolling; emits `chat` event; receives `chat_message`
- No persistence beyond current room
- [ ] **Step 5: Run tests** — pass
- [ ] **Step 6: Commit** "feat: in-game HUD, players panel, Kinh button, chat"
---
## Phase 4 (cont'd): Economy, Social & Polish — 2 weeks
### Task 17: Coin Economy & Daily Bonus
**Files:**
- Create: `server/routes/economy.js`
- Create: `server/services/coinService.js` (extend with dailyBonus)
- Create: `client/src/components/Profile/CoinWallet.jsx`
- Create: `client/src/components/Profile/DailyBonusButton.jsx`
- [ ] **Step 1: Write failing test**`server/services/coinService.test.js`: test `claimDailyBonus(userId)` gives coins only if last claim >24h ago; updates `last_claimed_at` in users (add column); test failure if already claimed
- [ ] **Step 2: Add DB column** `last_daily_bonus_at TIMESTAMP` to users (migration `002_add_daily_bonus.sql`)
- [ ] **Step 3: Implement `claimDailyBonus` in coinService** with 24h check; inserts coin_transaction type=DAILY_BONUS
- [ ] **Step 4: Create route `POST /api/economy/daily-bonus`** protected by auth; calls service; returns new balance
- [ ] **Step 5: Frontend: DailyBonusButton** that calls endpoint, shows next available time; update AuthContext user balance after success
- [ ] **Step 6: Run tests** — pass
- [ ] **Step 7: Commit** "feat: daily coin bonus system"
### Task 18: Achievements System
**Files:**
- Create: `server/routes/achievements.js`
- Create: `server/services/achievementService.js`
- Create: `server/db/migrations/003_achievements.sql` (tables: achievements, user_achievements)
- Create: `client/src/components/Profile/AchievementsList.jsx`
- [ ] **Step 1: Write failing test** — define achievement logic: e.g., FIRST_WIN (first Kinh win), CONCURRENT_KINH_3 (call Kinh with >=2 others simultaneously). Test that when game_settled event emitted, service checks and grants achievements; test duplicate prevention
- [ ] **Step 2: Create DB tables**:
- `achievements` (id, code unique, name, description, coin_reward)
- `user_achievements` (user_id, achievement_id, earned_at, UNIQUE(user_id,achievement_id))
- Insert seed data for several achievements
- [ ] **Step 3: Implement achievementService**:
- `checkAndGrantAchievements(userId, event, payload)` — switch on event (e.g., 'game_settled'); query user stats; if not earned, insert into user_achievements and add coin_reward via coinService
- `getUserAchievements(userId)` for UI
- [ ] **Step 4: Wire achievement check** into `processKinhClaims` after settlement: emit event to achievementService for each winner (and possibly others)
- [ ] **Step 5: Route `GET /api/achievements`** returns list of all achievements with earned flag
- [ ] **Step 6: Frontend: AchievementsList** component displays badges, earned status, coin rewards
- [ ] **Step 7: Run tests** — pass
- [ ] **Step 8: Commit** "feat: achievements system with coin rewards"
### Task 19: Spin Wheel Mini-Game
**Files:**
- Create: `server/routes/spin-wheel.js`
- Create: `server/services/spinWheelService.js`
- Create: `client/src/components/Profile/SpinWheel.jsx`
- [ ] **Step 1: Write failing test** — test `spinWheel(userId)` returns random prize segment; test cooldown (24h); test probability distribution
- [ ] **Step 2: Add DB columns** to users: `last_spun_at TIMESTAMP`
- [ ] **Step 3: Define wheel segments** array: `[{label: '100 coins', reward: 100, weight: 50}, {label: '500 coins', reward: 500, weight: 10}, {label: 'Try again', reward: 0, weight: 40}]` etc.
- [ ] **Step 4: Implement spinWheelService**:
- Weighted random selection based on segments
- Check cooldown; if available, grant coins via coinService, set `last_spun_at`, return prize
- [ ] **Step 5: Route `POST /api/spin`** returns prize result and new balance
- [ ] **Step 6: Frontend: SpinWheel component** with animated wheel (Framer Motion spin animation), button to spin (disabled if on cooldown), display result
- [ ] **Step 7: Run tests** — pass
- [ ] **Step 8: Commit** "feat: daily spin wheel mini-game"
### Task 20: Chat System
**Files:**
- Modify backend: `server/socket/handlers/gameHandlers.js` to store chat in Redis list `room:{roomId}:chat` (max 100) and broadcast `chat_message`
- Modify frontend: `client/src/components/Game/ChatPanel.jsx` already created; ensure displays messages with sender name and timestamp
- [ ] **Step 1: Write failing test** — integration test: two sockets in same room; one sends chat; other receives event with correct sender and message
- [ ] **Step 2: Implement server** chat handler:
- `onChat(socket, { message })`: validate non-empty, length < 200; record in Redis list (LTRIM to keep last 100); broadcast `chat_message` with userId, name (from room_players), message, timestamp
- [ ] **Step 3: Implement client** ChatPanel: maintain local messages array; on `chat_message` event, append; scroll to bottom
- [ ] **Step 4: (Optional) Profanity filter** — simple blacklist array; filter message before broadcast; if blocked, emit error to sender only
- [ ] **Step 5: Run tests** — pass
- [ ] **Step 6: Commit** "feat: in-room chat"
### Task 21: Profile & Stats
**Files:**
- Create: `server/routes/profile.js`
- Create: `client/src/components/Profile/ProfilePage.jsx`
- Modify: `client/src/App.jsx` add route for profile
- [ ] **Step 1: Write failing test**`server/routes/profile.test.js`: test GET returns user stats: total_wins, total_losses, recent history (last 10 coin_transactions)
- [ ] **Step 2: Implement route**:
- `GET /api/profile` → selects from users, left join coin_transactions, returns summary and recent history
- [ ] **Step 3: Frontend ProfilePage** displays: avatar (from user record if Zalo, else placeholder), name, coins, win/loss counts, recent transactions in table, achievements section (link to AchievementsList), daily bonus button, spin wheel section
- [ ] **Step 4: Navigation** to profile from lobby
- [ ] **Step 5: Run tests** — pass
- [ ] **Step 6: Commit** "feat: user profile and stats page"
---
## Phase 5: Testing, Docker & Deployment — 1 week
### Task 22: Jest Unit Tests (Comprehensive)
**Files:**
- Create: `server/jest.config.js`
- Create: `client/jest.config.js` (if using Jest for React)
- Fill: multiple unit tests for all services: auth, coinService, boardService, drawEngine, kinhService
- [ ] **Step 1: Write tests** for every service function (see earlier tasks). Aim 80%+ coverage.
- [ ] **Step 2: Configure Jest** for server (babel or ts if using TS; we're using plain JS so default)
- [ ] **Step 3: Add test script** to server/package.json: `"test": "jest"`
- [ ] **Step 4: Run coverage** and fix any gaps
- [ ] **Step 5: Commit** "test: comprehensive unit test suite"
### Task 23: Cypress E2E Multi-Player Tests
**Files:**
- Create: `client/cypress.config.js`
- Create: `client/cypress/e2e/create-room.cy.js`
- Create: `client/cypress/e2e/gameplay.cy.js`
- Create: `client/cypress/e2e/concurrent-kinh.cy.js`
- [ ] **Step 1: Write failing test** (setup): test that Cypress can visit app and create a room
- [ ] **Step 2: Implement `create-room.cy.js`**:
- Visit app, login anonymous, create room, verify room created, join second browser instance (using `cy.origin` or two windows simulation)
- [ ] **Step 3: Implement `gameplay.cy.js`**:
- 2 players join room, ready, game starts; simulate placing grains; call Kinh; verify settlement and balances
- [ ] **Step 4: Implement `concurrent-kinh.cy.js`**:
- 4 players; simulate near-simultaneous Kinh calls (some valid, some invalid); verify final balances match expected penalty distribution
- [ ] **Step 5: Add Cypress to client package.json**; script `"cypress:open"` and `"cypress:run"`
- [ ] **Step 6: Run E2E suite** to ensure passes
- [ ] **Step 7: Commit** "test: Cypress E2E multi-player scenarios"
### Task 24: Docker & Docker Compose for Production-Like Local
**Files:**
- Create: `Dockerfile` (for backend)
- Create: `client/Dockerfile` (for frontend static build)
- Modify: `docker-compose.yml` to include services: `frontend`, `backend`, `postgres`, `redis`
- Create: `.dockerignore`
- [ ] **Step 1: Write failing test** — none
- [ ] **Step 2: Write backend Dockerfile**:
- Node 20 alpine
- Copy package.json, install, copy server/, expose 3001, CMD `node server/index.js`
- [ ] **Step 3: Write frontend Dockerfile**:
- Node 20 alpine; copy client package.json; install; copy client/; `npm run build`; output static files; serve with nginx: `nginx:alpine` and copy build to `/usr/share/nginx/html`
- [ ] **Step 4: Update docker-compose** to build and link services; set env vars; frontend depends on backend; network them
- [ ] **Step 5: Test local docker compose up** — app accessible at http://localhost (frontend) and backend ws at ws://localhost:3001
- [ ] **Step 6: Commit** "feat: Docker and docker-compose for full stack"
### Task 25: GitHub Actions CI
**Files:**
- Create: `.github/workflows/ci.yml`
- [ ] **Step 1: Write failing test** — none
- [ ] **Step 2: Create CI workflow**:
- Triggers on push to main, PRs
- Jobs:
- `backend-tests`: setup Node, run `npm ci`, `npm test` in server/, also lint
- `frontend-tests`: setup Node, run `npm ci`, `npm test` in client/ (if unit tests exist), also `npm run build` to ensure build succeeds
- `e2e-tests`: uses Cypress Docker image; run `npm run cy:run` in client/; needs backend service running (docker compose up -d inside workflow? Use Cypress included services? Simpler: spin up backend via docker compose in before_script)
- Upload artifacts (screenshots, videos) on failure
- [ ] **Step 3: Commit** "ci: GitHub Actions pipeline"
### Task 26: Free Tier Deployment Configuration
**Files:**
- Create: `client/vercel.json` (or rely on defaults)
- Create: `railway.json` for backend (or use Dockerfile)
- Update: `client/.env.production` with VITE_API_URL and VITE_WS_URL pointing to Railway backend URL (which will be set as env at deploy time)
- Create: `server/.env.production.example` with required vars (Railway provides DATABASE_URL etc.)
- [ ] **Step 1: Write failing test** — none
- [ ] **Step 2: Create Vercel config** (optional) for SPA routing redirects
- [ ] **Step 3: Prepare Railway deployment**: ensure Dockerfile present; add `railway.json` if needed (just specify Docker build)
- [ ] **Step 4: Document environment variables** required for production in README
- [ ] **Step 5: Commit** "deploy: free tier configs for Vercel + Railway"
### Task 27: Zalo Mini App Build Script
**Files:**
- Create: `client/package.json` scripts: `"build:zalo": "vite build --config vite.zalo.config.js"`
- Create: `client/vite.zalo.config.js` with appropriate base and output dir `dist/zalo`
- Create: `client/zalo.config.json` (Zalo Mini App manifest) (placeholder)
- Create: `client/src/platforms/zalo/adapter.js` to wrap Zalo SDK calls
- [ ] **Step 1: Write failing test** — none
- [ ] **Step 2: Create Zalo-specific Vite config** that sets base to `./`, outputs to `dist/zalo`, builds as library? (Zalo Mini App expects a single JS file and assets). We'll produce static assets.
- [ ] **Step 3: Create zalo.config.json** with appID, name, version, orientation, etc. (placeholders)
- [ ] **Step 4: Create Zalo adapter** that provides methods: `login()`, `share()`, `getUserInfo()`. Use real SDK when in Zalo environment; fallback to mock in web.
- [ ] **Step 5: Update Auth flow** to use ZaloLoginButton that calls adapter.login; on success, hit backend Zalo OAuth flow
- [ ] **Step 6: Build test**: run `npm run build:zalo` and verify output in `dist/zalo/`
- [ ] **Step 7: Commit** "feat: Zalo Mini App build configuration"
---
## Post-Implementation: Local Development & QA
- Ensure `docker-compose up` brings up everything
- Backend runs on :3001, frontend on :5173 (Vite default)
- Provide instructions in README for running locally and for deployment
---
## Notes for Subagent Workers
- **TDD:** ALWAYS write test FIRST, then code to make it pass. No exceptions.
- **Commits:** Small, logical commits after each step with clear messages.
- **Self-review:** After completing your assigned tasks, run the tests, lint, and check that your code follows the architecture in the spec.
- **Cross-review:** After your task is complete and self-reviewed, you will be asked to review another worker's code. Use the 3-tier review criteria: Plan Alignment, Code Quality, Architecture, Documentation, Issues.
- **Blockers:** If you encounter a dependency on another task that is not yet done, ask the orchestrator to reorder tasks or create a minimal stub.
---
**Plan complete and saved to `docs/superpowers/plans/2026-03-31-lotto-game-full-stack.md`.**
**Execution approach:** Subagent-Driven (you will dispatch fresh subagents per task or small task group). I will now begin spawning subagents for Phase 1 tasks sequentially, ensuring each task is fully implemented, reviewed, and committed before moving to the next.
Let's start with **Task 1: Initialize Backend Project & Docker Compose**.