Files
huajishe-tts/CODEBUDDY.md
e2hang ddcb28472c feat(double-up): 双升游戏 + 单端口 start.sh 部署 + 文档统一更新
- 新增双升(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
2026-08-30 02:11:04 +08:00

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-exports boardgame.io, the TTSGame type (boardgame.io Game + custom ui field), and the createLobbyClient lobby SDK. Holds all game definitions in src/games/*.ts (drag-test, war, mill/Nine Men's Morris, holdem/Texas Hold'em, pig/拱猪). Game logic has vitest unit tests (*.test.ts).
  • packages/protocolTypes only (no runtime). Defines UISchema, ZoneDef, CardTemplate, ActionDef, and the resolveZoneEntities helper used by the UI to map game.ui.zones to entities in G.
  • packages/ui — React UI primitives (Board, Zone, Card, Token, ActionButton, LobbyRoomList). Thin wrappers driven declaratively by game.ui.
  • packages/server — Node/Koa server wrapping boardgame.io's Server with: /api-prefixed lobby routes, custom /api/rooms/* room management (delete/heartbeat/kick), an auth system (SQLite + JWT-style tokens), and static file serving of apps/web/dist in production.
  • apps/web — Vite + React SPA. src/App.tsx is the single entry; it registers games in the GAMES map, handles local vs online modes, login modal, and mounts per-game Board components (src/*Board.tsx for Mill/Holdem/Pig; generic Board from @tts-like/ui for DragTest/War). Has Playwright E2E in e2e/.
  • apps/desktop — Tauri shell scaffold (not yet active — only package.json scripts exist).
  • docs/impl.md (high-level plan), ui-primitives.md (UI API spec), dev-log/ (chronological build log: README.md index, 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 (exports point to src/*.ts), so no build step is needed to use them — just pnpm install.
  • boardgame.io version is pinned at 0.50.2 everywhere — keep it in sync across packages.
  • packages/server uses ESM ("type": "module") with .js import specifiers (e.g. import './rooms.js') even though source is .ts — required for tsx/node --import tsx.
  • Do not copy node_modules between machines; native deps (esbuild, Playwright browsers) are OS/arch-specific.
  • Game moves mutate G directly (no Immer) — drag-test.ts notes a past bug where local-variable aliasing caused duplicate card inserts; always create new arrays when moving entities.
  • GAME_ORDER in App.tsx controls the game dropdown order and default selection — edit there, not hardcoded elsewhere.
  • Detailed historical decisions and pitfalls live in docs/dev-log/decisions.md and docs/dev-log/lessons-learned.md — consult before non-trivial changes.