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:
1 parent
bb6078efc2
commit
67e862d2b3
9 files changed
+550
-28
No files matched your search
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user