OpenClaw + ClawTeam + Superpowers Git Initialization

This commit is contained in:
2026-04-01 10:15:43 +07:00
parent 356c18ac75
commit b49ba1a719
258 changed files with 50192 additions and 0 deletions
@@ -0,0 +1,580 @@
# 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**.
@@ -0,0 +1,269 @@
# Portfolio Tết Theme - Design Spec
**Date:** 2026-03-31
**Author:** Sagi (Orchestrator AI)
**Status:** Draft → Approved by user
---
## 1. Overview
Build a personal portfolio website with traditional Tết (Vietnamese Lunar New Year) theme: red/gold colors, falling petals, subtle fireworks, lion dance motifs. Multi-page with clear routing. Responsive and smooth animations.
**Tech Stack:**
- Next.js 14+ (latest) with App Router
- TypeScript
- Tailwind CSS
- Framer Motion
- Deploy target: Vercel (recommended)
---
## 2. Project Structure
```
clawteam-portfolio/
├── app/
│ ├── layout.tsx
│ ├── page.tsx (Home)
│ ├── projects/
│ │ └── page.tsx
│ ├── my-corner/
│ │ ├── page.tsx (Blog list)
│ │ └── [slug]/
│ │ └── page.tsx (Single post)
│ └── globals.css
├── components/
│ ├── Header.tsx
│ ├── Footer.tsx
│ ├── Hero.tsx
│ ├── About.tsx
│ ├── Experience.tsx
│ ├── Contact.tsx
│ ├── ProjectCard.tsx
│ ├── BlogCard.tsx
│ ├── TetEffects.tsx (falling petals, fireworks)
│ └── ui/ (buttons, inputs, etc.)
├── data/
│ ├── profile.ts (personal info from CV)
│ ├── experience.ts
│ ├── projects.ts
│ └── blogPosts.ts
├── lib/
│ └── utils.ts
├── public/
│ ├── avatar.jpeg
│ └── images/...
├── styles/
│ └── tailwind.css (if using @tailwind directive)
├── next.config.js
├── tsconfig.json
├── package.json
└── README.md
```
---
## 3. Pages & Routes
### `/` (Home) - Single page with sections
- Hero (avatar + intro)
- About
- Experience (timeline)
- Contact (form + info)
### `/projects` (Projects)
- Grid of project cards
### `/my-corner` (Blog list)
- List of personal posts
### `/my-corner/[slug]` (Single post)
- Full content page
---
## 4. Content Mapping (from CV & avatar)
### Profile Data (`data/profile.ts`)
```ts
{
name: "Nguyễn Ngọc Trí Vĩ",
title: "AI Engineer",
avatar: "/avatar.jpeg",
email: "nntrivi2001@gmail.com",
phone: "090 321 5095",
linkedin: "https://linkedin.com/in/nguyen-ngoc-tri-vi",
portfolio: "https://nguyenngoctrivi.vercel.app",
languages: ["Vietnamese (native)", "English (intermediate)"],
bio: `Short bio...`
}
```
### Experience Data (`data/experience.ts`)
Array of 4 positions with dates, company, role, description bullets.
### Projects Data (`data/projects.ts`)
From CV "Portfolio & Chứng chỉ" section. Initially minimal: title, description, tech stack, links (demo/GitHub). Can be expanded later.
### Blog Data (`data/blogPosts.ts`)
Initial placeholder posts with personal content (photos, memories, experiences). Slug-based.
---
## 5. Components Detail
#### Header
- Fixed top, transparent background with blur
- Logo (text "Trí Vĩ") left
- Nav links: Home, Projects, My Corner
- Mobile hamburger menu
- Active section highlight (Home only)
#### Footer
- Copyright: © 2025 - 2026 Nguyễn Ngọc Trí Vĩ
- Social icons (LinkedIn, GitHub, etc.)
#### Hero
- Large avatar (circular or rounded square)
- Name + title
- Short intro line
- Call-to-action buttons: "View Projects", "Contact"
- Background: gradient red/yellow
- TetEffects overlay (falling petals)
#### About
- Two-column: image (avatar or personal photo) + text
- Text: bio, languages, contact summary
#### Experience (Timeline)
- Vertical line with nodes
- Each node: date, company, role, bullet points
- Alternating left/right or single column left-aligned
- Framer Motion: slide in on scroll
#### Contact
- Simple form: name, email, message
- Submit: show success message + confetti effect (Framer Motion)
- Also display email, phone, LinkedIn as text/links
#### ProjectCard
- Image/thumbnail
- Title, description
- Tech badges
- Links: Demo, GitHub
- Hover: lift + shadow
#### BlogCard
- Optional image
- Title, date, excerpt
- "Read more" link
#### TetEffects
- Canvas-based falling petals (pink/red, slow drift)
- Subtle random fireworks (sparkles) on scroll or idle
- Decorative lion/dragon silhouettes in header/footer (SVG)
---
## 6. Styling & Theme
**Colors (Tailwind config):**
```js
colors: {
primary: {
DEFAULT: '#D32F2F', // Red
50: '#FFEBEE',
100: '#FFCDD2',
// ...
},
accent: {
DEFAULT: '#FFC107', // Gold/Yellow
// ...
},
// Neutrals...
}
```
**Fonts:**
- Headings: `Playfair Display` (or `Noto Serif Vietnamese`)
- Body: `Inter` (or `Noto Sans Vietnamese`)
- Load via Google Fonts in `layout.tsx`
**Responsive breakpoints:**
- Mobile: < 768px (stack, hamburger)
- Tablet: 768px - 1024px (2-col grid for projects)
- Desktop: > 1024px (3-col grid, full layout)
---
## 7. Animations (Framer Motion)
- **Page transitions:** fade in/out between routes
- **Scroll reveal:** sections slide up + fade when entering viewport (use `IntersectionObserver` or `framer-motion`'s `whileInView`)
- **Hero:** avatar pulse or float
- **Timeline items:** stagger entrance
- **Project cards:** hover scale + shadow
- **Contact form:** input focus transitions
- **TetEffects:** canvas animations; optional subtle CSS keyframes for decorative elements
---
## 8. Data Management
All content stored in `data/` as TypeScript modules. Easy to edit without touching components.
Example `data/profile.ts`:
```ts
export const profile = {
name: "Nguyễn Ngọc Trí Vĩ",
title: "AI Engineer",
avatar: "/avatar.jpeg",
email: "nntrivi2001@gmail.com",
phone: "090 321 5095",
linkedin: "https://linkedin.com/in/nguyen-ngoc-tri-vi",
portfolio: "https://nguyenngoctrivi.vercel.app",
languages: ["Vietnamese (native)", "English (intermediate)"],
bio: `A passionate AI Engineer...`,
};
```
---
## 9. Deployment
- `next build``out/` or Vercel automatic
- Environment: Node.js 18+
- Vercel: import repo, set build command `next build`, output `next start`
- Custom domain: optional
---
## 10. Future Enhancements (Out of Scope for v1)
- Blog CMS (Markdown files or headless CMS)
- Dark mode
- i18n (Vietnamese/English toggle)
- More advanced Tet animations (lion dance interactive)
- Testimonials section
- Resume PDF download
---
## 11. Acceptance Criteria
- [ ] Next.js project initializes and runs (`npm run dev`)
- [ ] Tailwind configured and functional
- [ ] All 3 routes render correctly
- [ ] Content from CV is accurately represented
- [ ] Responsive on mobile/tablet/desktop
- [ ] Tet theme applied (colors, fonts, decorations)
- [ ] Animations smooth (60fps)
- [ ] Avatar image displays
- [ ] Contact form shows success on submit (no backend needed)
- [ ] No console errors
- [ ] Ready to deploy to Vercel
---
**Spec approved by user on 2026-03-31. Proceed to implementation planning.**