Files
ark-stronghold-protocol/public/js/ui/gameLogic/draft.js
T
Sukvii 56a7b918a9 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).
2026-10-08 20:45:54 +08:00

90 lines
4.4 KiB
JavaScript

// ui/gameLogic/draft.js — 机变 and band-draft normalisation. Re-exported from ../gameLogic.js.
import { isObj } from './shared.js';
import { PHASE } from '../../../../shared/constants.js';
import { t } from '../../../../shared/i18n.js';
// ---- 机变 / band draft normalisation ----------------------------------------------------------------------
/**
* Normalise m.public.draft ({order, turn, picks, skips?}) — tolerant of index/playerId turns and
* object/array picks.
* @param {any} draft
* @param {any[]} [players]
* @returns {{ order: string[], turnPid: string|null, picks: Map<string,string>, skipsLeft: Map<string, number>, done: boolean }}
*/
export function normalizeDraft(draft, players = []) {
const ids = (Array.isArray(players) ? players : []).filter(isObj).map((p) => p.playerId);
const order = Array.isArray(draft?.order) && draft.order.length ? draft.order.filter((x) => typeof x === 'string') : ids;
let turnPid = null;
if (typeof draft?.turn === 'string') turnPid = draft.turn;
else if (Number.isInteger(draft?.turn) && draft.turn >= 0 && draft.turn < order.length) turnPid = order[draft.turn];
const picks = new Map();
const src = draft?.picks;
if (Array.isArray(src)) {
src.forEach((v, i) => {
if (typeof v === 'string' && order[i]) picks.set(order[i], v);
else if (isObj(v) && typeof v.playerId === 'string' && typeof v.bandId === 'string') picks.set(v.playerId, v.bandId);
});
} else if (isObj(src)) {
for (const [k, v] of Object.entries(src)) if (typeof v === 'string' && v) picks.set(k, v);
}
const skipsLeft = new Map();
const sk = draft?.skipsLeft ?? draft?.skips;
if (isObj(sk)) for (const [k, v] of Object.entries(sk)) if (Number.isFinite(v)) skipsLeft.set(k, v);
const done = order.length > 0 && order.every((pid) => picks.has(pid));
return { order, turnPid, picks, skipsLeft, done };
}
/**
* Normalise m.public.sp ({family, cards, turn, picks, order?}).
* Cards: string ids or objects; picks: {playerId: cardIdx} | [{playerId, idx}] | card.takenBy.
* @param {any} sp
* @param {any[]} [players]
*/
export function normalizeSp(sp, players = []) {
if (!isObj(sp)) return null;
const ids = (Array.isArray(players) ? players : []).filter(isObj).map((p) => p.playerId);
const order = Array.isArray(sp.order) && sp.order.length ? sp.order.filter((x) => typeof x === 'string') : ids;
const cards = (Array.isArray(sp.cards) ? sp.cards : []).slice(0, 6).map((c, idx) => {
const card = typeof c === 'string' ? { id: c } : isObj(c) ? { ...c } : {};
return { ...card, idx, takenBy: typeof card.takenBy === 'string' ? card.takenBy : null };
});
const takenBy = new Map(); // idx → playerId
const pickOf = new Map(); // playerId → idx
const src = sp.picks;
const add = (pid, idx) => {
if (typeof pid !== 'string' || !Number.isInteger(idx) || idx < 0 || idx >= cards.length) return;
takenBy.set(idx, pid);
pickOf.set(pid, idx);
};
if (Array.isArray(src)) src.forEach((v, i) => { if (isObj(v)) add(v.playerId, v.idx); else if (Number.isInteger(v) && order[i]) add(order[i], v); });
else if (isObj(src)) for (const [k, v] of Object.entries(src)) add(k, v);
if (isObj(sp.taken)) for (const [k, v] of Object.entries(sp.taken)) add(v, Number(k));
for (const c of cards) if (c.takenBy) add(c.takenBy, c.idx);
for (const c of cards) c.takenBy = takenBy.get(c.idx) ?? null;
let turnPid = null;
if (typeof sp.turn === 'string') turnPid = sp.turn;
else if (Number.isInteger(sp.turn) && sp.turn >= 0 && sp.turn < order.length) turnPid = order[sp.turn];
return {
family: typeof sp.family === 'string' ? sp.family : null,
name: typeof sp.name === 'string' && sp.name ? sp.name : null,
desc: typeof sp.desc === 'string' && sp.desc ? sp.desc : null,
untimed: !!sp.untimed,
cards, order, turnPid, pickOf, takenBy, pickedCount: pickOf.size,
};
}
/** Adapt only the recipient's current PREP choice; never write it into the public draft. */
export function normalizePersonalChoice(pub, priv, myId) {
const choice = priv?.personalChoice;
if (pub?.phase !== PHASE.PREP || !priv || priv.playerId !== myId || priv.alive === false
|| !choice || choice.round !== pub.round) return null;
return {
...normalizeSp({ family: 'bounty', name: t('教鞭 · 战术特训'), desc: t('请选择一项战术特训'),
cards: choice.cards, turn: myId, order: [myId], picks: {} }),
id: choice.id,
};
}