feedback1 WB: 战术决策 drawn with replacement like the 4 official drafts; R11 ally cards weighted by the screenshots

The user: "战术决策也按官方改成可以重复吧". The 4 official 战术决策 (R11 of
matches 7, 9, 18 and 20) show six cards each; match 7 offers 补给 twice.

- Every 战术决策 card is drawn on its own (with replacement), so the same
  card can come twice. choices.js tacticDraftCards / tacticCard.
- R11 (绝境 / 终极, the round of the screenshots; choices.json tacticDraft,
  build-data TACTIC_DRAFT): only the ally cards, weighted 1 + the official
  cards each showed on. All 24 official cards are ally cards (no 排斥 /
  责罚 / 裁决, no terrain card: about 0.04 % for the previous draw) and
  列装 / 财富 / 补给 / 整备 / 升华 made 12 of them (uniform: about 4.6).
  Other rounds (标准 / 险境, no screenshot) keep every card, uniform, with
  replacement too. The weights and the R11 terrain exclusion are assumed.
- The header is the official "进行协同调整,做好迎战准备。".
- Identical cards are separate cards: own index, own object; a pick
  applies its card once and leaves the twin pickable; two picks of a team
  card stack. Match / bot / overlay already go by index.
- Tests: official drafts vs data, generated R11 / solo / earlier rounds,
  a seeded draft with 补给 twice, co-op and solo E2E picks of twins, the
  bot, the overlay, and a headless-Chrome render (game-mock variant=tactic
  is now the official match 7 draft). Tests that drive given tactic cards
  lay them out explicitly. The 奥术 Final Assault test takes the first of
  a few seeds whose bots pair two 奥术 players (the draws moved seed 12).
This commit is contained in:
sganggs committed 2026-10-03 09:10:39 +08:00
1 parent bb6078efc2
commit 67e862d2b3
9 files changed
+550 -28

No files matched your search

