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

3.9 KiB
Raw Permalink Blame History

06 · 三个示例游戏

做什么

写 3 个游戏验证基础设施能覆盖:

  • DragTest(验证拖拽)
  • War 战争(验证卡牌游戏 + 按钮 + 简单 reducer
  • Mill 九子棋(验证棋类网格 + 多阶段 + 复杂规则)

遇到什么问题

共通:playerID 类型

boardgame.io 0.50.2 的 Ctx.currentPlayerstring,但游戏需要 '0' | '1'

解决:自己定义 PlayerID = '0' | '1',在 move 里强转:

const me = (playerID ?? '0') as PlayerID;

共通:events 不在 ctx 上

最初写 ctx.events.endTurn() 报错。

解决events 是 FnContextDefaultPluginAPIs)字段,不是 Ctx 字段:

moves: {
  flip: ({ G, ctx, events, playerID }) => {
    events.endTurn();  // ✅
  }
}

共通:noUncheckedIndexedAccess 严格模式

TS 严格模式下 arr[i]T | undefined,棋类里大量出现。

解决:兜底 ?? null

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 校验实现。

phases: {
  placing: { start: true, next: 'moving', endIf: (G) => /* 全放完 */ },
  moving: { next: 'flying', endIf: (G, ctx) => /* 当前玩家剩 3 枚 */ },
  flying: {},
}

怎么解决

DragTest5 张扑克 + 拖到桌)

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 翻牌比大小)

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 阶段)

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

关联