feedback7 G-SMALL: co-op rooms can make AI teammates draft last (PR #368 by @anAtheist987)

Applied the host-only room option 「AI 队友最后选择」, default off. Off, the draft order and the random streams stay as they are. On, humans draft before AI seats after the existing shuffle, and a skip stays ahead of the AI seats.

Dropped docs/history/0.2.2.md and every §27 citation. The player-facing sentence is in docs/PLAYING.md. The room-option rule is in docs/design/match.md, docs/design/client.md and docs/design/network.md. The #338 per-bot faction and ban presets are not included.

Tests: node --test test/lobby-ai-last.test.js test/match/ai-picks-last.test.js test/ui/ai-picks-last.test.js — 18 pass. node --test test/lobby.test.js test/client-static.test.js — 378 pass. node tools/i18n.mjs check --all --strict — 1032/1032 in en, ja, ko and zh-TW, 0 errors.
This commit is contained in:
anAtheist987 authored and sganggs committed 2026-10-08 19:57:26 +08:00
1 parent ae22335cfe
commit fd791a66cb
19 files changed
+527 -21

No files matched your search

+28 -2
View File
@@ -40,8 +40,9 @@ export class MatchPhases {
enterBandDraft() {
if (this.phase !== PHASE.INFO_CHECK) return;
this.phase = PHASE.BAND_DRAFT;
const order = this.order.map((p) => p.playerId);
let order = this.order.map((p) => p.playerId);
if (!this.isSolo) this.rngDraft.shuffle(order);
order = this.humansFirst(order);
const skips = this.isSolo ? 0 : this.gd.bandDraft.skipsPerPlayer;
const untimed = this.soloUntimed;
this.draft = {
@@ -54,6 +55,22 @@ export class MatchPhases {
this.markPublic();
}
/**
* The co-op room option 「AI 队友最后选择」 (this.aiPicksLast, GitHub #338): every human seat before every AI seat, each
* group in the order the draft drew (a stable partition applied AFTER the shuffle — no extra random draw: the order
* with the option off is unchanged, and with it on every random stream stands where it would without it; only the
* picks made in the new order can differ). A human is any seat that is not an AI seat (room.addBot): under AI 托管,
* disconnected or departed it still counts as a human. Used by the strategy draft and the 机变 draft
* (MatchSpDraft.enterSpDraft).
* @param {string[]} order playerIds in drawn order
* @returns {string[]}
*/
humansFirst(order) {
if (!this.aiPicksLast) return order;
const bot = (pid) => !!this.players.get(pid)?.isBot;
return [...order.filter((pid) => !bot(pid)), ...order.filter(bot)];
}
draftTurn() {
const d = this.draft;
if (!d) return null;
@@ -194,7 +211,16 @@ export class MatchPhases {
if (d.order.length - d.idx <= 1) return fail(ERR.BAD_TARGET, 'nobody to pass to');
d.skipsLeft[ps.playerId]--;
d.order.splice(d.idx, 1);
d.order.push(ps.playerId);
// the skipper goes to the end; with 「AI 队友最后选择」 to the end of the humans still to pick — behind them, ahead of
// the AI seats — and to the very end only when no other human is left to pass to [ASSUMED: the option's intent,
// humans before AI, kept through a skip; no source, a remake option]
let at = d.order.length;
if (this.aiPicksLast) {
for (let j = d.order.length - 1; j >= d.idx; j--) {
if (!this.players.get(d.order[j])?.isBot) { at = j + 1; break; }
}
}
d.order.splice(at, 0, ps.playerId);
this.startDraftTurn();
return OK;
}
+3 -1
View File
@@ -15,8 +15,10 @@ export class MatchSpDraft {
const alive = this.alivePlayers();
if (!draft || !alive.length) { this.enterPrep(); return; }
this.phase = PHASE.SP_DRAFT;
const order = alive.map((p) => p.playerId);
let order = alive.map((p) => p.playerId);
if (!this.isSolo) this.rngDraft.shuffle(order);
// 「AI 队友最后选择」 (GitHub #338): humans before AI seats, after the same shuffle (MatchPhases.humansFirst)
order = this.humansFirst(order);
// untimed: solo and any single-human match (soloUntimed); the co-op order / 6 cards stay
const untimed = this.soloUntimed;
this.sp = { ...draft, order, idx: 0, picks: {}, taken: {}, untimed, turnDeadline: 0 };