# 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 (1–90) 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**.