feedback7 dober: 教鞭 is the official personal choice of three 战术特训 cards again, not a random one (PR #353 by @Sukvii, the owner's decision of 2026-10-08); golden: coop2-NORMAL-8-serverrun and coop2-ABYSS-10-boosted move
Seen: the Art 教鞭 (杜宾's strategy) drew three 战术特训 cards and added a random one — a documented simplification (docs/META.md §2.5: a personal choice would need its own phase, protocol message, timer and AI takeover). Official: act2autochess choice_event hunter_band_1 is a PERSONAL_CHOOSE; PRTS 下半 记录 §法术 教鞭 "使用后销毁,于3个战术特训的悬赏任务中选择一项". The owner decided on 2026-10-08 to follow it. Taken from PR #353 (code and tests by @Sukvii), ported to the split 0.2.2 modules. Now: ctx.offerBountyChoice -> Match.offerBountyChoice keeps up to three different mode-eligible cards (15 of 20 in 标准 / 险境, 20 in 绝境 / 终极; one meta-rng shuffle) in PlayerState.personalChoice, m.private only (same cards after a reconnect); g.choice { idx, choiceId } confirms it (Match.pickPersonalChoice: owner, id, round, index; a request without a choiceId is still the public draft, a stale id never falls through to it). No new phase or timer: Ready is refused while it is open, the prep's deadline picks at random before the temp pieces and Ready, a bot or a 托管 seat picks with the bounty scorer (botPickCard, the bots' seeded rng; at the prep's start, after the cast, before Ready; the scheduler's fallback picks at random if a step fails). The client reuses ChoiceOverlay (personal: no order, no turns, the prep's countdown) over a locked prep, the Ready reasons, the mock harness; six UI strings in en / ja / ko / zh-TW. An eliminated player's offer goes with it; invariants.js flags an offer outside its owner's own open prep. Not taken from the PR: its §25.24 in the released docs/history/0.2.0.md (the 0.2.2 section is the integrator's), its edits of the Ready toggle beyond the personal-choice case, its tail placement of the i18n keys. Golden: coop2-NORMAL-8-serverrun (ai_0 杜宾 casts 教鞭 from R2 on and now scores the three cards: teamLp 39 -> 34, virtualMs 121400 -> 118400, rng shop 511 -> 483 / bots 18063 -> 18070 / meta 112 -> 103) and coop2-ABYSS-10-boosted (ai_1 杜宾: virtualMs 121650 -> 124650, shop 590 -> 589 / bots 22075 -> 22096 / meta 141 -> 154); both first differ in round 2, every other scenario and family is unchanged; node tools/golden.mjs --twice is identical. Tests: items.test.js (three distinct cards, the tapped one applied once, nothing before the confirm, a refused cast keeps the Art / count / rng / uid, an eliminated player's offer), economy.test.js (Ready refused, the deadline picks before endPrep, solo prep untimed), connection.test.js (two players' offers, reconnect, foreign / duplicate / old ids, an offline player's pick at the deadline), bot.test.js (托管 takeover incl. the failing-step fallback), bot-smarter.test.js (the scorer's pick, deterministic per seed, no rng draw when nothing is pending), followups.test.js (R14 / R15), draft.test.js, unitStats.test.js (protocol), hardening.test.js (invariant), gameLogic / playtest3 / playtest4 (view model, Ready reason, ChoiceView, actions), mock.e2e.test.js and playtest2.real.e2e.test.js (browser, SP_E2E=1).
This commit is contained in:
1 parent
05eb2b807f
commit
56a7b918a9
48 files changed
+901
-159
No files matched your search
@@ -1139,7 +1139,23 @@ test('教鞭 (Art): a 战术特训 bounty (PRTS "于3个战术特训的悬赏任
|
||||
const { m, ps } = setup({ seed: 60 + s });
|
||||
for (let k = 0; k < 2; k++) {
|
||||
const art = giveItem(m, ps, 'chess_item_6_03_m');
|
||||
const deadline = m.deadline;
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.art', itemUid: art.uid, row: 10, col: 5 }), OK);
|
||||
const pending = ps.personalChoice;
|
||||
assert.equal(ps.bounties.length, k, 'offering cards does not add a bounty');
|
||||
assert.equal(pending.cards.length, 3);
|
||||
assert.equal(new Set(pending.cards.map((c) => c.effectId)).size, 3);
|
||||
assert.equal(pending.round, m.round);
|
||||
assert.equal(pending.sourceItemId, art.id);
|
||||
assert.equal(m.deadline, deadline, 'uses the existing PREP clock');
|
||||
assert.equal(ps.find(art.uid), null);
|
||||
assert.equal(ps.round.arts, k + 1, 'consumed when offered');
|
||||
for (const c of pending.cards) assert.ok(training.has(c.effectId) && !m.gd.inactiveEnemies.has(c.enemyKey));
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.choice', idx: 1, choiceId: pending.id }), OK);
|
||||
assert.equal(ps.personalChoice, null);
|
||||
assert.equal(ps.bounties[k].card.effectId, pending.cards[1].effectId, 'the submitted card is applied');
|
||||
assert.equal(ps.round.arts, k + 1, 'picking does not consume another Art');
|
||||
assert.equal(m.handle('p_0', { t: 'g.choice', idx: 1, choiceId: pending.id }).error, 'BAD_TARGET');
|
||||
}
|
||||
assert.equal(ps.bounties.length, 2);
|
||||
for (const b of ps.bounties) {
|
||||
@@ -1150,7 +1166,7 @@ test('教鞭 (Art): a 战术特训 bounty (PRTS "于3个战术特训的悬赏任
|
||||
seen.add(b.card.effectId);
|
||||
}
|
||||
}
|
||||
assert.ok(seen.size >= 5, 'random pick');
|
||||
assert.ok(seen.size >= 5, 'different offered cards across seeds');
|
||||
const { h, m } = setup({ mode: 'coop', humans: 2, seed: 7 });
|
||||
const p0 = h.ps('p_0'), p1 = h.ps('p_1');
|
||||
const art = giveItem(m, p0, 'chess_item_6_01_m');
|
||||
@@ -1161,9 +1177,60 @@ test('教鞭 (Art): a 战术特训 bounty (PRTS "于3个战术特训的悬赏任
|
||||
const passed = p1.hand.find((p) => p && p.id === 'chess_item_6_01_m');
|
||||
assert.deepEqual(m.handle('p_1', { t: 'g.art', itemUid: passed.uid, row: 10, col: 5 }), OK);
|
||||
assert.match(p1.bounties[0].card.effectId, /^enemyeffect_b_/);
|
||||
assert.equal(p1.personalChoice, null, '神秘顾客 still applies its random bounty immediately');
|
||||
cover('chess_item_6_03_m', 'chess_item_6_01_m');
|
||||
});
|
||||
|
||||
test('教鞭: pending and empty-pool failures keep the Art, Arts count and RNG; short offers use actual cards', () => {
|
||||
const { m, ps } = setup();
|
||||
const first = giveItem(m, ps, 'chess_item_6_03_m');
|
||||
const second = giveItem(m, ps, 'chess_item_6_03_m', 'temp');
|
||||
assert.deepEqual(ps.useArt(first.uid, 10, 5), OK);
|
||||
const pending = ps.personalChoice;
|
||||
const rng = m.rngMeta.state(), uid = m.uidSeq;
|
||||
assert.deepEqual(ps.useArt(second.uid, 10, 5), { error: 'BAD_TARGET', detail: '请先完成当前教鞭选择' });
|
||||
assert.equal(ps.personalChoice, pending);
|
||||
assert.ok(ps.find(second.uid));
|
||||
assert.equal(ps.round.arts, 1);
|
||||
assert.equal(m.rngMeta.state(), rng);
|
||||
assert.equal(m.uidSeq, uid);
|
||||
assert.equal(m.pickPersonalChoice(ps, -1, pending.id).error, 'BAD_TARGET');
|
||||
assert.equal(m.pickPersonalChoice(ps, 0.5, pending.id).error, 'BAD_TARGET');
|
||||
assert.equal(m.pickPersonalChoice(ps, 3, pending.id).error, 'BAD_TARGET');
|
||||
assert.equal(ps.personalChoice, pending);
|
||||
assert.deepEqual(m.pickPersonalChoice(ps, 0, pending.id), OK);
|
||||
const training = DATA.choices.cards.bounty.filter((c) => c.payout === 'perfect');
|
||||
m.gd.inactiveEnemies = new Set(training.map((c) => c.enemyKey));
|
||||
assert.deepEqual(ps.useArt(second.uid, 10, 5), { error: 'BAD_TARGET', detail: '当前没有可用的战术特训' });
|
||||
assert.ok(ps.find(second.uid));
|
||||
assert.equal(ps.round.arts, 1);
|
||||
assert.equal(ps.personalChoice, null);
|
||||
assert.equal(m.rngMeta.state(), rng);
|
||||
const allowed = training.find((c) => c.multiRound && m.gd.enemy(c.enemyKey));
|
||||
m.gd.inactiveEnemies.delete(allowed.enemyKey);
|
||||
assert.deepEqual(ps.useArt(second.uid, 10, 5), OK);
|
||||
assert.ok(ps.personalChoice.cards.length > 0 && ps.personalChoice.cards.length < 3);
|
||||
assert.equal(new Set(ps.personalChoice.cards.map((c) => c.effectId)).size, ps.personalChoice.cards.length);
|
||||
assert.equal(ps.find(second.uid), null, 'Arts in temp are consumed too');
|
||||
const view = ps.privateView().personalChoice;
|
||||
assert.ok(view.cards.every((c) => c.kind === 'bounty' && c.rounds === 2));
|
||||
assert.ok(view.cards.every((c) => !/每场/.test(c.descRaw || c.desc)), 'multi-round candidates use the same two-battle text as applied bounties');
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('教鞭: an open choice goes with a player who is eliminated, and nothing of it is left in its views', () => {
|
||||
const { h, m } = setup({ mode: 'coop', humans: 2, seed: 8 });
|
||||
const a = h.ps('p_0');
|
||||
const art = giveItem(m, a, 'chess_item_6_03_m');
|
||||
assert.deepEqual(a.useArt(art.uid, 10, 5), { ok: true });
|
||||
assert.equal(a.privateView().personalChoice.cards.length, 3);
|
||||
a.eliminate(m.round);
|
||||
assert.equal(a.personalChoice, null);
|
||||
assert.equal(a.privateView().personalChoice, null);
|
||||
assert.equal(a.privateView().canReady, false);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('merging: two identical normal items become the golden one (upgradeNum 2); upgradeNum-100 items never merge', () => {
|
||||
const never = Object.values(DATA.items).filter((r) => r.itemType === 'EQUIP' && !r.isGolden && r.upgradeNum === 100).map((r) => r.name).sort();
|
||||
assert.deepEqual(never, ['人事部文档', '信标', '变形同构体', '寻呼模块', '拟态物质', '突变细胞'].sort());
|
||||
|
||||
Reference in new issue
Block a user