diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4a7ea30 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..a6769ef --- /dev/null +++ b/.eslintignore @@ -0,0 +1,6 @@ +node_modules +dist +.kilo +*.lock +e2e/ +load/ diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..5014f01 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,45 @@ +module.exports = { + root: true, + env: { + es2021: true, + }, + extends: ['eslint:recommended'], + parserOptions: { + ecmaVersion: 'latest', + }, + overrides: [ + { + files: ['api/**/*.js', '*.config.js', '*.config.mjs', '.eslintrc.js'], + env: { node: true, es2021: true }, + parserOptions: { sourceType: 'script' }, + rules: { + 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + 'no-console': 'off', + }, + }, + { + files: ['pro/**/*.js', 'pro/**/*.jsx'], + env: { browser: true, es2021: true }, + parserOptions: { + sourceType: 'module', + ecmaFeatures: { jsx: true }, + }, + plugins: ['react', 'react-hooks'], + extends: ['eslint:recommended', 'plugin:react/recommended', 'plugin:react-hooks/recommended'], + settings: { react: { version: 'detect' } }, + rules: { + 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + 'react/react-in-jsx-scope': 'off', + 'react/prop-types': 'off', + 'no-console': 'off', + }, + }, + { + files: ['*.test.js', '*.test.jsx'], + env: { mocha: true, jest: true }, + rules: { + 'no-unused-expressions': 'off', + }, + }, + ], +}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6aeb5ff --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,157 @@ +name: CI + +on: + push: + branches: [main, fix/*, feat/*] + pull_request: + branches: [main] + +jobs: + api: + name: API (Server) + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + cache-dependency-path: | + package-lock.json + api/package-lock.json + + - name: Install root dependencies + run: npm ci + working-directory: . + - name: Install API dependencies + run: npm ci + working-directory: api + - name: Lint + run: npx eslint --ext .js . --max-warnings 50 + working-directory: . + - name: Test + run: npm test + working-directory: api + + pro: + name: Frontend + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + cache-dependency-path: | + package-lock.json + pro/package-lock.json + + - name: Install root dependencies + run: npm ci + working-directory: . + - name: Install Frontend dependencies + run: npm ci + working-directory: pro + - name: Lint + run: npx eslint --ext .js,.jsx . --max-warnings 50 + working-directory: . + - name: Test + run: npx vitest run + working-directory: pro + + e2e: + name: E2E (Playwright) + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + cache-dependency-path: | + api/package-lock.json + pro/package-lock.json + + - name: Install API dependencies + run: npm ci + working-directory: api + + - name: Install Frontend dependencies + run: npm ci + working-directory: pro + + - name: Install E2E dependencies + run: npm install + working-directory: e2e + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + working-directory: e2e + + - name: Run E2E tests + run: npx playwright test + working-directory: e2e + env: + CI: true + + - name: Upload Playwright report + uses: actions/upload-artifact@v4 + if: failure() + with: + name: playwright-report + path: e2e/playwright-report/ + retention-days: 7 + + load: + name: Load Test + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + cache-dependency-path: | + api/package-lock.json + + - name: Install API dependencies + run: npm ci + working-directory: api + + - name: Install Load Test dependencies + run: npm install + working-directory: load + + - name: Start API server + run: | + node index.js & + echo "Waiting for server..." + for i in $(seq 1 10); do + curl -s http://localhost:4000/health > /dev/null && echo "Server ready" && break + sleep 1 + done + working-directory: api + + - name: Run load test + run: node runner.js --connections 10 --duration 15 + working-directory: load + env: + LOAD_SERVER_URL: http://localhost:4000 + + - name: Stop API server + run: kill $(lsof -t -i:4000) 2>/dev/null || true + + - name: Upload load report + uses: actions/upload-artifact@v4 + if: always() + with: + name: load-report + path: load/*.log + retention-days: 7 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..2312dc5 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged diff --git a/.kilo/plans/1779339173170-crisp-wizard.md b/.kilo/plans/1779339173170-crisp-wizard.md new file mode 100644 index 0000000..df9d893 --- /dev/null +++ b/.kilo/plans/1779339173170-crisp-wizard.md @@ -0,0 +1,225 @@ +# Comprehensive Fix for DAB (Dots & Boxes) Game + +## Bugs Identified + +### 1. Loading State - No Room Creation/Join UI + +**File:** `pro/src/dab/DabGame.jsx` lines 361-370 + +- Shows "Connecting..." with no buttons to create/join a room +- Missing `handleCreateRoom` and `handleJoinRoom` functions + +### 2. Event Name Mismatches (Critical) + +| Component | Emits | Should Be | +| --------- | ------------------ | --------------------- | +| Backend | `dab_redoRequest` | → `dab_redoRequested` | +| Frontend | `dab_approveRedo` | → `dab_respondRedo` | +| Frontend | `dab_denyRedo` | → `dab_respondRedo` | +| Backend | `dab_redoAccepted` | → `dab_moveUndone` | + +**Verification:** + +- Backend line 174: `dab_redoRequest` +- Frontend line 253: listens for `dab_redoRequested` +- Backend line 14: listens for `dab_respondRedo` +- Frontend lines 707, 717: emit `dab_approveRedo`/`dab_denyRedo` +- Frontend line 264: listens for `dab_moveUndone` +- Backend line 214: emits `dab_redoAccepted` + +### 3. Crash Bug in Redo System + +**File:** `api/game-logic/dab.js` line 198 + +- `respondRedo` tries to destructure `room.redoRequest.lastMove` +- But `requestRedo` (lines 167-172) never stores `lastMove` or `claimedBox` in the request object +- **Result:** `TypeError: Cannot destructure property 'lineType' of 'undefined'` + +### 4. Memory Leak in Frontend + +**File:** `pro/src/dab/DabGame.jsx` line 296 + +- `useEffect` dependency array includes `[socket, players, rows, cols, playLine, playBox]` +- When `players`, `rows`, `cols` change, new socket listeners are added without proper cleanup +- **Result:** Multiple listeners fire for single events + +--- + +## Fix Implementation + +### Phase 1: Backend Fixes (`api/game-logic/dab.js`) + +#### 1.1 Fix Event Name: `dab_redoRequest` → `dab_redoRequested` + +**Line 174:** + +```javascript +// Change from: +this.io.to(room.id).emit('dab_redoRequest', {...}); +// To: +this.io.to(room.id).emit('dab_redoRequested', { requester: room.players[playerIndex].name }); +``` + +#### 1.2 Fix RedoRequest Object - Store Move Data + +**Lines 167-172 - Replace with:** + +```javascript +room.redoRequest = { + requesterId: socket.id, + requesterIndex: playerIndex, + requesterName: room.players[playerIndex].name, + targetId: prevPlayer.id, + targetIndex: prevPlayerIndex, + lastMove: room.lastMove ? { ...room.lastMove } : null, // CRITICAL FIX + claimedBox: room.lastClaimedBox || null, // Track last claimed box +}; +``` + +#### 1.3 Track lastClaimedBox in makeMove + +**After line 299 (after checkBoxes call):** + +```javascript +// Track last claimed box for potential redo: +if (claimedBoxes.length > 0) { + room.lastClaimedBox = claimedBoxes[claimedBoxes.length - 1]; +} +``` + +#### 1.4 Fix Event Name: `dab_redoAccepted` → `dab_moveUndone` + +**Lines 214-221 - Replace with:** + +```javascript +if (accept) { + const { lineType, r, c } = room.redoRequest.lastMove; + if (lineType === 'h') { + room.horizontalLines[r][c] = null; + } else { + room.verticalLines[r][c] = null; + } + const claimedBox = room.redoRequest.claimedBox; + if (claimedBox) { + room.boxes[claimedBox.r][claimedBox.c] = null; + room.scores[room.redoRequest.requesterIndex]--; + } + room.lastMove = null; + room.lastClaimedBox = null; + room.currentTurn = room.redoRequest.targetIndex; + + // Emit full state update that frontend expects: + this.io.to(room.id).emit('dab_moveUndone', { + horizontalLines: room.horizontalLines, + verticalLines: room.verticalLines, + boxes: room.boxes, + scores: [...room.scores], + currentTurn: room.currentTurn, + }); +} +``` + +#### 1.5 Add `dab_startGame` Listener + +**Add in handleConnection (line 15):** + +```javascript +socket.on('dab_startGame', (data) => this.startGame(socket, data)); +``` + +**Add new method:** + +```javascript +startGame(socket, { roomId }) { + const room = this.rooms.get(roomId); + if (!room || room.creator !== socket.id) return; + room.gameState = 'playing'; + this.io.to(roomId).emit('dab_gameStarted', { firstTurn: room.players[0].id }); + this.sendRoomInfo(roomId); +} +``` + +--- + +### Phase 2: Frontend Fixes (`pro/src/dab/DabGame.jsx`) + +#### 2.1 Add Create/Join Room Functions + +**After line 75 (after reconnectAttempted ref):** + +```javascript +const handleCreateRoom = () => { + logger.socket('➡️', 'dab_createRoom', { mode, customRows, customCols, customPlayers, playerName }); + socket.emit('dab_createRoom', { mode, customRows, customCols, customPlayers, playerName }); +}; + +const handleJoinRoom = async () => { + const { value: joinRoomId } = await Swal.fire({ + title: 'Join DAB Room', + input: 'text', + inputPlaceholder: 'Enter Room ID', + showCancelButton: true, + customClass: { popup: 'sketch-popup' }, + }); + if (joinRoomId) { + logger.socket('➡️', 'dab_joinRoom', { roomId: joinRoomId.toUpperCase(), playerName }); + socket.emit('dab_joinRoom', { roomId: joinRoomId.toUpperCase(), playerName }); + } +}; +``` + +#### 2.2 Replace Loading Screen with Lobby UI + +**Lines 361-370 - Replace:** + +```javascript +{!roomId ? ( +