# 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 共享。 **解决**:写自定义 `` 组件,整个棋盘一个大 SVG + 24 个 ``。 ### 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) — 三个游戏相关的踩坑