# 07 · Bug 修复 · 第一轮 ## 做什么 用户手动验证 3 个游戏后报告 2 个 bug: 1. **War**:UI 上看不到第二个玩家的牌堆;翻牌逻辑也有问题 2. **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 陷阱**: ```ts 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 ```ts 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 ```ts 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 ```ts moves: { transferCards: ({ G }, cardIds, fromZoneId, toZoneId) => { if (fromZoneId === toZoneId) return; // no-op // ... 原有逻辑 }, } ``` ### 修复 4:hand zone 加 dropMove ```ts { 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](./05-drag-and-drop.md) — 拖拽实现(产生 bug 的源头) - [06-three-games.md](./06-three-games.md) — 三个游戏(bug 暴露的场景) - [lessons-learned.md §19-22](./lessons-learned.md) — War 拆 move / Immer 同 key 的详细内容