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.
140 lines
3.9 KiB
Markdown
140 lines
3.9 KiB
Markdown
# 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: {},
|
||
}
|
||
```
|
||
|
||
## 怎么解决
|
||
|
||
### DragTest(5 张扑克 + 拖到桌)
|
||
|
||
```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];
|
||
},
|
||
}
|
||
```
|
||
|
||
### War(26/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])];
|
||
// ... 收牌
|
||
},
|
||
}
|
||
```
|
||
|
||
### Mill(24 交叉点 + 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) => { ... },
|
||
}
|
||
```
|
||
|
||
## 验证
|
||
|
||
- DragTest:5 牌手牌 → 拖到桌 ✓
|
||
- War:26 牌均分 → 翻牌 → 收牌 ✓(修正后)
|
||
- Mill:24 交叉点 → 落子 → 切换玩家 → 落子 ✓
|
||
|
||
## 成果
|
||
|
||
- 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) — 三个游戏相关的踩坑
|