- 新增双升(DoubleUp)完整实现:引擎 double-up-core.ts / double-up.ts、牌桌 UI DoubleUpBoard.tsx、单测与 E2E - 甩牌跟牌修复:validateFollow 新增 mixed 分支,甩牌后其他家可正常跟单张同花色牌 - start.sh:PORT=5173 单端口一键启动(Koa 同时 serve 页面+/api+/socket.io),浏览器直连无需 Vite 代理 - 玩家名 server 权威化(join 时写入 G.playerNames);LobbyRoomList 支持 fixedSeats(双升固定 4 人) - 文档:新增 dev-log/14、更新 dev-log README 索引与进度、更新根 README 启动/部署说明 - .gitignore 忽略根目录 data/*.db 运行时 SQLite
8.1 KiB
CODEBUDDY.md
This file provides guidance to CodeBuddy Code when working with code in this repository.
Overview
tts-like is a Tabletop Simulator-like board game platform built on boardgame.io (rules engine + relay server) with a React/SVG web client. The desktop shell uses Tauri (not yet started). The product name shown in the UI is 滑稽社TTS (hxs TTS).
The architecture is a strict 4-layer separation: game definitions (pure boardgame.io Game objects) ← rules engine (@tts-like/engine, pure functions, no UI/network) → network relay (@tts-like/server, server-authoritative) → UI (@tts-like/ui primitives rendered by apps/web).
Repository / Workspace layout
pnpm workspace (pnpm-workspace.yaml globs packages/* and apps/*). Each package/app has its own package.json with scripts; run them via pnpm --filter @tts-like/<name> <script> or pnpm -r <script> (recursive).
packages/engine— Re-exportsboardgame.io, theTTSGametype (boardgame.ioGame+ customuifield), and thecreateLobbyClientlobby SDK. Holds all game definitions insrc/games/*.ts(drag-test,war,mill/Nine Men's Morris,holdem/Texas Hold'em,pig/拱猪). Game logic has vitest unit tests (*.test.ts).packages/protocol— Types only (no runtime). DefinesUISchema,ZoneDef,CardTemplate,ActionDef, and theresolveZoneEntitieshelper used by the UI to mapgame.ui.zonesto entities inG.packages/ui— React UI primitives (Board,Zone,Card,Token,ActionButton,LobbyRoomList). Thin wrappers driven declaratively bygame.ui.packages/server— Node/Koa server wrapping boardgame.io'sServerwith:/api-prefixed lobby routes, custom/api/rooms/*room management (delete/heartbeat/kick), an auth system (SQLite + JWT-style tokens), and static file serving ofapps/web/distin production.apps/web— Vite + React SPA.src/App.tsxis the single entry; it registers games in theGAMESmap, handles local vs online modes, login modal, and mounts per-game Board components (src/*Board.tsxfor Mill/Holdem/Pig; genericBoardfrom@tts-like/uifor DragTest/War). Has Playwright E2E ine2e/.apps/desktop— Tauri shell scaffold (not yet active — onlypackage.jsonscripts exist).docs/—impl.md(high-level plan),ui-primitives.md(UI API spec),dev-log/(chronological build log:README.mdindex,decisions.md,lessons-learned.md).
Common commands
Requires Node ≥ 22.12 and pnpm ≥ 10.16 (enable via corepack enable).
pnpm install # install workspace deps
# Dev: two processes (terminal 1 + terminal 2), or use run.sh for one-shot:
./run.sh # starts server (:8000) + vite (:5173) via nvm
pnpm --filter @tts-like/server dev # terminal 1: tsx watch relay server
pnpm dev # terminal 2: vite web (:5173); proxy → :8000
# Build
pnpm -r build # build all packages + web (vite build → apps/web/dist)
pnpm --filter @tts-like/server build # tsc → packages/server/dist
# Type check / tests / lint
pnpm -r ts # type-check every package
pnpm -r test # vitest run (engine + server unit tests)
pnpm --filter @tts-like/engine test # single package test (vitest run)
pnpm lint # eslint .
# E2E (Playwright, auto-starts server + web)
pnpm test:e2e # = pnpm --filter @tts-like/web test:e2e
npx playwright install # first-time Chromium download
pnpm verify # ts && test && test:e2e
# Production
docker compose up --build # server image (packages/server/Dockerfile)
To run a single unit test file: pnpm --filter @tts-like/engine exec vitest run src/games/war.test.ts. To run a single E2E spec: pnpm --filter @tts-like/web exec playwright test e2e/war-multiplayer.spec.ts.
Key architectural concepts
Game definitions = boardgame.io Game + ui field
Each game in packages/engine/src/games/*.ts exports a TTSGame (a boardgame.io Game plus a custom ui field that the UI consumes — boardgame.io ignores it). ui declares zones, cards template (SVG front/back), actions (buttons → moves), and optional overrides. The DragTest game is the minimal reference for this pattern (drag a card between zones via the transferCards move).
Adding a game requires three coordinated edits: (1) create packages/engine/src/games/<name>.ts, (2) register it in packages/server/src/index.ts (both the Server({ games: [...] }) array and the GAMES_ENTRIES array — these are separate and both needed), and (3) add an entry to the GAMES map in apps/web/src/App.tsx (with engineName matching the boardgame.io name, maxSeats, optional seatChoices/setupOptions).
UI is data-driven by game.ui
packages/ui/src/Board.tsx iterates game.ui.zones, resolves each zone's entities from G via resolveZoneEntities (protocol package; reads zone.collection for the ID array, zone.entityField for the ID→object map, defaulting to 'drawPile'), and renders <Zone>/<Card>. Drag-drop calls zone.dropMove with dropArgs. Player-view filtering (hiding opponents' cards) is handled entirely by boardgame.io's playerView server-side — the UI must NOT re-implement it. Holdem/Mill/Pig use bespoke Board components in apps/web/src/*Board.tsx instead of the generic Board.
Unified-port networking (dev-log #12)
The web (:5173) is the only port the browser/LAN sees. Vite proxies /api (→ :8000/api, rewritten to strip /api by mountApiPrefix) and /socket.io (ws) to the server (:8000). In production the server serves apps/web/dist itself, so a single port works. createLobbyClient('') uses same-origin /api automatically. ALLOWED_ORIGINS defaults to LAN ranges when unset (dev); set it explicitly in production.
Server is server-authoritative relay, not just boardgame.io
packages/server/src/index.ts mounts, in order: /healthz → room management (/api/rooms/*, must be registered before mountApiPrefix so raw /api/rooms paths are handled) → auth (/api/auth/*) → a gateway forcing login for create/join → mountApiPrefix (rewrites /api/* → boardgame.io router) → static serve. Room state, presence, and the single-room-per-user constraint are all in-memory maps in rooms.ts (lost on restart). Auth uses Node 22 built-in node:sqlite (auth-db.ts).
Player accounts & single-room constraint (dev-log #8/#12)
Create/join require a JWT-style token (HMAC-signed, 30-day TTL) from /api/auth/login or /api/auth/register. A logged-in user can only be in one room at a time (409 if already in another). rooms.ts enforces this and tracks userRoom/roomPlayers; /api/auth/me returns the current room so the client auto-reconnects. Admin users (preset e2hang/evan1115, overridable via env) can delete/kick in any room.
Conventions & gotchas
- TypeScript strict with
noUncheckedIndexedAccess; target ES2022,moduleResolution: Bundler.packages/*are consumed via source (exportspoint tosrc/*.ts), so no build step is needed to use them — justpnpm install. - boardgame.io version is pinned at
0.50.2everywhere — keep it in sync across packages. packages/serveruses ESM ("type": "module") with.jsimport specifiers (e.g.import './rooms.js') even though source is.ts— required fortsx/node --import tsx.- Do not copy
node_modulesbetween machines; native deps (esbuild, Playwright browsers) are OS/arch-specific. - Game
movesmutateGdirectly (no Immer) —drag-test.tsnotes a past bug where local-variable aliasing caused duplicate card inserts; always create new arrays when moving entities. GAME_ORDERinApp.tsxcontrols the game dropdown order and default selection — edit there, not hardcoded elsewhere.- Detailed historical decisions and pitfalls live in
docs/dev-log/decisions.mdanddocs/dev-log/lessons-learned.md— consult before non-trivial changes.