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
+22
-3
@@ -101,7 +101,7 @@ function refreshPrivate() {
|
||||
const p = S.priv;
|
||||
p.deployCount = p.board.filter((x) => x.kind === 'chess').length;
|
||||
p.bonds = computeBonds(p);
|
||||
p.canReady = !p.temp.some(Boolean);
|
||||
p.canReady = !p.personalChoice && !p.temp.some(Boolean);
|
||||
const upg = [5, 8, 11, 12, 13];
|
||||
p.shop.maxLevel = 6;
|
||||
if (p.shop.upgradePrice == null) p.shop.upgradePrice = upg[p.shop.level - 1] ?? 0;
|
||||
@@ -178,7 +178,7 @@ function buildState() {
|
||||
].sort((a, b) => a.t - b.t);
|
||||
|
||||
const priv = {
|
||||
playerId: ME, seat: 0, alive: !VARIANTS.has('dead'), lp: 24, funds: 13, bandId: 'band_bldsk', ready: false, canReady: true,
|
||||
playerId: ME, seat: 0, alive: !VARIANTS.has('dead'), lp: 24, funds: 13, bandId: 'band_bldsk', ready: false, canReady: true, personalChoice: null,
|
||||
shop: { level, maxLevel: 6, upgradePrice: 9, refreshPrice: 1, freeRefreshes: 0, frozen: VARIANTS.has('frozen'), slots, rewardOffer: null },
|
||||
hand, temp, board, deployCap: 8, deployCount: 0, bonds: [],
|
||||
effects: [
|
||||
@@ -661,7 +661,16 @@ async function mockRequest(t, f = {}) {
|
||||
}
|
||||
pushPublic(); return {};
|
||||
}
|
||||
case 'g.art': { prepOnly(); const it = findPiece(f.itemUid); if (!it) fail('BAD_TARGET'); removeAt(it); toast('奇术已生效', 'success'); refreshPrivate(); return {}; }
|
||||
case 'g.art': {
|
||||
prepOnly(); const it = findPiece(f.itemUid); if (!it) fail('BAD_TARGET');
|
||||
if (it.piece.id === 'chess_item_6_03_m') {
|
||||
if (p.personalChoice) fail('BAD_TARGET');
|
||||
const cards = data.get('choices').cards.bounty.filter((c) => c.payout === 'perfect' && c.rounds < 90).slice(0, 3)
|
||||
.map((c) => ({ ...c, kind: 'bounty', id: c.effectId, descRaw: data.lookup('effects', c.effectId)?.descRaw }));
|
||||
p.personalChoice = { id: `mock.choice.${nextUid()}`, round: pub.round, sourceItemId: it.piece.id, cards };
|
||||
}
|
||||
removeAt(it); toast('奇术已生效', 'success'); refreshPrivate(); return {};
|
||||
}
|
||||
case 'g.reward': {
|
||||
if (!p.shop.rewardOffer) fail('WRONG_PHASE');
|
||||
const s = p.shop.rewardOffer.slots[f.idx]; if (!s) fail('BAD_TARGET');
|
||||
@@ -669,6 +678,15 @@ async function mockRequest(t, f = {}) {
|
||||
p.hand[idx] = chessPiece(data.lookup('chess', s.id)); p.shop.rewardOffer = null; refreshPrivate(); return {};
|
||||
}
|
||||
case 'g.choice': {
|
||||
if (f.choiceId !== undefined) {
|
||||
prepOnly();
|
||||
const choice = p.personalChoice;
|
||||
if (!choice || choice.id !== f.choiceId || choice.round !== pub.round || !choice.cards[f.idx]) fail('BAD_TARGET');
|
||||
const card = choice.cards[f.idx];
|
||||
p.effects.push({ id: choice.id, name: card.name, desc: card.descRaw || card.desc, iconKind: 'choice', iconId: card.id });
|
||||
p.personalChoice = null;
|
||||
refreshPrivate(); return {};
|
||||
}
|
||||
if (!pub.sp) fail('WRONG_PHASE');
|
||||
const taken = Object.values(pub.sp.picks).includes(f.idx);
|
||||
if (taken) fail('BAD_TARGET');
|
||||
@@ -676,6 +694,7 @@ async function mockRequest(t, f = {}) {
|
||||
}
|
||||
case 'g.ready': {
|
||||
if (pub.phase !== PHASE.PREP) fail('WRONG_PHASE');
|
||||
if (f.ready && p.personalChoice) fail('BAD_TARGET');
|
||||
if (f.ready && !p.canReady) fail('TEMP_NOT_EMPTY');
|
||||
p.ready = !!f.ready; refreshPrivate(); return {};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user