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
07 · Bug 修复 · 第一轮
做什么
用户手动验证 3 个游戏后报告 2 个 bug:
- War:UI 上看不到第二个玩家的牌堆;翻牌逻辑也有问题
- DragTest:在已经拖到桌的牌上继续拖会重复加;拖走之后拖不回手牌
遇到什么问题
Bug 1:War 看不到 P1 牌堆
根因:4 个 zone 都设了 owner='0' / owner='1',Board 的 owner 过滤逻辑让对方的私有 zone 完全隐藏。
Bug 2:War 翻牌"啥也没看到"
根因:原 flip move 一次完成翻+收,pile 永远只显示 0 张。牌翻出来立刻被赢家收走,玩家看不到"对比"。
Bug 3:DragTest 桌→桌重复加卡
根因 Immer 陷阱:
const fromArr = G[fromKey]; // 假设 fromKey === toKey
const toArr = G[toKey];
G[fromKey] = fromArr.filter(...); // 改了 G[fromKey](也是 G[toKey])
G[toKey] = [...toArr, ...moved]; // toArr 是修改前的旧引用!
当 fromKey === toKey 时,toArr 仍指向过滤前的原数组,所以 [...toArr, ...moved] 把已经"过滤掉"的牌又加回来。
具体场景:桌子有 [2♠],拖 2♠ 又到桌:
fromArr = toArr = [2♠]moved = [2♠]G.table = [2♠].filter(!idSet.has) = []G.table = [...[2♠], 2♠] = [2♠, 2♠]❌ 重复
Bug 4:DragTest 拖不回手牌
根因:hand zone 没配 dropMove,所以 drop 处理器什么都不做。
怎么解决
修复 1:去掉 War zone 的 owner
ui: {
zones: [
{ id: 'p0-deck', type: 'pile', position: { x: 100, y: 50 }, faceDown: true, collection: 'p0Deck' },
{ id: 'p1-deck', type: 'pile', position: { x: 700, y: 50 }, faceDown: true, collection: 'p1Deck' },
// 关键:pile 用 type='discard',渲染时强制 face-up
{ id: 'p0-pile', type: 'discard', position: { x: 300, y: 300 }, collection: 'p0Pile' },
{ id: 'p1-pile', type: 'discard', position: { x: 500, y: 300 }, collection: 'p1Pile' },
],
actions: [
{ label: '翻牌', move: 'flip' },
{ label: '收牌', move: 'collect' },
],
}
修复 2:拆 flip → flip + collect
moves: {
flip: ({ G }) => {
if (G.winner || !G.p0Deck.length || !G.p1Deck.length) return;
// 翻牌到 pile,记录 lastWinner,但不收
G.p0Pile.push(p0CardId);
G.p1Pile.push(p1CardId);
G.lastWinner = ...;
},
collect: ({ G }) => {
// 按 lastWinner 把 pile 收给赢家
if (G.lastWinner === '0') G.p0Deck = [...G.p0Deck, ...shuffle([...G.p0Pile, ...G.p1Pile])];
G.p0Pile = [];
G.p1Pile = [];
G.lastWinner = null;
},
}
UI 上两个按钮:翻牌 → 收牌 → 翻牌 ... 循环。
修复 3:transferCards 同 zone 直接 return
moves: {
transferCards: ({ G }, cardIds, fromZoneId, toZoneId) => {
if (fromZoneId === toZoneId) return; // no-op
// ... 原有逻辑
},
}
修复 4:hand zone 加 dropMove
{
id: 'hand',
type: 'hand',
position: { x: 250, y: 500 },
owner: 'self',
collection: 'hand',
layout: { fan: 0, spacing: 70 },
dropMove: 'transferCards', // ← 新增
dropArgs: (cardIds, fromZoneId, toZoneId) => [cardIds, fromZoneId, toZoneId],
},
验证
Playwright 端到端:
- War 翻牌:4 张牌在桌上可见 → 翻牌 → pile 各 1 张 face-up(K♠ / 8♦)→ 收牌 → p0 deck 25→27
- DragTest 桌→桌:拖 2♠ 到桌(hand=4, table=1)→ 拖 2♠ 又到桌(仍然 hand=4, table=1,不重复)✓
- DragTest 桌→手:拖 2♠ 回手牌(hand=5, table=0)✓
成果
- Commit
e9a7737 fix(war,drag-test): public piles, two-button UX, same-zone no-op
关联
- 05-drag-and-drop.md — 拖拽实现(产生 bug 的源头)
- 06-three-games.md — 三个游戏(bug 暴露的场景)
- lessons-learned.md §19-22 — War 拆 move / Immer 同 key 的详细内容