feedback1 gaps: bands.json bondIds is built once and read by the bot

The band -> bond derivation moves out of GameData into shared/bandBonds.js,
which tools/build-data.mjs runs per band to write bands.json bondIds
(regenerated with --offline; 9 bands are tied, the build checks the ids).
GameData.bandBondIds reads the field, so the server and the client share
one source. DATA.md documents the field; the test checks the helper on
synthetic bands and that the data matches it.
This commit is contained in:
sganggs committed 2026-10-03 15:56:33 +08:00
1 parent 3bd855982e
commit 1f29b8a3d0
7 files changed
+86 -46

No files matched your search

+2 -2
View File
@@ -60,8 +60,8 @@
// 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).
// mode switches off (gd.bandBondIds = bands.json bondIds: the bond its text names in <…> or its 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, 升华 …);
+9 -32
View File
@@ -90,8 +90,7 @@ 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]));
/** bandBondIds memo */
this._bandBonds = new Map();
this.inactiveEnemies = new Set(Array.isArray(this.mode.inactiveEnemyKeys) ? this.mode.inactiveEnemyKeys : []);
/** data/tuning.json (titles only, see the header) */
@@ -462,41 +461,19 @@ export class GameData {
}
/**
* 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 潘格尼尼 / 克莱门莎 / 玛恩纳).
* The bonds a strategy's mechanic is built around (DESIGN §21.26): bands.json `bondIds`, written at build time by
* shared/bandBonds.js from the band's own text and blackboards (潘格尼尼 → 拉特兰, 克莱门莎 → 阿戈尔, 玛恩纳 → 卡西米尔 …) —
* the field the strategy draft's 本局禁用 mark reads too. Known bond ids in data order; [] for an unknown band, one tied to
* no bond (华法琳, 阿米娅 …) or data without the field. The bot never picks a strategy tied to a bond the mode switches
* off (bot.js botPickBand).
* @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)));
const listed = this.band(bandId)?.bondIds;
const set = new Set(Array.isArray(listed) ? listed : []);
const out = Object.freeze(this.bondIds.filter((id) => set.has(id)));
this._bandBonds.set(bandId, out);
return out;
}