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

+44 -18
View File
@@ -33,11 +33,18 @@
// each card on its own (duplicates allowed) [ASSUMED]. FREE — the official card text is "无需消耗资金,获得装备
// 补给" (research 01 A4/04 addendum), so the price is 0. Two identical cards are two cards: picks,
// `taken` and the client go by the card's `idx`
// tactic 战术决策 distinct cards.tactic entries; terrain cards only for the match stage; a 驰援 card
// (single_special_choice_gain_bond_chess) or a 盟誓 card (global_special_choice_bond_addlayer)
// only while at least one of its bonds is live in this match (opts.bondAvailable = Match.bondLive:
// not in the mode's static inactive list — 标准 turns off 拉特兰 / 阿戈尔 / 卡西米尔 / 奥术 … —
// and still with chess in the pool); a card acting only on dead bonds would do nothing
// tactic 战术决策 cards.tactic entries, each card drawn on its own — with replacement, so the same card can be
// offered twice (the 4 official 战术决策, R11 of matches 7 / 9 / 18 / 20: 补给 ×2 in match 7; the
// user: "战术决策也按官方改成可以重复吧"; `tacticDraftCards`): at the rounds of choices.json
// `tacticDraft` (R11) only its `kinds` (the ally cards — the official 24 cards hold no debuff and
// no terrain card), a card by `weights` (1 + the official cards it showed on) [ASSUMED: the
// weights]; other rounds (标准 / 险境, no screenshot) every card uniform, terrain cards only for
// the match stage [ASSUMED]; a 驰援 card (single_special_choice_gain_bond_chess) or a 盟誓 card
// (global_special_choice_bond_addlayer) only while at least one of its bonds is live in this match
// (opts.bondAvailable = Match.bondLive: not in the mode's static inactive list — 标准 turns off
// 拉特兰 / 阿戈尔 / 卡西米尔 / 奥术 … — and still with chess in the pool); a card acting only on dead
// bonds would do nothing. Two identical cards are two cards: picks, `taken` and the client go by
// the card's `idx`, and each pick applies its own card once (two picks of one team card stack)
// Application (applyCard): a registered `choice:<effectId>` handler (content) wins; otherwise the family default:
// bounty → the picker's bounty list (waves.js adds the enemies to the next `rounds` battles; kill payout to the
// killer via the Battle, perfect payout at settlement when the picker's own battle was perfect)
@@ -344,8 +351,38 @@ export function shopDraftCards(gd, rng, n, round = null) {
return rng.shuffle(out).slice(0, Math.max(0, n));
}
/** The draft card of a cards.tactic entry. */
export function tacticCard(c) {
return { kind: 'tactic', id: c.effectId, name: c.name, desc: c.desc || '', tier: null, team: !!c.team, tacticKind: c.kind };
}
/**
* The 战术决策 cards (module header): `n` cards, each drawn on its own — with replacement, so one card can fill two places
* (official R11 of match 7: 补给 ×2). At the rounds of choices.json `tacticDraft` (`rounds`; R11) only the cards of its
* `kinds` (ally), each by `weights` (1 when unlisted); elsewhere — or when those kinds leave nothing — every card, uniform.
* Terrain cards only for the match stage; a card whose every target bond is dead in this match (mode-inactive, or no
* chess left in the pool: `bondAvailable`) is never offered.
*/
export function tacticDraftCards(gd, rng, n, { stageId = null, bondAvailable = null, round = null } = {}) {
const bondOk = (c) => {
if (typeof bondAvailable !== 'function') return true;
const bonds = cardTargetBonds(gd, c.effectId);
return !bonds || bonds.some((b) => !!bondAvailable(b));
};
const list = Array.isArray(gd.choices.cards && gd.choices.cards.tactic) ? gd.choices.cards.tactic : [];
const pool = list.filter((c) => c && (c.kind !== 'terrain' || (stageId && c.stageId === stageId)) && gd.effect(c.effectId) && bondOk(c));
const spec = gd.choices.tacticDraft && typeof gd.choices.tacticDraft === 'object' ? gd.choices.tacticDraft : null;
const official = !!spec && (!Array.isArray(spec.rounds) || spec.rounds.includes(round));
const kinds = official && Array.isArray(spec.kinds) && spec.kinds.length ? spec.kinds : null;
const w = official && spec.weights && typeof spec.weights === 'object' ? spec.weights : {};
const ofKinds = kinds ? pool.filter((c) => kinds.includes(c.kind)) : pool;
const pairs = (ofKinds.length ? ofKinds : pool).map((c) => [c, Object.hasOwn(w, c.effectId) ? w[c.effectId] : 1]);
const out = [];
for (let i = 0; i < n && pairs.length; i++) out.push(tacticCard(weightedPick(rng, pairs)));
return out;
}
function buildCards(gd, rng, family, n, sch, { stageId = null, bondAvailable = null, round = 1 } = {}) {
const cardsData = gd.choices.cards || {};
if (family === 'bounty') return bountyDraftCards(gd, rng, n, sch, round);
if (family === 'shop') {
const cards = shopDraftCards(gd, rng, n, round);
@@ -361,18 +398,7 @@ function buildCards(gd, rng, family, n, sch, { stageId = null, bondAvailable = n
for (let i = 0; i < n && list.length; i++) out.push(itemCard(gd, list[Math.floor(rng() * list.length)]));
return out;
}
if (family === 'tactic') {
// a card whose every target bond is dead in this match (mode-inactive, or no chess left in the pool) does nothing
const bondOk = (c) => {
if (typeof bondAvailable !== 'function') return true;
const bonds = cardTargetBonds(gd, c.effectId);
return !bonds || bonds.some((b) => !!bondAvailable(b));
};
const pool = (Array.isArray(cardsData.tactic) ? cardsData.tactic : []).filter((c) => c && (c.kind !== 'terrain' || (stageId && c.stageId === stageId)) && gd.effect(c.effectId) && bondOk(c));
const pick = pool.slice();
rng.shuffle(pick);
return pick.slice(0, n).map((c) => ({ kind: 'tactic', id: c.effectId, name: c.name, desc: c.desc || '', tier: null, team: !!c.team, tacticKind: c.kind }));
}
if (family === 'tactic') return tacticDraftCards(gd, rng, n, { stageId, bondAvailable, round });
return [];
}