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.
3.9 KiB
3.9 KiB
06 · 三个示例游戏
做什么
写 3 个游戏验证基础设施能覆盖:
- DragTest(验证拖拽)
- War 战争(验证卡牌游戏 + 按钮 + 简单 reducer)
- Mill 九子棋(验证棋类网格 + 多阶段 + 复杂规则)
遇到什么问题
共通:playerID 类型
boardgame.io 0.50.2 的 Ctx.currentPlayer 是 string,但游戏需要 '0' | '1'。
解决:自己定义 PlayerID = '0' | '1',在 move 里强转:
const me = (playerID ?? '0') as PlayerID;
共通:events 不在 ctx 上
最初写 ctx.events.endTurn() 报错。
解决:events 是 FnContext(DefaultPluginAPIs)字段,不是 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: {},
}
怎么解决
DragTest(5 张扑克 + 拖到桌)
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 翻牌比大小)
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 阶段)
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 — 用户手动验收发现的 bug
- lessons-learned.md §13-18 — 三个游戏相关的踩坑