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
@@ -31,7 +31,9 @@ export class MatchIntents {
|
||||
case 'g.art': return ps.useArt(msg.itemUid, msg.row, msg.col, msg.dir);
|
||||
case 'g.destroy': return ps.destroy(msg.uid);
|
||||
case 'g.reward': return ps.pickReward(msg.idx);
|
||||
case 'g.choice': return this.pickCard(ps, msg.idx);
|
||||
case 'g.choice': return msg.choiceId !== undefined
|
||||
? this.pickPersonalChoice(ps, msg.idx, msg.choiceId)
|
||||
: this.pickCard(ps, msg.idx);
|
||||
case 'g.ready': return ps.setReady(!!msg.ready);
|
||||
case 'g.emote': return this.emote(ps, msg.id);
|
||||
// playerId: the player tapped (a shared field names two) — the watch preference (item 56)
|
||||
|
||||
@@ -62,6 +62,7 @@ export class MatchPrep {
|
||||
};
|
||||
const ready = () => {
|
||||
if (!ps.ready) {
|
||||
this.autoPickPersonalChoice(ps, 'random');
|
||||
ps.resolveTemp();
|
||||
ps.setReady(true);
|
||||
}
|
||||
@@ -113,6 +114,7 @@ export class MatchPrep {
|
||||
if (this.phase !== PHASE.PREP) return;
|
||||
for (const ps of this.alivePlayers()) {
|
||||
if (ps.ready) continue;
|
||||
this.autoPickPersonalChoice(ps, 'random');
|
||||
ps.resolveTemp();
|
||||
ps.ready = true;
|
||||
ps.dirty();
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
// server/match/match/spDraft.js — Match methods: the 机变 draft (SP_DRAFT: turn order, timers, AI picks, the card applied
|
||||
// by choices.js applyCard) and the rolls the meta effects ask for — bounties (addBounty), item ids (rollItemId) and
|
||||
// choices.json pools (rollPool).
|
||||
// by choices.js applyCard), the personal bounty choice of the Art 教鞭 (offerBountyChoice / pickPersonalChoice /
|
||||
// autoPickPersonalChoice: held by the player inside PREP — no phase, no timer of its own) and the rolls the meta effects
|
||||
// ask for — bounties (addBounty), item ids (rollItemId) and choices.json pools (rollPool).
|
||||
// Installed on Match.prototype by server/match/Match.js (a method container: never instantiated; `this` is the match).
|
||||
|
||||
import { PHASE, ERR } from '../../../shared/constants.js';
|
||||
import { generateDraft, applyCard, bountyBattles, isMultiRoundBounty } from '../choices.js';
|
||||
import { generateDraft, applyCard, bountyBattles, isMultiRoundBounty, bountyCard } from '../choices.js';
|
||||
import { weightedPick } from '../waves.js';
|
||||
import { botPickCard } from '../bot.js';
|
||||
import { OK, fail, DELAYS } from './common.js';
|
||||
|
||||
/** Cards of the personal choice (PRTS 卫戍协议:盟约 下半 §法术 教鞭: "于3个战术特训的悬赏任务中选择一项"). */
|
||||
const PERSONAL_OFFER_SIZE = 3;
|
||||
|
||||
export class MatchSpDraft {
|
||||
enterSpDraft() {
|
||||
const draft = generateDraft(this.gd, this.rngDraft, this.round, { stageId: this.stageId, bondAvailable: (bondId) => this.bondLive(bondId) });
|
||||
@@ -106,6 +110,58 @@ export class MatchSpDraft {
|
||||
this.enterPrep();
|
||||
}
|
||||
|
||||
/**
|
||||
* 教鞭 (content/items/meta.js, ctx.offerBountyChoice): the Art opens a PERSONAL choice (act2autochess choice event
|
||||
* `hunter_band_1`, choiceType PERSONAL_CHOOSE) between up to three of `candidates` — drawn once from the meta rng, kept in
|
||||
* `ps.personalChoice` (m.private only) until the owner confirms one with `g.choice { idx, choiceId }`. It lives inside
|
||||
* PREP: no phase of its own and no timer — the prep's deadline resolves it (prepDeadline), Ready is refused while it is
|
||||
* pending (PlayerPrep.setReady). Refused — before an rng draw or an id is used, so the Art stays in the hand — when it
|
||||
* is not the player's PREP, a choice is already open or no card is left to offer.
|
||||
* @param {import('../PlayerState.js').PlayerState} ps @param {any[]} candidates cards.bounty entries
|
||||
* @param {string} sourceItemId the Art
|
||||
*/
|
||||
offerBountyChoice(ps, candidates, sourceItemId) {
|
||||
if (this.phase !== PHASE.PREP) return fail(ERR.WRONG_PHASE);
|
||||
if (!ps.alive) return fail(ERR.ELIMINATED);
|
||||
if (ps.ready) return fail(ERR.WRONG_PHASE, 'ready');
|
||||
if (ps.personalChoice) return fail(ERR.BAD_TARGET, '请先完成当前教鞭选择'); // i18n-ignore: developer error detail
|
||||
if (!candidates.length) return fail(ERR.BAD_TARGET, '当前没有可用的战术特训'); // i18n-ignore: developer error detail
|
||||
const cards = this.rngMeta.shuffle(candidates.slice()).slice(0, PERSONAL_OFFER_SIZE);
|
||||
ps.personalChoice = { id: `${this.battlePrefix}.choice.${this.nextUid()}`, round: this.round, sourceItemId, cards };
|
||||
ps.dirty();
|
||||
return OK;
|
||||
}
|
||||
|
||||
/** g.choice { idx, choiceId }: the owner's pick of its open personal choice — the card becomes a bounty on its next battles. */
|
||||
pickPersonalChoice(ps, idx, choiceId) {
|
||||
if (this.phase !== PHASE.PREP) return fail(ERR.WRONG_PHASE);
|
||||
if (!ps.alive) return fail(ERR.ELIMINATED);
|
||||
if (ps.ready) return fail(ERR.WRONG_PHASE, 'ready');
|
||||
const pending = ps.personalChoice;
|
||||
if (!pending || pending.id !== choiceId || pending.round !== this.round) return fail(ERR.BAD_TARGET);
|
||||
if (!Number.isInteger(idx) || idx < 0 || idx >= pending.cards.length) return fail(ERR.BAD_TARGET);
|
||||
if (!this.addBounty(ps, pending.cards[idx])) return fail(ERR.BAD_TARGET);
|
||||
ps.personalChoice = null;
|
||||
ps.dirty();
|
||||
return OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* The pick made for a player who did not make it. 'bot': an AI seat or a human under AI 托管 — the bounty scorer of the
|
||||
* 机变 draft (bot.js botPickCard: the expected pay minus the expected LP lost against the own board, the bots' seeded rng
|
||||
* only breaks ties); 'random': the prep's deadline and a seat the engine finishes by other means — one of the cards at
|
||||
* random from the meta rng, like a 机变 turn that runs out. Nothing pending: nothing happens, no rng draw.
|
||||
*/
|
||||
autoPickPersonalChoice(ps, mode) {
|
||||
const pending = ps.personalChoice;
|
||||
if (!pending) return undefined;
|
||||
const indices = pending.cards.map((c, i) => i);
|
||||
const idx = mode === 'bot'
|
||||
? botPickCard(this, ps, pending.cards.map((c) => bountyCard(this.gd, c)), indices)
|
||||
: this.rngMeta.pick(indices);
|
||||
return this.pickPersonalChoice(ps, idx, pending.id);
|
||||
}
|
||||
|
||||
addBounty(ps, card) {
|
||||
if (!ps || !card || !this.gd.enemy(card.enemyKey)) return null;
|
||||
// a multi-round card lasts MULTI_ROUND_BOUNTY_BATTLES battles (choices.js; the user's call after playtest #6)
|
||||
|
||||
Reference in new issue
Block a user