Files
huajishe-tts/docs/dev-log/06-three-games.md
Claude 24822d0d6a docs(dev-log): add chronological development log
Per user request: split the development process into per-topic docs under
docs/dev-log/. Each entry follows the same structure:
  What was asked → Problems → How solved

Files:
- README.md            index
- 01-initial-scope-and-design.md
- 02-ui-primitives-api-design.md
- 03-monorepo-scaffold.md
- 04-ui-primitive-impl.md
- 05-drag-and-drop.md
- 06-three-games.md
- 07-bugfixes-round-1.md
- lessons-learned.md   all gotchas consolidated
- decisions.md         locked decisions with dates

Total: 1071 lines across 10 files.
2026-08-17 16:49:09 +08:00

140 lines
3.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 06 · 三个示例游戏
## 做什么
写 3 个游戏验证基础设施能覆盖:
- **DragTest**(验证拖拽)
- **War 战争**(验证卡牌游戏 + 按钮 + 简单 reducer
- **Mill 九子棋**(验证棋类网格 + 多阶段 + 复杂规则)
## 遇到什么问题
### 共通:`playerID` 类型
boardgame.io 0.50.2 的 `Ctx.currentPlayer``string`,但游戏需要 `'0' | '1'`
**解决**:自己定义 `PlayerID = '0' | '1'`,在 move 里强转:
```ts
const me = (playerID ?? '0') as PlayerID;
```
### 共通:`events` 不在 ctx 上
最初写 `ctx.events.endTurn()` 报错。
**解决**events 是 `FnContext``DefaultPluginAPIs`)字段,不是 `Ctx` 字段:
```ts
moves: {
flip: ({ G, ctx, events, playerID }) => {
events.endTurn(); // ✅
}
}
```
### 共通:`noUncheckedIndexedAccess` 严格模式
TS 严格模式下 `arr[i]``T | undefined`,棋类里大量出现。
**解决**:兜底 `?? null`
```ts
const cell: Cell = G.board[i] ?? null;
```
### Mill 特有:棋类不适合用通用 Zone/Card
九子棋 24 个交叉点如果做成 24 个 zone 会有大量 React 组件实例化开销,没有 click handler 共享。
**解决**:写自定义 `<MillBoard>` 组件,整个棋盘一个大 SVG + 24 个 `<g>`
### Mill 特有mill 检测
16 条三连线outer 4 + middle 4 + inner 4 + cross 4。每次落子后要检查是否成 mill。
**解决**:预计算 `MILLS` 数组 + `isMillAt(board, pos, player)` helper。
### Mill 特有3 阶段 (placing/moving/flying)
通过 boardgame.io 的 `phases` 配置 + move 内 `ctx.phase` 校验实现。
```ts
phases: {
placing: { start: true, next: 'moving', endIf: (G) => /* 全放完 */ },
moving: { next: 'flying', endIf: (G, ctx) => /* 当前玩家剩 3 枚 */ },
flying: {},
}
```
## 怎么解决
### DragTest5 张扑克 + 拖到桌)
```ts
moves: {
transferCards: ({ G }, cardIds, fromZoneId, toZoneId) => {
const fromArr = G[fromZoneId];
const toArr = G[toZoneId];
if (!Array.isArray(fromArr) || !Array.isArray(toArr)) return;
const idSet = new Set(cardIds);
const moved = fromArr.filter(id => idSet.has(id));
G[fromZoneId] = fromArr.filter(id => !idSet.has(id));
G[toZoneId] = [...toArr, ...moved];
},
}
```
### War26/26 翻牌比大小)
```ts
moves: {
flip: ({ G }) => {
if (G.winner || !G.p0Deck.length || !G.p1Deck.length) return;
const p0CardId = G.p0Deck[G.p0Deck.length - 1]!;
const p1CardId = G.p1Deck[G.p1Deck.length - 1]!;
const p0Card = G.drawPile[p0CardId]!;
const p1Card = G.drawPile[p1CardId]!;
G.p0Deck = G.p0Deck.slice(0, -1);
G.p1Deck = G.p1Deck.slice(0, -1);
G.p0Pile.push(p0CardId);
G.p1Pile.push(p1CardId);
if (p0Card.rank > p1Card.rank) G.lastWinner = '0';
else if (p1Card.rank > p0Card.rank) G.lastWinner = '1';
else G.lastWinner = 'tie';
},
collect: ({ G }) => {
if (G.lastWinner === '0') G.p0Deck = [...G.p0Deck, ...shuffle([...G.p0Pile, ...G.p1Pile])];
// ... 收牌
},
}
```
### Mill24 交叉点 + 16 mills + 3 阶段)
```ts
moves: {
placeAt: ({ G, ctx, events, playerID }, pos) => {
if (ctx.phase !== 'placing') return;
if (isMillAt(G.board, pos, me)) G.mustRemove = true;
else events.endTurn();
},
selectFrom: ({ G, ctx, playerID }, pos) => { ... },
moveTo: ({ G, ctx, events, playerID }, to) => { ... },
removeAt: ({ G, events, playerID }, pos) => { ... },
}
```
## 验证
- DragTest5 牌手牌 → 拖到桌 ✓
- War26 牌均分 → 翻牌 → 收牌 ✓(修正后)
- Mill24 交叉点 → 落子 → 切换玩家 → 落子 ✓
## 成果
- Commit `ab2a979 feat(mill): Nine Men's Morris game + custom SVG board`
- 3 个游戏都能在浏览器里跑(虽然都有初始 bug详见 07
## 关联
- [07-bugfixes-round-1.md](./07-bugfixes-round-1.md) — 用户手动验收发现的 bug
- [lessons-learned.md §13-18](./lessons-learned.md) — 三个游戏相关的踩坑