fix(bot): respect teammates opening factions
This commit is contained in:
4 files changed
+54
-4
No files matched your search
@@ -122,7 +122,9 @@ Official rule (bwiki 盟约, 更新公告 5114): before a match the player canno
|
|||||||
|
|
||||||
[ASSUMED] Bot purchasing applies one soft 18-point penalty to non-elite, non-DIY tier V/VI shared-pool cards whose
|
[ASSUMED] Bot purchasing applies one soft 18-point penalty to non-elite, non-DIY tier V/VI shared-pool cards whose
|
||||||
active bonds overlap another living teammate's opening strategy. Human and AI teammates are treated equally; it
|
active bonds overlap another living teammate's opening strategy. Human and AI teammates are treated equally; it
|
||||||
neither reserves stock nor prevents a stronger purchase. Existing strategy presets and rehearsal counts are unchanged.
|
neither reserves stock nor prevents a stronger purchase. A living teammate's selected strategy also reserves its bond
|
||||||
|
for bot strategy selection and focus planning before any member is deployed. The reservation ends when that teammate is
|
||||||
|
eliminated. Existing strategy presets and rehearsal counts are unchanged.
|
||||||
|
|
||||||
[ASSUMED] AI reactions are cosmetic social choices, enabled by default and disabled with `SP_BOT_EMOTES=0`. Only
|
[ASSUMED] AI reactions are cosmetic social choices, enabled by default and disabled with `SP_BOT_EMOTES=0`. Only
|
||||||
mixed human/AI matches emit them, through the existing whitelist and cooldown. A human emote gets at most one eligible
|
mixed human/AI matches emit them, through the existing whitelist and cooldown. A human emote gets at most one eligible
|
||||||
|
|||||||
@@ -274,3 +274,10 @@ enemy: picking emitted a unit click with no definition, instead of the tile clic
|
|||||||
engine-canvas regression retains the card/100 HP assertions and also covers another field refresh, live HP loss and
|
engine-canvas regression retains the card/100 HP assertions and also covers another field refresh, live HP loss and
|
||||||
closing the card when the crate is destroyed. Device metadata supplied by the new entry still wins; changing fields
|
closing the card when the crate is destroyed. Device metadata supplied by the new entry still wins; changing fields
|
||||||
does not carry old devices across. Gameplay and allied targeting rules are unchanged.
|
does not carry old devices across. Gameplay and allied targeting rules are unchanged.
|
||||||
|
|
||||||
|
### 29.15 Opening faction reservations for bots
|
||||||
|
|
||||||
|
When a living teammate has already selected a faction strategy, the bot now treats that strategy's bond as an opening
|
||||||
|
reservation before the teammate has deployed a member. It avoids selecting the same offered strategy and includes the
|
||||||
|
bond in focus planning and the existing soft penalty for shared high-tier cards. The reservation is removed when the
|
||||||
|
teammate is eliminated; it does not reserve cards or prevent an immediate merge from winning.
|
||||||
+5
-2
@@ -146,7 +146,8 @@ export function botPickBand(m, ps) {
|
|||||||
const ids = gd.bandIds();
|
const ids = gd.bandIds();
|
||||||
if (!ids.length) return gd.defaultBandId;
|
if (!ids.length) return gd.defaultBandId;
|
||||||
const lateFunds = (id) => /暂存/.test(String(gd.band(id)?.desc || ''));
|
const lateFunds = (id) => /暂存/.test(String(gd.band(id)?.desc || ''));
|
||||||
const offBond = (id) => gd.bandBondIds(id).some((b) => gd.modeInactiveBonds.has(b));
|
const reserved = new Set(m.alivePlayers().filter((p) => p !== ps).flatMap((p) => gd.bandBondIds(p.bandId)));
|
||||||
|
const offBond = (id) => gd.bandBondIds(id).some((b) => gd.modeInactiveBonds.has(b) || reserved.has(b));
|
||||||
const pairs = ids.map((id) => [id, offBond(id) ? 0 : Math.max(1, (gd.startLp(id) - 18) ** 2) * (m.isSolo && lateFunds(id) ? 0.02 : 1)]);
|
const pairs = ids.map((id) => [id, offBond(id) ? 0 : Math.max(1, (gd.startLp(id) - 18) ** 2) * (m.isSolo && lateFunds(id) ? 0.02 : 1)]);
|
||||||
let total = 0;
|
let total = 0;
|
||||||
for (const [, w] of pairs) total += w;
|
for (const [, w] of pairs) total += w;
|
||||||
@@ -353,7 +354,8 @@ function mainCoreBond(m, p) {
|
|||||||
* across rounds.
|
* across rounds.
|
||||||
*/
|
*/
|
||||||
export function bondPlan(m, ps, owned = ownedBonds(m, ps)) {
|
export function bondPlan(m, ps, owned = ownedBonds(m, ps)) {
|
||||||
const key = `${m.round}|${ps.shop.level}|${[...owned.counts.entries()].map(([k, v]) => k + v).join()}`;
|
const key = `${m.round}|${ps.shop.level}|${[...owned.counts.entries()].map(([k, v]) => k + v).join()}|${m.alivePlayers()
|
||||||
|
.filter((p) => p !== ps).flatMap((p) => m.gd.bandBondIds(p.bandId)).sort().join()}`;
|
||||||
if (ps._botFocus && ps._botFocus.key === key) return ps._botFocus;
|
if (ps._botFocus && ps._botFocus.key === key) return ps._botFocus;
|
||||||
const prev = ps._botFocusId ?? null;
|
const prev = ps._botFocusId ?? null;
|
||||||
const mates = new Set();
|
const mates = new Set();
|
||||||
@@ -361,6 +363,7 @@ export function bondPlan(m, ps, owned = ownedBonds(m, ps)) {
|
|||||||
if (p === ps) continue;
|
if (p === ps) continue;
|
||||||
const b = mainCoreBond(m, p);
|
const b = mainCoreBond(m, p);
|
||||||
if (b) mates.add(b);
|
if (b) mates.add(b);
|
||||||
|
for (const id of m.gd.bandBondIds(p.bandId)) mates.add(id);
|
||||||
}
|
}
|
||||||
let focus = null;
|
let focus = null;
|
||||||
let bestS = 0;
|
let bestS = 0;
|
||||||
|
|||||||
@@ -7,12 +7,50 @@
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { PHASE } from '../../shared/constants.js';
|
import { PHASE } from '../../shared/constants.js';
|
||||||
import { botPickCard, bountyKillChance, itemTarget, arrange, botPrepBegin, botPrepEnd, bondPlan, rangeTiles, effDps, rangeRec, buyScoreOf } from '../../server/match/bot.js';
|
import { botPickBand, botPickCard, bountyKillChance, itemTarget, arrange, botPrepBegin, botPrepEnd, bondPlan, rangeTiles, effDps, rangeRec, buyScoreOf } from '../../server/match/bot.js';
|
||||||
import { parseKey, tileKey } from '../../server/match/board.js';
|
import { parseKey, tileKey } from '../../server/match/board.js';
|
||||||
import { makeMatch, checkInvariants, give, giveItem, legalTileFor, DATA } from './harness.js';
|
import { makeMatch, checkInvariants, give, giveItem, legalTileFor, DATA } from './harness.js';
|
||||||
|
|
||||||
const soloBot = (o = {}) => makeMatch({ mode: 'solo', difficulty: 'NORMAL', seats: [{ seat: 0, playerId: 'ai_0', name: 'AI', isBot: true, connected: true }], ...o });
|
const soloBot = (o = {}) => makeMatch({ mode: 'solo', difficulty: 'NORMAL', seats: [{ seat: 0, playerId: 'ai_0', name: 'AI', isBot: true, connected: true }], ...o });
|
||||||
|
|
||||||
|
test('#514: bots respect a human opening faction before deployment and release it after elimination', () => {
|
||||||
|
const { m } = makeMatch({ humans: 2, difficulty: 'HARD', seed: 11 }).start().toPrep();
|
||||||
|
const [human, bot] = m.order;
|
||||||
|
try {
|
||||||
|
bot.isBot = true;
|
||||||
|
human.bandId = 'band_chiave';
|
||||||
|
give(m, bot, 'chess_char_1_01_a', 'hand'); // Siracusa, despite no human board yet
|
||||||
|
give(m, bot, 'chess_char_1_19_a', 'hand'); // Kazimierz alternative
|
||||||
|
assert.notEqual(bondPlan(m, bot).focus, 'siracusaShip');
|
||||||
|
assert.notEqual(bondPlan(m, bot).second, 'siracusaShip');
|
||||||
|
human.bandId = 'band_pepe';
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
assert.ok(!m.gd.bandBondIds(botPickBand(m, bot)).includes('sargonShip'), 'avoid both Sargon strategies');
|
||||||
|
}
|
||||||
|
human.bandId = 'band_chiave';
|
||||||
|
assert.notEqual(bondPlan(m, bot).focus, 'siracusaShip', 'same-round changes invalidate the plan');
|
||||||
|
human.alive = false;
|
||||||
|
assert.notEqual(bondPlan(m, bot).focus, 'siracusaShip', 'the bot still compares its own bond value after elimination');
|
||||||
|
} finally { m.dispose(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
test('#514: a bot leaves Lappland for a human Chiave even when it could complete its own merge', () => {
|
||||||
|
for (const reward of [false, true]) {
|
||||||
|
const { m } = makeMatch({ humans: 2, difficulty: 'HARD', seed: 11 }).start().toPrep();
|
||||||
|
const [human, bot] = m.order;
|
||||||
|
try {
|
||||||
|
human.bandId = 'band_chiave';
|
||||||
|
bot.isBot = true;
|
||||||
|
const lappland = 'chess_char_6_18_a', alternative = 'chess_char_6_17_a';
|
||||||
|
bot.shop.level = 6;
|
||||||
|
bot.funds = reward ? 0 : 5;
|
||||||
|
bot.shop.slots = reward ? [] : [lappland, alternative].map((id) => ({ kind: 'chess', id, basePrice: 5, sold: false }));
|
||||||
|
if (reward) bot.pushRewardOffer('merge', { tier: 6, ids: [lappland, alternative] });
|
||||||
|
assert.ok(buyScoreOf(m, bot, alternative) > buyScoreOf(m, bot, lappland), 'prefer the non-reserved high-tier card');
|
||||||
|
} finally { m.dispose(); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('user: Given a faction strategy teammate, When scoring shared high-tier chess, Then humans and bots receive the same preference even with full stock', () => {
|
test('user: Given a faction strategy teammate, When scoring shared high-tier chess, Then humans and bots receive the same preference even with full stock', () => {
|
||||||
const { m } = makeMatch({ humans: 3, seed: 11 }).start().toPrep();
|
const { m } = makeMatch({ humans: 3, seed: 11 }).start().toPrep();
|
||||||
const [ps, mate, other] = m.order;
|
const [ps, mate, other] = m.order;
|
||||||
|
|||||||
Reference in new issue
Block a user