feedback1 gaps: bots skip strategies built around a bond the mode switches off
GameData.bandBondIds reads a band's tie from its own data: the bond names in <...> of its text (the official note naming the bond among them) and the bond ids / bond pools of its blackboards (choices.json pools[pool].bond for pool_char_later). botPickBand weighs a band tied to a bond in modeInactiveBonds 0, keeps one rng draw per pick and falls back to the default band when every offered band is excluded. In 标准 潘格尼尼, 克莱门莎 and 玛恩纳 are no longer picked (seeds 1-200: 5/200 solo and 23/800 co-op before, 0 after); the other modes keep the same picks.
This commit is contained in:
1 parent
842e1e13c4
commit
481b76027d
2 files changed
+67
-8
No files matched your search
+24
-8
@@ -59,6 +59,9 @@
|
||||
// 6. resolve the temp slots (a 突变细胞 left there — it comes back after every transformation — gets a hand slot made
|
||||
// for it, makeHandRoom, instead of being destroyed), keep one hand slot free (freeHandSlot: a kept bounty Art goes
|
||||
// before a chess on a bot's own seat, never on a human's seat under AI 托管), then Ready.
|
||||
// Strategy (botPickBand): weighted by starting LP among the offered bands; never one whose mechanic rides on a bond the
|
||||
// mode switches off (gd.bandBondIds: the bond its text names in <…> or its effect's blackboards name — 标准's 潘格尼尼,
|
||||
// 克莱门莎, 玛恩纳; DESIGN §21.26); alone, 老鲤's withheld first-round funds only rarely (× 0.02).
|
||||
// 机变 (botPickCard): a bounty by its expected payout minus the expected LP lost — bountyKillChance runs the exposure
|
||||
// model for that one enemy against the own board; a card the board is unlikely to beat wins only when nothing better
|
||||
// is offered or it pays much more —; tactic cards by what they act on (a 盟誓 / 驰援 card on the own bonds, 升华 …);
|
||||
@@ -118,19 +121,32 @@ const ECON_TRAIT_RE = /GOLD|REFRESH|COIN/;
|
||||
const DEFAULT_MELEE_RANGE = [[0, 0], [0, 1]];
|
||||
|
||||
/**
|
||||
* Band pick: weighted by starting LP (sturdier strategies are preferred). Alone, a band that withholds the first
|
||||
* rounds' funds (老鲤 "资金暂存": no operator in R1–R2, every enemy leaks) is avoided — only 联防 teammates cover that.
|
||||
* Band pick among the strategies the mode offers (gd.bandIds): weighted by starting LP (sturdier strategies are
|
||||
* preferred). Alone, a band that withholds the first rounds' funds (老鲤 "资金暂存": no operator in R1–R2, every enemy
|
||||
* leaks) is avoided — only 联防 teammates cover that. A band whose mechanic rides on a bond the mode switches off
|
||||
* (gd.bandBondIds ∩ gd.modeInactiveBonds — 标准: 潘格尼尼 <拉特兰>, 克莱门莎 <阿戈尔>, 玛恩纳 <卡西米尔>) weighs 0, never taken
|
||||
* (DESIGN §21.26); with every band excluded, the default band. One rng draw per call (deterministic per seed); modes
|
||||
* without inactive bonds keep exactly the earlier picks.
|
||||
*/
|
||||
export function botPickBand(m, ps) {
|
||||
const ids = m.gd.bandIds();
|
||||
if (!ids.length) return m.gd.defaultBandId;
|
||||
const lateFunds = (id) => /暂存/.test(String(m.gd.band(id)?.desc || ''));
|
||||
const pairs = ids.map((id) => [id, Math.max(1, (m.gd.startLp(id) - 18) ** 2) * (m.isSolo && lateFunds(id) ? 0.02 : 1)]);
|
||||
const gd = m.gd;
|
||||
const ids = gd.bandIds();
|
||||
if (!ids.length) return gd.defaultBandId;
|
||||
const lateFunds = (id) => /暂存/.test(String(gd.band(id)?.desc || ''));
|
||||
const offBond = (id) => gd.bandBondIds(id).some((b) => gd.modeInactiveBonds.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)]);
|
||||
let total = 0;
|
||||
for (const [, w] of pairs) total += w;
|
||||
let r = m.rngBots() * total;
|
||||
for (const [id, w] of pairs) { r -= w; if (r < 0) return id; }
|
||||
return pairs[pairs.length - 1][0];
|
||||
if (!(total > 0)) return gd.defaultBandId;
|
||||
let last = null;
|
||||
for (const [id, w] of pairs) {
|
||||
if (!(w > 0)) continue;
|
||||
last = id;
|
||||
r -= w;
|
||||
if (r < 0) return id;
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -90,6 +90,9 @@ export class GameData {
|
||||
for (const k of Object.keys(this.shopItemsByTier)) this.shopItemsByTier[k].sort();
|
||||
this.bondIds = Object.keys(this._bonds).sort((a, b) => (numOr(this._bonds[a].identifier, 99) - numOr(this._bonds[b].identifier, 99)) || (a < b ? -1 : 1));
|
||||
this.modeInactiveBonds = new Set(Array.isArray(this.mode.inactiveBondIds) ? this.mode.inactiveBondIds : []);
|
||||
/** bond name → id (bandBondIds) and its memo */
|
||||
this._bondByName = new Map(Object.entries(this._bonds).filter(([, r]) => r && typeof r.name === 'string').map(([id, r]) => [r.name, id]));
|
||||
this._bandBonds = new Map();
|
||||
this.inactiveEnemies = new Set(Array.isArray(this.mode.inactiveEnemyKeys) ? this.mode.inactiveEnemyKeys : []);
|
||||
/** data/tuning.json (titles only, see the header) */
|
||||
this.tuning = this.raw.tuning && typeof this.raw.tuning === 'object' ? this.raw.tuning : {};
|
||||
@@ -458,6 +461,46 @@ export class GameData {
|
||||
return b && Number.isInteger(b.totalHp) && b.totalHp > 0 ? b.totalHp : this.defaultStartLp;
|
||||
}
|
||||
|
||||
/**
|
||||
* The bonds a strategy's mechanic is built around, read from its own data (DESIGN §21.26): the bond names its text puts
|
||||
* in <…> — the official note "在<X>部分干员缺席时体验可能不完整" names that bond too — and the bond ids and bond pools its
|
||||
* effect's blackboards name (bb / bbStr values: 克莱门莎's `bond_id` egirShip, 玛恩纳's `bond` kazimierzShip, 潘格尼尼's
|
||||
* `pool` pool_char_later → choices.json pools[pool].bond lateranoShip). Names map to ids through bonds.json; a bracketed
|
||||
* name that is no bond (<寻呼模块>, <画卷>, <鸭爵> …) and a pool without a bond add nothing. Bond ids in data order,
|
||||
* [] for an unknown band or one tied to no bond (华法琳, 阿米娅 …). The bot never picks a strategy tied to a bond the mode
|
||||
* switches off (bot.js botPickBand: 标准's 潘格尼尼 / 克莱门莎 / 玛恩纳).
|
||||
* @param {string} bandId
|
||||
* @returns {string[]}
|
||||
*/
|
||||
bandBondIds(bandId) {
|
||||
if (this._bandBonds.has(bandId)) return this._bandBonds.get(bandId);
|
||||
const b = this.band(bandId);
|
||||
const found = new Set();
|
||||
if (b) {
|
||||
for (const [, name] of String(b.desc || '').matchAll(/<([^<>]+)>/g)) {
|
||||
const id = this._bondByName.get(name.trim());
|
||||
if (id) found.add(id);
|
||||
}
|
||||
const pools = this.choices.pools && typeof this.choices.pools === 'object' ? this.choices.pools : {};
|
||||
for (const buff of Array.isArray(b.buffs) ? b.buffs : []) {
|
||||
for (const board of [buff && buff.bb, buff && buff.bbStr]) {
|
||||
if (!board || typeof board !== 'object') continue;
|
||||
for (const v of Object.values(board)) {
|
||||
if (typeof v !== 'string') continue;
|
||||
for (const part of v.split(',').map((s) => s.trim())) {
|
||||
if (this.bond(part)) found.add(part);
|
||||
const pool = own(pools, part);
|
||||
if (pool && typeof pool.bond === 'string' && this.bond(pool.bond)) found.add(pool.bond);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const out = Object.freeze(this.bondIds.filter((id) => found.has(id)));
|
||||
this._bandBonds.set(bandId, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeable (hand) tokens a chess sends to the hand when placed on the board: [{ tokenId, count }] — its manually
|
||||
* deployable summons (tokens.json `placeable`: 医疗探机, 诅咒娃娃, 海嗣, 狼群, 流形, 爬行号·防护单元; user playtest #6)
|
||||
|
||||
Reference in new issue
Block a user