feedback1 WB: bounty drafts follow the official R3 / R9 / R11 drafts (33 screenshots of 11 official matches)

R3 offers one of the official sets of six "接下来两场作战" cards (3 x I, 2 x II, 1 x III):
the 7 sets seen, the 3 unseen slots built by their rule. R9 offers a seen group of
named bosses filled to 6 with the cheap boss bounties and 源石虫·特训. R11 of 绝境 / 终极
is 悬赏决策, 机密商店 or 战术决策 (never 道具补给); its bounty is one 特异III giant and 5
"下场战斗" cards, one per faction series. Cards no official draft showed (the
multi-round cards, enemyeffect_3_*, five boss bounties) are no longer drafted.
The screenshot readings are a test fixture; tests check every reading against the
data and every generated draft against the official structure.
This commit is contained in:
sganggs committed 2026-10-03 04:43:44 +08:00
1 parent 6e0c26af9e
commit 8ac1aaadbb
9 files changed
+542 -241

No files matched your search

+43 -50
View File
@@ -7,7 +7,7 @@ import { PHASE } from '../../shared/constants.js';
import { DATA, makeMatch, give, chessOfTier, checkInvariants } from './harness.js';
import { FakeBattle } from './fakeBattle.js';
import { GameData } from '../../server/match/gamedata.js';
import { generateDraft, cardView, MULTI_ROUND_BOUNTY_BATTLES } from '../../server/match/choices.js';
import { generateDraft, cardView, bountyCard, MULTI_ROUND_BOUNTY_BATTLES } from '../../server/match/choices.js';
import { createRng } from '../../server/sim/rng.js';
import { Battle } from '../../server/sim/Battle.js';
import { uniteLeft, battleProgress } from '../../server/sim/spec.js';
@@ -106,12 +106,12 @@ test('#19 a tier short of different operators: the offer tops up from the tier b
// =====================================================================================================================
// #4 — "官方版悬赏决策的敌人我没记错的话正常只会在选择之后出现两回合,现在是一直出"
// The draft pool follows the PRTS 敌人轮选 table (卫戍协议:盟约 下半/PRTS盟约记录 §机变阶段), which lists the multi-round
// "之后 / 后续的每场作战" cards too: v2.4.1 also drafted 战术特训 (only 法术教鞭 makes those) and the hidden 鸭爵 set, put
// one or more multi-round cards in about half of the co-op R3 drafts and showed every card as plain text, so a "每场"
// card looked like a "下场" one. Now: no 战术特训 / 鸭爵 set (战术特训 is the 教鞭 Art's, test/content/items.test.js), the
// multi-round cards stay in as PRTS lists them, and each card names its battles in the official colours (blue "下场作战" /
// "两场作战", red "每场"); a 1–2-battle bounty stops after its battles.
// v2.4.1 drafted 战术特训 (only 法术教鞭 makes those) and the hidden 鸭爵 set, put one or more multi-round "之后 / 后续的每场
// 作战" cards in about half of the co-op R3 drafts and showed every card as plain text, so a "每场" card looked like a
// "下场" one. Now: no 战术特训 / 鸭爵 set (战术特训 is the 教鞭 Art's, test/content/items.test.js); since player feedback
// #2 the drafts follow the 33 official screenshots (test/match/feedback1-bounty.test.js), where no multi-round card
// appears; each card names its battles in the official colours (blue "下场作战" / "两场作战", red "每场"); a 1–2-battle
// bounty stops after its battles; a multi-round card (教鞭's 法术大师A2·多轮战术特训) lasts two battles.
const BOUNTY_MODES = ['mode_multi_normal', 'mode_multi_hard', 'mode_multi_abyss', 'mode_single_hard', 'mode_single_abyss'];
/** 鸭爵 / 高普尼克 / 流泪小子 / 圆仔·悬赏: commented out of the PRTS 敌人轮选 table */
@@ -121,10 +121,9 @@ const DURATION_RE = { 1: /<@ba\.vup>下场(作战|战斗)<\/>/, 2: /<@ba\.vup>
/** The official multi-round cards ("之后 / 后续的每场作战"): two battles since the user's playtest #6 answer. */
const MULTI = new Set(DATA.choices.cards.bounty.filter((c) => c.multiRound).map((c) => c.effectId));
test('#4 the 悬赏决策 draft: PRTS 敌人轮选 cards only (no 战术特训, no 鸭爵 set), each naming its battles in the official colours', () => {
test('#4 the 悬赏决策 draft: kill bounties only (no 战术特训, no 鸭爵 set, no multi-round card), each naming its battles in the official colours', () => {
let bountyDrafts = 0;
let cards = 0;
let withMulti = 0;
for (const modeId of BOUNTY_MODES) {
const gd = new GameData(DATA, modeId);
const sch = DATA.choices.schedule[modeId];
@@ -133,53 +132,46 @@ test('#4 the 悬赏决策 draft: PRTS 敌人轮选 cards only (no 战术特训,
const d = generateDraft(gd, createRng(seed * 131 + r), r, { stageId: 'act2autochess_m01' });
if (!d || d.family !== 'bounty') continue;
bountyDrafts++;
let multi = 0;
assert.equal(new Set(d.cards.map((c) => c.id)).size, d.cards.length, 'distinct cards');
for (const c of d.cards) {
cards++;
assert.equal(c.payout, 'kill', `${modeId} R${r}: ${c.id} ${c.name} — 战术特训 comes from 法术教鞭 only`);
assert.ok(!HIDDEN.has(c.id), `${c.id} ${c.name}: hidden from the PRTS table`);
assert.ok([1, 2].includes(c.rounds), `${c.id}: ${c.rounds} battles`);
if (MULTI.has(c.id)) {
multi++;
assert.equal(c.rounds, 2, `${c.id} ${c.name}: a multi-round card lasts two battles (the user's call)`);
assert.ok(!/每场/.test(c.descRaw || ''), `${c.id}: no 每场 left in the card text`);
}
assert.ok(!MULTI.has(c.id), `${c.id} ${c.name}: no official draft shows a multi-round card (player feedback #2)`);
assert.match(c.descRaw || '', DURATION_RE[c.rounds], `${c.id} ${c.name}: the card text shows its battles (${c.descRaw})`);
assert.equal(cardView(c).descRaw, c.descRaw, 'the public card carries the rich text (ui/choiceOverlay.js renders it first)');
}
if (multi) withMulti++;
}
}
}
assert.ok(bountyDrafts >= 100 && cards >= 400, `${bountyDrafts} bounty drafts, ${cards} cards`);
assert.ok(withMulti > 0, 'the multi-round cards are still offered (PRTS lists them), as two-battle cards');
});
test('#4 data: cards.bounty marks the draft pool after the PRTS table — 战术特训 (教鞭) and the 鸭爵 set are out, the multi-round cards and 源石虫·特训 in', () => {
test('#4 data: cards.bounty marks the draft pool — 战术特训 (教鞭), the 鸭爵 set and the cards no official draft shows are out, 源石虫·特训 in', () => {
const list = DATA.choices.cards.bounty;
const reasons = {};
for (const c of list) {
const expect = c.payout === 'kill' && !HIDDEN.has(c.effectId);
assert.equal(c.draft, expect, `${c.effectId} ${c.name}: draft ${c.draft}`);
if (c.payout !== 'kill') assert.equal(c.draftExcluded, 'perfect', `${c.effectId} ${c.name}`);
else if (HIDDEN.has(c.effectId)) assert.equal(c.draftExcluded, 'hidden', `${c.effectId} ${c.name}`);
if (!c.draft) reasons[c.draftExcluded] = (reasons[c.draftExcluded] || 0) + 1;
else assert.equal(c.draftExcluded, null);
}
assert.deepEqual(reasons, { perfect: 20, hidden: 4 });
assert.equal(list.filter((c) => c.draft).length, 105);
assert.deepEqual(reasons, { perfect: 20, hidden: 4, unseen: 19 });
assert.equal(list.filter((c) => c.draft).length, 86);
const multi = list.filter((c) => c.rounds >= 99 && c.payout === 'kill');
assert.equal(multi.length, 7, '多轮悬赏 · 假想敌 ×6 + 山海众头目·多轮悬赏');
assert.ok(multi.every((c) => c.draft), 'PRTS lists them in 敌人轮选');
assert.equal(list.find((c) => c.effectId === 'enemyeffect_5_1').draft, true, '源石虫·特训 is in the table too');
assert.ok(multi.every((c) => !c.draft && c.draftExcluded === 'unseen'), 'in none of the 27 official bounty drafts');
const slime = list.find((c) => c.effectId === 'enemyeffect_5_1');
assert.ok(slime.draft && slime.draftPool === 'boss', '源石虫·特训 is an R9 card (8 of the 11 official R9 drafts)');
});
/** Drive a co-op 绝境 match to its R3 (or R`round`) bounty draft; the human takes the first free card `want(card, free)`
* accepts. */
function pickAtR3(seed, want, round = 3) {
/** Drive a co-op 绝境 match to its R3 bounty draft; the human takes the first free card `want(card, free)` accepts. */
function pickAtR3(seed, want) {
const h = makeMatch({ mode: 'coop', difficulty: 'HARD', humans: 1, bots: 1, seed, fake: true }).start();
const m = h.m;
const ok = h.drive(() => m.phase === PHASE.SP_DRAFT && m.round === round, { ready: true });
assert.ok(ok && m.sp.family === 'bounty', `seed ${seed}: the R${round} bounty draft`);
const ok = h.drive(() => m.phase === PHASE.SP_DRAFT && m.round === 3, { ready: true });
assert.ok(ok && m.sp.family === 'bounty', `seed ${seed}: the R3 bounty draft`);
let picked = null;
for (let guard = 0; guard < 200 && m.phase === PHASE.SP_DRAFT; guard++) {
if (m.spTurn() === 'p_0') {
@@ -222,28 +214,29 @@ test('#4 E2E (co-op 绝境, the real R3 draft): a 1–2-battle bounty\'s enemies
});
test('#4 E2E: a multi-round card lasts two battles like the "两场作战" cards — blue text on the card and in the effects column (the user\'s call)', () => {
// "我不记得有过多轮悬赏" (user, after playtest #6): choices.js MULTI_ROUND_BOUNTY_BATTLES = 2; null restores 每场. The
// multi-round cards are second-half cards (the R9 draft; player feedback #2, choices.js bountyDraftHalf)
// "我不记得有过多轮悬赏" (user, after playtest #6): choices.js MULTI_ROUND_BOUNTY_BATTLES = 2; null restores 每场. No
// official draft shows a multi-round card (player feedback #2); 教鞭's 法术大师A2·多轮战术特训 is one
assert.equal(MULTI_ROUND_BOUNTY_BATTLES, 2);
let done = false;
for (let seed = 1; seed <= 40 && !done; seed++) {
const { h, m, ps, picked } = pickAtR3(700 + seed, (c) => MULTI.has(c.id), 9);
if (!picked) { m.dispose(); continue; }
done = true;
assert.equal(picked.rounds, 2);
assert.match(picked.descRaw, /接下来<@ba\.vup>两场作战<\/>/, 'the draft card says 两场作战 in blue');
assert.ok(!/每场/.test(picked.descRaw), 'no red 每场');
const b = ps.bounties.find((x) => x.card.effectId === picked.id);
assert.equal(b.roundsLeft, 2);
const e = ps.privateView().effects.find((x) => x.id === b.id);
assert.equal(e.counterText, '还剩 2 场作战');
assert.match(e.desc, /<@ba\.vup>两场作战<\/>/, 'the effects tooltip says the same');
h.drive(() => m.phase === PHASE.ROUND_START && m.round === 13, { ready: true });
assert.deepEqual(bountyRounds(b.id), [9, 10], 'its enemy comes for two battles, then never again');
assert.ok(!ps.bounties.some((x) => x.id === b.id), 'and the bounty is gone');
m.dispose();
}
assert.ok(done, 'some R9 draft offered a multi-round card');
const h = makeMatch({ mode: 'coop', difficulty: 'HARD', humans: 1, bots: 1, seed: 701, fake: true }).start();
const m = h.m;
h.toPrep(4);
const ps = h.ps('p_0');
const src = DATA.choices.cards.bounty.find((c) => c.effectId === 'enemyeffect_2');
assert.ok(MULTI.has(src.effectId) && src.payout === 'perfect', '法术大师A2·多轮战术特训');
const card = bountyCard(m.gd, src);
assert.equal(card.rounds, 2);
assert.match(card.descRaw, /接下来<@ba\.vup>两场作战<\/>/, 'the card says 两场作战 in blue');
assert.ok(!/每场/.test(card.descRaw), 'no red 每场');
const id = m.addBounty(ps, src);
const b = ps.bounties.find((x) => x.id === id);
assert.equal(b.roundsLeft, 2);
const e = ps.privateView().effects.find((x) => x.id === b.id);
assert.equal(e.counterText, '还剩 2 场作战');
assert.match(e.desc, /<@ba\.vup>两场作战<\/>/, 'the effects tooltip says the same');
h.drive(() => m.phase === PHASE.ROUND_START && m.round === 7, { ready: true });
assert.deepEqual(bountyRounds(b.id), [4, 5], 'its enemies come for two battles, then never again');
assert.ok(!ps.bounties.some((x) => x.id === b.id), 'and the bounty is gone');
m.dispose();
});
test('#4 the active bounty says how many battles it has left, with its card text in the official colours (effects column)', () => {