feedback5 R19F: community report 「小拉和赫默有概率在备战区叠不上叙拉古和远见」 checked, not a bug — 拉普兰德 stacks 叙拉古 from the 整备区 whenever 叙拉古 is active (it counts deployed 叙拉古 only, her bench copy is not one of the three) at her first manual refresh of the round; 赫默's trait has no 「此干员在整备区时也有效」 and the official data marks it deployed-only (conditionkey character_target_inboard); the 临时整备区 is not the 整备区. No code change: pinned by test/match/feedback5-bench-traits.test.js (the official data rule and the report's cases through the match paths), PLAYING.md §6 says where a 特质 works, CHANGELOG 核实后不是问题, DESIGN §25.22.7; golden unchanged
This commit is contained in:
1 parent
0255afd12b
commit
522f6287bb
4 files changed
+247
No files matched your search
@@ -0,0 +1,211 @@
|
||||
// Community report relayed by the owner (2026-10-07): 「有的时候在备战区的干员不能100%叠层…小拉和赫默有概率在备战区叠不上叙拉古
|
||||
// 和远见」 — 拉普兰德 (小拉, Ⅱ) in the 整备区 sometimes adds no 【叙拉古】 layers, 赫默 (Ⅱ) no 【远见】 layers. Reproduced over
|
||||
// whole matches (scratchpad notes, R19F): every non-firing case is a rule of the official data, nothing random —
|
||||
// * 拉普兰德 garrison_123 「<刷新时>若为本回合首次主动刷新,使已激活的【叙拉古】层数+4,此干员在整备区时也有效」: she works in
|
||||
// the 整备区, but 【叙拉古】 must be active (activeCondition BOARD: 3 different 叙拉古 deployed — her bench copy is not one
|
||||
// of them), and her trait fires on the first manual refresh she sees this round: a refresh made while 叙拉古 is
|
||||
// inactive spends it. The 临时整备区 is not the 整备区 (as for every 整备区 rule): she fires once she is in it.
|
||||
// * 赫默 garrison_80 「<进入休整期时>使自身已激活的盟约层数+2」 has no 「此干员在整备区时也有效」 and carries the official
|
||||
// blackboard conditionkey character_target_inboard: deployed only. 【远见】's 「整备区的【远见】干员也可用于激活盟约」 counts
|
||||
// bench members for the bond, it does not move traits to the bench.
|
||||
// Through the real match paths (g.refresh / g.buy / g.move, the round loop's ROUND_START dispatch). Pins; no change.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeMatch, give, legalTileFor, checkInvariants, chessOfTier, DATA } from './harness.js';
|
||||
import { createRegistry } from '../../server/match/effectsMeta.js';
|
||||
|
||||
const QUIET = { warn() {}, error() {}, info() {} };
|
||||
const REG = createRegistry({ log: QUIET });
|
||||
const OK = { ok: true };
|
||||
const LAP = 'chess_char_2_16_a'; // 拉普兰德 (Ⅱ, 叙拉古) — garrison_123_a
|
||||
const LAP_B = 'chess_char_2_16_b';
|
||||
const PROVENCE = 'chess_char_1_07_a'; // 普罗旺斯 (Ⅰ, 叙拉古; <获得时>获得1次免费刷新)
|
||||
const TEXAS = 'chess_char_1_08_a'; // 德克萨斯 (Ⅰ, 叙拉古)
|
||||
const SIYE = 'chess_char_3_18_a'; // 伺夜 (Ⅲ, 叙拉古)
|
||||
const SILENCE = 'chess_char_2_02_a'; // 赫默 (Ⅱ, 远见) — garrison_80_a
|
||||
const SILENCE_B = 'chess_char_2_02_b';
|
||||
const SWIRE = 'chess_char_3_03_a'; // 诗怀雅 (Ⅲ, 远见)
|
||||
const FILLER = 'chess_char_1_01_a';
|
||||
/** distinct fillers for a full 整备区: no 叙拉古 / 远见, no prep-side 特质 (one copy each, so nothing merges) */
|
||||
const FILLERS = [1, 2].flatMap((t) => chessOfTier(t, (c) => !c.bonds.includes('siracusaShip') && !c.bonds.includes('visiShip')
|
||||
&& (c.garrisonIds || []).every((g) => DATA.garrisons[g].eventType === 'IN_BATTLE')));
|
||||
const LAP_KEY = 'garrison:SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT';
|
||||
const SIL_KEY = 'garrison:SERVER_ADD_BOND_CHESS_ALL';
|
||||
|
||||
function setup({ mode = 'solo', bots = 0, round = 1, pid = 'p_0', seed = 70 } = {}) {
|
||||
const h = makeMatch({ mode, bots, seed, registry: REG, fake: true }).start();
|
||||
h.toPrep(round);
|
||||
const m = h.m;
|
||||
const ps = h.ps(pid);
|
||||
for (const p of [...ps.board.values(), ...ps.hand.filter(Boolean), ...ps.temp.filter(Boolean)]) if (p.kind === 'chess') ps.returnCopies(p);
|
||||
ps.board.clear();
|
||||
ps.hand.fill(null);
|
||||
ps.temp.fill(null);
|
||||
ps.offers.length = 0;
|
||||
ps.funds = 50;
|
||||
ps.layers = {};
|
||||
ps.pendingFunds = 0;
|
||||
ps.shop.freeRefreshes = 0;
|
||||
ps.bandId = null; // isolate from the band's own prep effects
|
||||
ps.ready = false;
|
||||
ps.recompute();
|
||||
/** layers each trait added (the dispatcher's reason = the registry key) */
|
||||
const got = { lap: 0, sil: 0 };
|
||||
const addLayers = ps.addLayers.bind(ps);
|
||||
ps.addLayers = (b, n, o = {}) => {
|
||||
const a = addLayers(b, n, o);
|
||||
if (o.reason === LAP_KEY && b === 'siracusaShip') got.lap += a;
|
||||
if (o.reason === SIL_KEY && b === 'visiShip') got.sil += a;
|
||||
return a;
|
||||
};
|
||||
const refresh = () => assert.deepEqual(m.handle(pid, { t: 'g.refresh' }), OK);
|
||||
const deploy = (id) => give(m, ps, id, 'board', legalTileFor(m, ps, id));
|
||||
const place = (piece) => {
|
||||
const [row, col] = legalTileFor(m, ps, piece.id);
|
||||
assert.deepEqual(m.handle(pid, { t: 'g.move', uid: piece.uid, to: { area: 'board', row, col }, dir: 'RIGHT' }), OK);
|
||||
};
|
||||
const buy = (id) => {
|
||||
ps.shop.slots[0] = { kind: 'chess', id, basePrice: m.gd.chessPrice(id), frozen: false, sold: false };
|
||||
const before = new Set(ps.allChess().map((p) => p.uid));
|
||||
assert.deepEqual(m.handle(pid, { t: 'g.buy', slot: 0 }), OK, `buy ${id}`);
|
||||
return ps.allChess().find((p) => !before.has(p.uid));
|
||||
};
|
||||
const fillHand = () => { let i = 0; while (ps.hand.some((x) => x == null)) give(m, ps, FILLERS[i++], 'hand'); };
|
||||
const nextRound = () => { ps.ready = false; h.toPrep(m.round + 1); ps.funds = 50; };
|
||||
const siracusa = () => ({ active: !!ps.bonds.siracusaShip?.active, count: ps.bonds.siracusaShip?.count ?? 0 });
|
||||
return { h, m, ps, got, refresh, deploy, place, buy, fillHand, nextRound, siracusa };
|
||||
}
|
||||
|
||||
test('official data: a 进入休整期时 / 休整期结束时 / 刷新时 特质 works in the 整备区 exactly when its text says so', () => {
|
||||
const PREP = new Set(['SERVER_PREP_START', 'SERVER_PREP_FIN', 'SERVER_REFRESH_SHOP']);
|
||||
const bench = [];
|
||||
for (const g of Object.values(DATA.garrisons)) {
|
||||
if (!PREP.has(g.eventType)) continue;
|
||||
const key = g.bbStr && g.bbStr.conditionkey;
|
||||
if (/整备区时也有效|整备区也有效/.test(g.desc)) {
|
||||
assert.equal(key, undefined, `${g.garrisonId}: a bench trait carries no condition key`);
|
||||
bench.push(g.garrisonId);
|
||||
} else if (!/^身前一格干员若为/.test(g.desc)) {
|
||||
// the rest: deployed only (character_target_inboard), or a row condition that only a deployed operator can meet;
|
||||
// the two copy traits (白面鸮 / 塞雷娅 "身前一格…") read the tile in front and are positional by nature
|
||||
assert.ok(key === 'character_target_inboard' || key === 'character_same_row', `${g.garrisonId}: ${key} — ${g.desc}`);
|
||||
}
|
||||
}
|
||||
// 拉普兰德 123, 调香师 81, 波登可 102, 琳琅诗怀雅 98 (若已激活【炎】/【投资人】) — normal and elite records
|
||||
assert.deepEqual(bench.sort(), ['garrison_102_a', 'garrison_102_b', 'garrison_123_a', 'garrison_123_b', 'garrison_81_a', 'garrison_81_b', 'garrison_98_a', 'garrison_98_b']);
|
||||
for (const id of ['garrison_80_a', 'garrison_80_b']) {
|
||||
const g = DATA.garrisons[id];
|
||||
assert.equal(g.bbStr.conditionkey, 'character_target_inboard', `${id} (赫默): deployed only`);
|
||||
assert.doesNotMatch(g.desc, /整备区/);
|
||||
}
|
||||
assert.equal(DATA.bonds.siracusaShip.countMode, 'BOARD', '叙拉古 counts deployed operators only');
|
||||
assert.equal(DATA.bonds.visiShip.countMode, 'BOARD_AND_DECK', '远见 also counts the 整备区');
|
||||
});
|
||||
|
||||
test('拉普兰德 in the 整备区 with 3 different 叙拉古 deployed: +4 on her first manual refresh (+8 elite), whatever else holds', () => {
|
||||
const cases = {
|
||||
plain: () => {},
|
||||
'hand full (10/10)': (s) => s.fillHand(),
|
||||
'shop frozen': (s) => assert.deepEqual(s.m.handle('p_0', { t: 'g.freeze' }), OK),
|
||||
'a free refresh': (s) => { s.ps.shop.freeRefreshes = 1; },
|
||||
'a reward offer pending': (s) => { s.ps.offers.push({ source: 'merge', kind: 'chess', ids: [FILLER], tier: 2 }); },
|
||||
'a reconnect': (s) => { s.m.onDisconnect('p_0'); s.m.onReconnect('p_0'); },
|
||||
};
|
||||
for (const [name, before] of Object.entries(cases)) {
|
||||
for (const [id, n] of [[LAP, 4], [LAP_B, 8]]) {
|
||||
const s = setup();
|
||||
for (const x of [PROVENCE, TEXAS, SIYE]) s.deploy(x);
|
||||
give(s.m, s.ps, id, 'hand');
|
||||
before(s);
|
||||
assert.deepEqual(s.siracusa(), { active: true, count: 3 }, name);
|
||||
s.refresh();
|
||||
assert.equal(s.got.lap, n, `${name} ${id}`);
|
||||
s.refresh();
|
||||
assert.equal(s.got.lap, n, `${name}: once per round`);
|
||||
s.m.dispose();
|
||||
}
|
||||
}
|
||||
// a 机变 round (solo R6) and a bot seat (its own ps.refresh, bot.js) work the same
|
||||
const r6 = setup({ round: 6 });
|
||||
for (const x of [PROVENCE, TEXAS, SIYE]) r6.deploy(x);
|
||||
give(r6.m, r6.ps, LAP, 'hand');
|
||||
r6.refresh();
|
||||
assert.equal(r6.got.lap, 4, 'a 机变 round');
|
||||
r6.m.dispose();
|
||||
const bot = setup({ mode: 'coop', bots: 1, pid: 'ai_0' });
|
||||
for (const x of [PROVENCE, TEXAS, SIYE]) bot.deploy(x);
|
||||
give(bot.m, bot.ps, LAP, 'hand');
|
||||
assert.deepEqual(bot.ps.refresh(), OK);
|
||||
assert.equal(bot.got.lap, 4, 'a bot');
|
||||
bot.m.dispose();
|
||||
});
|
||||
|
||||
test('拉普兰德 in the 整备区 is not one of the 3 叙拉古: inactive bond, nothing — and that refresh was her first of the round', () => {
|
||||
const s = setup();
|
||||
s.deploy(PROVENCE);
|
||||
s.deploy(TEXAS);
|
||||
const lap = give(s.m, s.ps, LAP, 'hand');
|
||||
assert.deepEqual(s.siracusa(), { active: false, count: 2 }, 'two 叙拉古 deployed: 2/3 (she sits in the 整备区)');
|
||||
s.refresh();
|
||||
assert.equal(s.got.lap, 0, '已激活的【叙拉古】: not active, no layers');
|
||||
s.place(lap);
|
||||
assert.deepEqual(s.siracusa(), { active: true, count: 3 }, 'deployed she is the third');
|
||||
s.refresh();
|
||||
assert.equal(s.got.lap, 0, '若为本回合首次主动刷新: her first refresh of the round was the inactive one');
|
||||
s.nextRound();
|
||||
s.refresh();
|
||||
assert.equal(s.got.lap, 4, 'the next round: her first refresh with 叙拉古 active');
|
||||
checkInvariants(s.m);
|
||||
s.m.dispose();
|
||||
});
|
||||
|
||||
test('拉普兰德 in the 临时整备区: no layers there; once she is in the 整备区 her next refresh adds +4', () => {
|
||||
const s = setup();
|
||||
for (const x of [PROVENCE, TEXAS, SIYE]) s.deploy(x);
|
||||
s.fillHand();
|
||||
const lap = give(s.m, s.ps, LAP, 'temp');
|
||||
assert.equal(s.ps.find(lap.uid).area, 'temp');
|
||||
s.refresh();
|
||||
assert.equal(s.got.lap, 0, 'the 临时整备区 is not the 整备区');
|
||||
assert.deepEqual(s.m.handle('p_0', { t: 'g.sell', uid: s.ps.hand[0].uid }), OK); // a free slot pulls her in
|
||||
assert.equal(s.ps.find(lap.uid).area, 'hand');
|
||||
s.refresh();
|
||||
assert.equal(s.got.lap, 4, 'her first refresh in the 整备区');
|
||||
s.refresh();
|
||||
assert.equal(s.got.lap, 4, 'once');
|
||||
checkInvariants(s.m);
|
||||
s.m.dispose();
|
||||
});
|
||||
|
||||
test('拉普兰德 bought after the round\'s first refresh, and a free refresh from 普罗旺斯: both stack (each copy counts its own refreshes)', () => {
|
||||
const s = setup();
|
||||
s.deploy(TEXAS);
|
||||
s.deploy(SIYE);
|
||||
give(s.m, s.ps, LAP, 'hand');
|
||||
const prov = s.buy(PROVENCE); // <获得时>获得1次免费刷新
|
||||
assert.equal(s.ps.shop.freeRefreshes, 1);
|
||||
s.place(prov);
|
||||
s.refresh(); // the free one is a manual refresh too
|
||||
assert.equal(s.ps.shop.freeRefreshes, 0);
|
||||
assert.equal(s.got.lap, 4);
|
||||
s.buy(LAP); // a second copy, bought after the round's first refresh, stays in the 整备区
|
||||
s.refresh();
|
||||
assert.equal(s.got.lap, 8, 'her own first refresh');
|
||||
s.m.dispose();
|
||||
});
|
||||
|
||||
test('赫默: +2 / +4 at the round start only when deployed; in the 整备区 nothing even with 远见 active (character_target_inboard)', () => {
|
||||
for (const [id, where, partner, n] of [
|
||||
[SILENCE, 'board', 'hand', 2], [SILENCE, 'board', 'board', 2], [SILENCE_B, 'board', 'hand', 4],
|
||||
[SILENCE, 'hand', 'board', 0], [SILENCE, 'hand', 'hand', 0], [SILENCE_B, 'hand', 'board', 0],
|
||||
]) {
|
||||
const s = setup();
|
||||
if (where === 'board') s.deploy(id); else give(s.m, s.ps, id, 'hand');
|
||||
if (partner === 'board') s.deploy(SWIRE); else give(s.m, s.ps, SWIRE, 'hand');
|
||||
assert.ok(s.ps.bonds.visiShip.active, '远见 counts the 整备区: active');
|
||||
s.nextRound();
|
||||
assert.equal(s.got.sil, n, `${id} ${where}, partner ${partner}`);
|
||||
assert.equal(s.ps.layers.visiShip || 0, n);
|
||||
s.m.dispose();
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user