+80 -2
View File
@@ -8,7 +8,7 @@ import { makeMatch, give, giveItem, chessOfTier, legalTileFor, checkInvariants a
import { FakeBattle } from '../match/fakeBattle.js';
import { makeBattle, chessRec, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
import { createRegistry } from '../../server/match/effectsMeta.js';
import { applyCard, generateDraft } from '../../server/match/choices.js';
import { applyCard, generateDraft, tacticCard } from '../../server/match/choices.js';
import { withBounties } from '../../server/match/waves.js';
import { Battle } from '../../server/sim/Battle.js';
import { bountyOf, battlePlanOf, choiceCardIds, gateOf, hasBattlePart, mapAliases, BUILTIN_REFS } from '../../server/sim/content/choices.js';
@@ -41,6 +41,17 @@ function toDraft(h, r) {
return m.sp;
}
/**
* Lay out the open 战术决策 as exactly the tactic cards `ids` (each its own index). The draft draws every card on its own,
* with replacement (the official 补给 ×2), so a small test pool can repeat one card and miss another: the tests that
* drive a card's effect through g.choice offer their cards this way, after checking what the generator drew.
*/
function offerTactic(m, ids) {
m.sp.cards = ids.map((id, idx) => ({ ...tacticCard(DATA.choices.cards.tactic.find((c) => c.effectId === id)), idx, family: 'tactic' }));
m.markPublic();
return m.sp;
}
/** Pick cards in the draft's (random) order: plan = { playerId: cardId }; bots pick by themselves. */
function draftPicks(h, plan) {
const m = h.m;
@@ -428,6 +439,66 @@ test('机密商店 E2E (co-op): two identical cards are two cards — both can b
m.dispose();
});
test('战术决策 E2E (co-op): identical cards are separate cards — one pick applies its card exactly once, the twin stays pickable, picks stack', () => {
// a pool of one card: every place draws 补给 (with replacement; the official match 7 R11 offered 补给 twice)
const one = 'allybuff_select_4';
const data = dataWith('mode_multi_normal', 3, 'tactic', { tactic: [one] });
const h = makeMatch({ mode: 'coop', difficulty: 'NORMAL', humans: 2, bots: 2, seed: 44, data, registry: REG, fake: true });
const m = h.m;
h.start();
const sp = toDraft(h, 3);
assert.equal(sp.family, 'tactic');
const pub = m.publicView().sp;
assert.deepEqual(pub.cards.map((c) => c.id), Array(6).fill(one), 'six 补给');
assert.deepEqual(pub.cards.map((c) => c.idx), [0, 1, 2, 3, 4, 5], 'each its own index');
assert.equal(new Set(sp.cards).size, 6, 'six card objects, not one shared');
const all = [...m.players.values()];
const free0 = new Map(all.map((ps) => [ps.playerId, ps.shop.freeRefreshes]));
const freeNow = () => all.map((ps) => ps.shop.freeRefreshes - free0.get(ps.playerId));
const picked = {};
for (let guard = 0; guard < 1000 && m.phase === 'SP_DRAFT'; guard++) {
const pid = m.spTurn();
if (pid !== 'p_0' && pid !== 'p_1') { h.sched.runNext(); continue; }
const taken = Object.keys(m.sp.taken).map(Number);
if (taken.length) assert.deepEqual(m.handle(pid, { t: 'g.choice', idx: taken[0] }), { error: 'SOLD_OUT' }, 'a taken index stays taken while a twin is free');
const before = freeNow();
const idx = m.sp.cards.find((c) => m.sp.taken[c.idx] == null).idx;
assert.deepEqual(m.handle(pid, { t: 'g.choice', idx }), { ok: true });
picked[pid] = idx;
assert.deepEqual(m.sp.taken[idx], pid, 'that index alone is taken');
assert.equal(Object.values(m.sp.taken).filter((x) => x === pid).length, 1, 'one card per pick');
// 补给 is a team card: +2 free refreshes for every alive player, once — not once per identical card
assert.deepEqual(freeNow().map((x, i) => x - before[i]), all.map(() => 2), `${pid}: one 补给 applied once`);
}
assert.equal(m.phase, 'PREP');
assert.notEqual(picked.p_0, picked.p_1, 'two different cards of the same tactic');
assert.equal(Object.keys(sp.taken).length, 4, 'every player took one of the identical cards');
assert.deepEqual(freeNow(), all.map(() => 8), 'four picks of 补给 stack (+2 each)');
matchInvariants(m);
m.dispose();
});
test('战术决策 E2E (solo): of three identical personal cards the one picked applies once; the other two stay untaken', () => {
const one = 'allybuff_select_6'; // 升华: the next purchased operator becomes elite
const data = dataWith('mode_single_normal', 9, 'tactic', { tactic: [one] });
const h = makeMatch({ mode: 'solo', difficulty: 'NORMAL', humans: 1, seed: 45, data, registry: REG, fake: true });
const m = h.m;
h.start();
const sp = toDraft(h, 9);
assert.deepEqual(sp.cards.map((c) => [c.idx, c.id]), [[0, one], [1, one], [2, one]]);
assert.deepEqual(m.handle('p_0', { t: 'g.choice', idx: 1 }), { ok: true });
assert.deepEqual(sp.taken, { 1: 'p_0' });
assert.deepEqual(sp.picks, { p_0: 1 });
const refs = h.ps('p_0').effects.filter((e) => e.key === BUILTIN_REFS.eliteChess);
assert.equal(refs.length, 1, 'one 升华');
assert.equal(refs[0].counter, 1);
for (let guard = 0; guard < 50 && m.phase === 'SP_DRAFT'; guard++) h.sched.runNext();
assert.equal(m.phase, 'PREP');
assert.equal(h.ps('p_0').effects.filter((e) => e.key === BUILTIN_REFS.eliteChess).length, 1, 'still one after the draft closed');
matchInvariants(m);
m.dispose();
});
// =====================================================================================================================
// 战术决策 — prep-side cards
@@ -441,7 +512,10 @@ test('战术决策 E2E (co-op 3): team cards reach every teammate (AI included),
const sp = toDraft(h, 3);
assert.equal(sp.family, 'tactic');
assert.equal(m.publicView().sp.name, '战术决策');
assert.deepEqual(sp.cards.map((c) => c.id).sort(), [...TEAM_PREP].sort());
assert.equal(m.publicView().sp.desc, '进行协同调整,做好迎战准备。', 'the official header');
assert.equal(sp.cards.length, 6);
assert.ok(sp.cards.every((c) => TEAM_PREP.includes(c.id)), 'drawn from the pool (with replacement)');
offerTactic(m, TEAM_PREP);
const view = m.publicView().sp.cards;
for (const c of view) assert.equal(c.team, DATA.choices.cards.tactic.find((x) => x.effectId === c.id).team, `${c.id} team flag`);
const all = [...m.players.values()];
@@ -532,6 +606,7 @@ test('战术决策 升华 / 整备: the next purchased operator becomes elite, t
const m = h.m;
h.start();
toDraft(h, 9);
offerTactic(m, ['allybuff_select_5', 'allybuff_select_6', 'allybuff_select_3']);
draftPicks(h, { p_0: 'allybuff_select_6' });
const ps = h.ps('p_0');
const ref = ps.effects.find((e) => e.key === BUILTIN_REFS.eliteChess);
@@ -575,6 +650,7 @@ test('战术决策 E2E: 火力 / 锐利 evaluate the 手牌区 at every prep end
const m = h.m;
h.start();
const sp = toDraft(h, 3);
offerTactic(m, ['allybuff_select_13', 'allybuff_select_19']);
const [a, b] = sp.order;
draftPicks(h, { [a]: 'allybuff_select_13', [b]: 'allybuff_select_19' });
const A = h.ps(a);
@@ -637,6 +713,7 @@ test('战术决策 E2E (real battles): 排斥 debuffs every enemy of the picker\
};
h.start();
const sp = toDraft(h, 3);
offerTactic(m, [id, 'allybuff_select_3']);
const [a, b] = sp.order;
draftPicks(h, { [a]: id, [b]: 'allybuff_select_3' });
assert.ok(h.ps(a).effects.some((e) => e.key === `choice:${id}` && e.iconId === 'icon_enemy_debuff'));
@@ -787,6 +864,7 @@ test('模拟战场演变 E2E (act1 m01): the card clears the picker\'s crates fo
h.setStage('act1autochess_m01');
const sp = toDraft(h, 3);
assert.ok(!sp.cards.some((c) => c.id === 'map_m02_1'), 'terrain cards only for the match stage');
offerTactic(m, ['map_m01_1', 'allybuff_select_3']);
const [a, b] = sp.order;
draftPicks(h, { [a]: 'map_m01_1', [b]: 'allybuff_select_3' });
const A = h.ps(a);