diff --git a/docs/agents/card_player_model.md b/docs/agents/card_player_model.md index ec04bfb..a6da7d5 100644 --- a/docs/agents/card_player_model.md +++ b/docs/agents/card_player_model.md @@ -28,7 +28,9 @@ ## BaseCard:静态元数据 - `BaseCard` 构造时通过 `CardConfig.GetInstance().getCard(id)` 自动拉取 `name`、`color`、 - `number`、`type`、`ncn`。 + `number`、`type`、`spellId`、`ncn`。 +- `spellId` 是卡牌自身技能 ID,匿名牌或配置缺失时为 `0`;与移动协议、标记区使用的 + `Card.spellID` 含义不同。焚巢筛牌与红黑杀统计使用此字段识别牌种。 - `setCardInfo(id)` 是元数据刷新入口;匿名槽物化为真实身份时由 `Card.materializeIdentity()` 调用,避免外部直接改写内部展示字段。 diff --git a/docs/protocols/PubGsCMoveCard-3752.md b/docs/protocols/PubGsCMoveCard-3752.md new file mode 100644 index 0000000..f5f2e0b --- /dev/null +++ b/docs/protocols/PubGsCMoveCard-3752.md @@ -0,0 +1,55 @@ +# 焚巢(SpellID=3752):从弃牌堆获得牌 + +## 协议样例 + +```json +{ + "CardCount": 2, + "CardIDs": [], + "FromID": 255, + "FromPosition": 65282, + "FromZone": 2, + "FromZoneParam": 0, + "MoveType": 18, + "SpellID": 3752, + "ToID": 7, + "ToPosition": 65280, + "ToZone": 5, + "ToZoneParam": 0 +} +``` + +`FromZone=2` 表示弃牌堆,`FromID=255` 是公共区占位;`ToZone=5`、`ToID=7` 表示进入座位 7 +的手牌。`MoveType=18` 为获得,`FromPosition=65282` 表示协议未给出精确来源位置。 + +## 当前推断规则 + +根据实测获得结果反推,焚巢从当前弃牌堆中优先获得**最早进入弃牌堆**的以下牌: + +- 杀,包括火杀、雷杀、冰杀。 +- 决斗、南蛮入侵、万箭齐发、火攻。 + +所有符合条件的牌共用入堆顺序,不按牌名设优先级,也不限制每种牌只取一张。获得张数以 +`CardCount` 为准;牌离开弃牌堆后再次进入时,按重新入堆的顺序处理。 + +运行时按 `BaseCard.spellId` 识别目标牌:杀(含属性杀)为 `1`、决斗为 `8`、南蛮入侵为 `9`、 +万箭齐发为 `10`、火攻为 `83`,不依赖牌名或简称。此字段与移动协议、标记区使用的 +`Card.spellID` 不同。 +弃牌区数组按底到顶保存,因此正向筛选即可选择最早入堆的牌。 + +## 适配边界 + +- 仅处理 `SpellID=3752`、`FromZone=2`、`ToZone=5`、`MoveType=18`、 + `FromPosition=65282`,且 `CardIDs` 没有正 ID 的事件。 +- 保留原始 `CardIDs`,通过 `sourceCards` 指定已有弃牌实体,交由通用移动同步手牌数、位置和明牌。 +- 协议已给出正 ID 时,以协议为准;无匹配牌时保持默认移动路径。 +- 本地可识别的目标牌不足时,复用通用来源补足规则,不重复消费选中实体。这不能保证缺失牌面或 + 缺失弃牌历史下的推断准确性。 +- 此规则来自当前样例及结果观察,后续有反例时应同步调整筛选规则与回归测试。 + +## 代码与验证 + +- 技能装饰:`src/tracker/skill/FenChao.ts` 的 `decorateFenChao`。 +- 默认注册:`src/tracker/runtime/moveEventHandlers.ts` 的 `registerDefaultMoveEventHandlers`。 +- 回归:`tests/tracker/fenChao.test.ts`。 +- 协议索引:[README.md](README.md)。 diff --git a/docs/protocols/README.md b/docs/protocols/README.md index 7b06083..6979e93 100644 --- a/docs/protocols/README.md +++ b/docs/protocols/README.md @@ -15,6 +15,12 @@ | [`move-position.md`](move-position.md) | `PubGsCMoveCard` 目标位置 | 公共区顶、底、随机与普通数值精确插槽 | `ToPosition` 通用解析;普通整数按目标区底端零基定位 | | [`hand-exchange.md`](hand-exchange.md) | `PubGsCMoveCard` 整手交换 | 双方整手牌经 `exchange(10)` 互易;示例 SpellID=`121` | `MoveType=11` + `5<->10` + 整手张数;不绑单一 SpellID | +## `PubGsCMoveCard` + +| 文档 | SpellID | 场景 | 关键识别 | +| --- | ---: | --- | --- | +| [焚巢](PubGsCMoveCard-3752.md) | `3752` | 弃牌堆取目标牌 | `2 -> 5`、获得、随机、无正 ID | + ## `GsCRoleOptTargetNtf` | 文档 | SpellID | 场景 | 关键识别 | diff --git a/src/handler/GsCUpdateRoleDataExNtf.js b/src/handler/GsCUpdateRoleDataExNtf.js index 9cca56d..a14188d 100644 --- a/src/handler/GsCUpdateRoleDataExNtf.js +++ b/src/handler/GsCUpdateRoleDataExNtf.js @@ -75,6 +75,7 @@ export function handleUpdateRoleDataExNtf(msg) { // OPT_DATA_UPDATE_NEW_SPELL case 17: + // 飞扬技能通过此消息获得 if (!Array.isArray(Datas)) break if (SeatID !== undefined && SeatID == Game.myID && import.meta.env.DEV) { console.info(msg) diff --git a/src/index.js b/src/index.js index 49504e9..2caa5fa 100644 --- a/src/index.js +++ b/src/index.js @@ -56,5 +56,7 @@ function main() { } main('INIT').then((r) => { - if (r) window._SGSMODULE.push(main) + if (r) { + window._SGSMODULE.push(main) + } }) diff --git a/src/logic.js b/src/logic.js index c313d4f..5c19daf 100644 --- a/src/logic.js +++ b/src/logic.js @@ -156,6 +156,7 @@ export function logic(msg) { if (ProtoObj.matchName === '单骑无双') { Game.isRoguelike1v1 = true + Game.needShowName = true return } @@ -179,7 +180,7 @@ export function logic(msg) { case 'decodeGsClientUserSeatFlagNtf': // 新录像两个消息都有 旧录像只有这个消息 handleRecordStartGame(msg) - if (Game.needShowName && globalConfig.showNameSwitch) laya.showName() + if (globalConfig.showNameSwitch && Game.needShowName) laya.showName() break case 'GsCUpdateRoleDataNtf': diff --git a/src/runtime/gameAdapter.js b/src/runtime/gameAdapter.js index 2da92c3..5c73def 100644 --- a/src/runtime/gameAdapter.js +++ b/src/runtime/gameAdapter.js @@ -1,4 +1,5 @@ import { Game } from '@/tracker/Game' +import { wait } from '@/utils' import { addTooltip } from '@/utils/notification' // 每回合都可以发动 @@ -499,8 +500,17 @@ export class GameRuntime { return foundWindow ?? null } - showName() { - this.gamescene?.seatContainer?.seatUIs?.forEach(({ seat, otherTopManager }) => { + HideStartLoadingListener() { + const GameEventDispatcher = this.ged + if (!GameEventDispatcher) return + GameEventDispatcher.on('HIDE_CHALLENGE_GAME_START', this, this.showName) + } + + async showName() { + const seatUIs = await wait(() => this.gamescene?.seatContainer?.seatUIs) + if (!seatUIs) return + + seatUIs.forEach(({ seat, otherTopManager }) => { if (seat?.playerInfo?.ClientId >= 4e9) return otherTopManager?.createPlayerNameBg() otherTopManager?.createPlayerName() diff --git a/src/tracker/BaseCard.ts b/src/tracker/BaseCard.ts index 2613a06..ad6d0d4 100644 --- a/src/tracker/BaseCard.ts +++ b/src/tracker/BaseCard.ts @@ -11,6 +11,8 @@ export class BaseCard { declare color: number declare number: number declare type: number + /** 卡牌自身技能 ID,与运行时 Card.spellID 区分。 */ + declare spellId: number declare ncn: string /** @@ -30,6 +32,7 @@ export class BaseCard { this.color = cardInfo?.color ?? 0 this.number = cardInfo?.number ?? 0 this.type = cardInfo?.type ?? 0 + this.spellId = cardInfo?.spellId ?? 0 this.ncn = cardInfo?.ncn ?? this.name return this } diff --git a/src/tracker/CardCounter.ts b/src/tracker/CardCounter.ts index 65699b9..8a49257 100644 --- a/src/tracker/CardCounter.ts +++ b/src/tracker/CardCounter.ts @@ -189,7 +189,7 @@ export class CardCounter { this.numberIndex[0].add(cardID) } - if (instance.name.match(/^(冰|火|雷)?杀$/)) { + if (instance.spellId === 1) { this.colorIndex[Math.ceil(instance.color / 2) + 4].add(cardID) } diff --git a/src/tracker/runtime/moveEventHandlers.ts b/src/tracker/runtime/moveEventHandlers.ts index 56bcd61..815a19e 100644 --- a/src/tracker/runtime/moveEventHandlers.ts +++ b/src/tracker/runtime/moveEventHandlers.ts @@ -1,5 +1,6 @@ import { POSITION_RANDOM } from '../candidate/cardPositions' import type { Room } from '../Room' +import decorateFenChao from '../skill/FenChao' import decorateGuanXu, { isGuanXuSpellID } from '../skill/GuanXu' import decorateHandExchange from '../skill/HandExchange' import decorateJieLi from '../skill/JieLi' @@ -125,6 +126,9 @@ export function registerDefaultMoveEventHandlers(room: Room): void { //【思泣】:协议不公开返回牌 ID,按弃牌堆顺序筛选红牌实体作为明确来源。 room.registerMoveEventHandler(3543, decorateSiQi) + //【焚巢】:从弃牌堆暗取时,优先最早入堆的杀、决斗、南蛮、万箭和火攻。 + room.registerMoveEventHandler(3752, decorateFenChao) + // 魔吕布【夺炁】: 3730/3731 获取修正。 room.registerMoveEventHandler(3730, decorateDuoQiMove) room.registerMoveEventHandler(3731, decorateDuoQiMove) diff --git a/src/tracker/skill/FenChao.ts b/src/tracker/skill/FenChao.ts new file mode 100644 index 0000000..33e0920 --- /dev/null +++ b/src/tracker/skill/FenChao.ts @@ -0,0 +1,50 @@ +import { POSITION_RANDOM } from '../candidate/cardPositions' +import type { Card } from '../Card' +import { MOVE_TYPE } from '../MoveEventNormalizer' +import { PROTOCOL_ZONE } from '../protocolZones' +import type { Room } from '../Room' +import { + getCount, + getRaw, + hasPositiveID, + nextGroupID, + patchEvent, + type MoveEventDraft +} from './moveEventUtils' + +const FEN_CHAO_SPELL_ID = 3752 +// 杀(含属性杀)、决斗、南蛮入侵、万箭齐发、火攻。 +const FEN_CHAO_CARD_SPELL_IDS = new Set([1, 8, 9, 10, 83]) + +export default function decorateFenChao(event: MoveEventDraft, room: Room): MoveEventDraft { + const raw = getRaw(event) + if ( + hasPositiveID(event.cardIDs) || + Number(raw.FromZone) !== PROTOCOL_ZONE.DISCARD || + Number(raw.ToZone) !== PROTOCOL_ZONE.HAND || + Number(raw.MoveType) !== MOVE_TYPE.GAIN || + Number(raw.FromPosition) !== POSITION_RANDOM + ) { + return event + } + + const cardCount = getCount(event) + if (cardCount === 0) return event + + const sourceCards: Card[] = [] + // 按实测结果推断焚巢优先获得最早入堆的目标牌;弃牌数组按底 -> 顶保存。 + for (const card of room.zones.get('discard')?.cards ?? []) { + if (!FEN_CHAO_CARD_SPELL_IDS.has(card.spellId)) continue + sourceCards.push(card) + if (sourceCards.length === cardCount) break + } + + if (sourceCards.length === 0) return event + + return patchEvent(event, { + options: { + sourceCards, + combinationID: nextGroupID(room, FEN_CHAO_SPELL_ID, 'fenchao_discard_candidate') + } + }) +} diff --git a/src/ui/PeiXiuMapWindow.js b/src/ui/PeiXiuMapWindow.js index f936956..219f4c1 100644 --- a/src/ui/PeiXiuMapWindow.js +++ b/src/ui/PeiXiuMapWindow.js @@ -1,4 +1,5 @@ import { PEIXIU_DIRECTIONS } from '../utils/peixiuRouteFeature' +import { getLayoutViewportWidth } from './drag' const WINDOW_ID = 'peixiu-map-window' const STYLE_ID = 'peixiu-map-style' @@ -281,7 +282,7 @@ function ensureStyle() { document.head.appendChild(style) } -function bindDrag(element, handle) { +function bindPeiXiuMapWindowDrag(element, handle) { cleanupDrag?.() let pointerId = null let offsetX = 0 @@ -290,7 +291,7 @@ function bindDrag(element, handle) { const move = (event) => { if (event.pointerId !== pointerId) return const left = Math.min( - Math.max(0, window.innerWidth - element.offsetWidth), + Math.max(0, getLayoutViewportWidth() - element.offsetWidth), Math.max(0, event.clientX - offsetX) ) const top = Math.min( @@ -352,7 +353,7 @@ function createWindow() { document.body.appendChild(element) if (lastPosition) { const left = Math.min( - Math.max(0, window.innerWidth - element.offsetWidth), + Math.max(0, getLayoutViewportWidth() - element.offsetWidth), Math.max(0, lastPosition.left) ) const top = Math.min( @@ -364,7 +365,7 @@ function createWindow() { element.style.right = 'auto' lastPosition = { left, top } } - bindDrag(element, element.querySelector('.peixiu-header')) + bindPeiXiuMapWindowDrag(element, element.querySelector('.peixiu-header')) return element } diff --git a/src/ui/drag.js b/src/ui/drag.js index b77dc6c..f89f8bb 100644 --- a/src/ui/drag.js +++ b/src/ui/drag.js @@ -5,7 +5,7 @@ export function addDragHint() { document.body.appendChild(sidebarHint) } -function getLayoutViewportWidth() { +export function getLayoutViewportWidth() { return document.documentElement.clientWidth || window.innerWidth } diff --git a/tests/tracker/cardCounter.test.ts b/tests/tracker/cardCounter.test.ts index d3c1f83..89b6f05 100644 --- a/tests/tracker/cardCounter.test.ts +++ b/tests/tracker/cardCounter.test.ts @@ -1,14 +1,37 @@ -import { describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { CardConfig } from '@/config/CardConfig' import { Card } from '@/tracker/Card' import { CARD_INSTANCE_STATUS } from '@/tracker/CardCounter' import { collectTraversalStats } from '@/tracker/traversalStats' import { createTestRoom, getCard } from './helpers/room' +afterEach(() => { + vi.restoreAllMocks() +}) + function ids(set: Set): number[] { return Array.from(set).sort((a, b) => a - b) } describe('CardCounter 状态索引', () => { + it('红黑杀按卡牌技能 ID 分类,显示名不影响识别', () => { + const cardInfos = [ + { name: '更名后的红杀', color: 1, spellId: 1 }, + { name: '火杀', color: 2, spellId: 1 }, + { name: '雷杀', color: 3, spellId: 1 }, + { name: '冰杀', color: 4, spellId: 1 }, + { name: '杀', color: 1, spellId: 8 }, + { name: '雷杀', color: 3 } + ] + vi.spyOn(CardConfig.GetInstance(), 'getCard').mockImplementation((id) => cardInfos[id - 1]) + const { room } = createTestRoom({ cardIDs: [1, 2, 3, 4, 5, 6], seatIDs: [1] }) + + room.counter.query('colorIndex', 5) + expect(ids(room.counter.querySet)).toEqual([1, 2]) + room.counter.query('colorIndex', 6) + expect(ids(room.counter.querySet)).toEqual([3, 4]) + }) + it('按未知、出现、弃牌、移出游戏四类同步派生索引', () => { const { room } = createTestRoom({ cardIDs: [1, 2, 3, 4, 5], seatIDs: [1] }) diff --git a/tests/tracker/fenChao.test.ts b/tests/tracker/fenChao.test.ts new file mode 100644 index 0000000..3c57dd8 --- /dev/null +++ b/tests/tracker/fenChao.test.ts @@ -0,0 +1,216 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { CardConfig } from '@/config/CardConfig' +import { POSITION_RANDOM, POSITION_TOP } from '@/tracker/candidate/cardPositions' +import { normalizeMoveEvent } from '@/tracker/MoveEventNormalizer' +import type { RawMoveCardEvent } from '@/tracker/types' +import { getCard } from './helpers/room' +import { createTrackerControllerHarness, protocolMove } from './helpers/trackerController' + +const CARD_SPELL_IDS: Record = { + 杀: 1, + 火杀: 1, + 雷杀: 1, + 冰杀: 1, + 决斗: 8, + 南蛮: 9, + 南蛮入侵: 9, + 万箭: 10, + 万箭齐发: 10, + 火攻: 83 +} + +afterEach(() => { + vi.restoreAllMocks() +}) + +function createRoomWithDiscard( + cardNames: string[], + cardSpellIds = cardNames.map((name) => CARD_SPELL_IDS[name] ?? 0) +) { + const cardInfos = cardNames.map((name, index) => ({ + id: index + 1, + name, + spellId: cardSpellIds[index] + })) + vi.spyOn(CardConfig.GetInstance(), 'getCard').mockImplementation((id) => cardInfos[id - 1]) + + const { controller } = createTrackerControllerHarness() + controller.initTrackerRoom() + controller.registerTrackerPlayers( + [ + { SeatID: 1, ClientID: 100 }, + { SeatID: 7, ClientID: 700 } + ], + 100 + ) + + const cardIDs = cardNames.map((_, index) => index + 1) + controller.initTrackerDeck(cardIDs) + controller.syncTrackerMove(protocolMove({ CardIDs: cardIDs, ToZone: 2, ToID: 255, MoveType: 16 })) + + const room = controller.getTrackerRoom()! + + return { controller, room } +} + +function createFenChaoMove(overrides: RawMoveCardEvent = {}) { + return protocolMove({ + CardCount: 2, + CardIDs: [], + FromID: 255, + FromPosition: POSITION_RANDOM, + FromZone: 2, + FromZoneParam: 0, + MoveType: 18, + SpellID: 3752, + ToID: 7, + ToPosition: POSITION_TOP, + ToZone: 5, + ToZoneParam: 0, + ...overrides + }) +} + +describe('焚巢弃牌堆取牌', () => { + it.each([{ CardIDs: [] }, { CardIDs: [0, 0] }])( + '未公开牌 ID $CardIDs 时按入堆先后获得两张目标明牌', + ({ CardIDs }) => { + const { controller, room } = createRoomWithDiscard([ + '闪', + '决斗', + '桃', + '杀', + '南蛮', + '万箭', + '火攻' + ]) + const message = createFenChaoMove({ CardIDs }) + const originalCardIDs = [...CardIDs] + + controller.syncTrackerMove(message) + + const player = room.players.get(7)! + expect(player.knownHandCards.map((card) => card.id).sort((a, b) => a - b)).toEqual([2, 4]) + expect(player.observedHandCount).toBe(2) + expect(player.unknownCardCount).toBe(0) + expect(room.zones.get('discard')!.cards.map((card) => card.id)).toEqual([1, 3, 5, 6, 7]) + expect(room.zones.get('pile')!.cards).toEqual([]) + expect(room.cards).toHaveLength(7) + expect(message.CardIDs).toEqual(originalCardIDs) + } + ) + + it.each(['杀', '火杀', '雷杀', '冰杀', '决斗', '南蛮', '南蛮入侵', '万箭', '万箭齐发', '火攻'])( + '识别 %s 并优先获得同名牌中最早入堆的一张', + (name) => { + const { controller, room } = createRoomWithDiscard(['闪', name, '桃', name, '无懈']) + + controller.syncTrackerMove(createFenChaoMove({ CardCount: 1 })) + + expect(room.players.get(7)!.knownHandCards.map((card) => card.id)).toEqual([2]) + expect(room.zones.get('discard')!.cards.map((card) => card.id)).toEqual([1, 3, 4, 5]) + } + ) + + it.each([1, 8, 9, 10, 83])('按卡牌技能 ID %i 识别目标牌,不受显示名影响', (spellId) => { + const { controller, room } = createRoomWithDiscard( + ['杀', '更名后的卡牌', '决斗'], + [7, spellId, 0] + ) + + controller.syncTrackerMove(createFenChaoMove({ CardCount: 1 })) + + expect(room.players.get(7)!.knownHandCards.map((card) => card.id)).toEqual([2]) + expect(room.zones.get('discard')!.cards.map((card) => card.id)).toEqual([1, 3]) + expect(getCard(room, 2)!.spellId).toBe(spellId) + expect(getCard(room, 2)!.spellID).toBe(3752) + }) + + it('连续获得按当前弃牌顺序继续筛选,张数以协议为准', () => { + const { controller, room } = createRoomWithDiscard([ + '火攻', + '闪', + '南蛮', + '决斗', + '杀', + '万箭', + '桃' + ]) + + controller.syncTrackerMove(createFenChaoMove()) + controller.syncTrackerMove(createFenChaoMove({ CardCount: 3 })) + + const player = room.players.get(7)! + expect(player.knownHandCards.map((card) => card.id).sort((a, b) => a - b)).toEqual([ + 1, 3, 4, 5, 6 + ]) + expect(player.observedHandCount).toBe(5) + expect(player.unknownCardCount).toBe(0) + expect(room.zones.get('discard')!.cards.map((card) => card.id)).toEqual([2, 7]) + }) + + it('牌离开后重新弃入时,以本次入堆顺序为准', () => { + const { controller, room } = createRoomWithDiscard(['杀', '火攻', '决斗', '闪']) + controller.syncTrackerMove(createFenChaoMove({ CardCount: 1, CardIDs: [1], SpellID: 0 })) + controller.syncTrackerMove( + protocolMove({ CardIDs: [1], FromZone: 5, FromID: 7, ToZone: 2, ToID: 255, MoveType: 4 }) + ) + + controller.syncTrackerMove(createFenChaoMove()) + + expect( + room.players + .get(7)! + .knownHandCards.map((card) => card.id) + .sort((a, b) => a - b) + ).toEqual([2, 3]) + expect(room.zones.get('discard')!.cards.map((card) => card.id)).toEqual([4, 1]) + }) + + it('协议明确给出牌 ID 时沿用真实身份,不按最早入堆规则覆盖', () => { + const { controller, room } = createRoomWithDiscard(['杀', '闪', '决斗', '火攻']) + + controller.syncTrackerMove(createFenChaoMove({ CardCount: 1, CardIDs: [4] })) + + expect(room.players.get(7)!.knownHandCards.map((card) => card.id)).toEqual([4]) + expect(room.zones.get('discard')!.cards.map((card) => card.id)).toEqual([1, 2, 3]) + }) + + it('来源含匿名牌时复用不同实体补足数量,保留已识别明牌', () => { + const { controller, room } = createRoomWithDiscard(['火攻', '闪']) + controller.syncTrackerMove( + protocolMove({ CardCount: 1, CardIDs: [], FromZone: 0, ToZone: 2, ToID: 255, MoveType: 19 }) + ) + + controller.syncTrackerMove(createFenChaoMove()) + + const player = room.players.get(7)! + expect(player.knownHandCards.map((card) => card.id)).toEqual([1]) + expect(player.observedHandCount).toBe(2) + expect(player.unknownCardCount).toBe(1) + expect(room.zones.get('discard')!.cards.map((card) => card.id)).toEqual([2]) + expect(room.cards).toHaveLength(3) + }) + + it.each([ + { scenario: '其他技能', overrides: { SpellID: 9999 } }, + { scenario: '牌堆来源', overrides: { FromZone: 1 } }, + { scenario: '非手牌目标', overrides: { ToZone: 6 } }, + { scenario: '非获得移动', overrides: { MoveType: 4 } }, + { scenario: '明确端点取牌', overrides: { FromPosition: POSITION_TOP } }, + { scenario: '零张移动', overrides: { CardCount: 0 } } + ])('$scenario 不应用焚巢来源推断', ({ overrides }) => { + const { room } = createRoomWithDiscard(['杀', '决斗', '闪']) + const event = room.decorateMoveEvent(normalizeMoveEvent(createFenChaoMove(overrides))) + + expect(event.options.sourceCards).toBeUndefined() + expect(room.zones.get('discard')!.cards.map((card) => card.id)).toEqual([1, 2, 3]) + }) + + it('没有目标牌时保留默认移动,不将其他锦囊纳入焚巢筛选', () => { + const { room } = createRoomWithDiscard(['闪', '桃', '借刀', '水淹', '火烧']) + const event = room.decorateMoveEvent(normalizeMoveEvent(createFenChaoMove())) + + expect(event.options.sourceCards).toBeUndefined() + }) +})