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.
128 lines
3.9 KiB
Markdown
128 lines
3.9 KiB
Markdown
# 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 的详细内容
|