feedback1 WF: final A/B numbers; botbench records the best bounty card still on offer; META §1.5 for the review fixes

This commit is contained in:
sganggs committed 2026-10-03 03:50:52 +08:00
1 parent b9545f9c2c
commit e065cda0a4
3 files changed
+52 -29

No files matched your search

+22 -16
View File
@@ -335,26 +335,32 @@ previous pass (old generator), kept for history. \* the bot code of this pass (
**Player feedback after 0.1.0 (#10 "人机有点太笨了").** Measured first (`tools/botbench.mjs`): the 0.1.0 bots refreshed **Player feedback after 0.1.0 (#10 "人机有点太笨了").** Measured first (`tools/botbench.mjs`): the 0.1.0 bots refreshed
0–2 times a prep and bought side-grade singles they sold again at a loss (every sale returns 1), so they merged ≈ 2 0–2 times a prep and bought side-grade singles they sold again at a loss (every sale returns 1), so they merged ≈ 2
times a match and fielded ≈ 2 elites at R13; at 绝境 ≈ 1 in 2 of their bounty picks leaked (the old score ignored the times a match and fielded ≈ 2 elites at R13; in co-op 绝境 ≈ 1 in 2 of their bounty picks leaked (solo 10 of 80; the
enemy); 信标 went on the best operator (it destroys its carrier) and a level-up could spend the funds a third copy in old score ignored the enemy); 信标 went on the best operator (it destroys its carrier) and a level-up could spend the
the shop needed. Changes (server/match/bot.js header, META §1.5): refresh-vs-buy by the shop odds of completing held funds a third copy in the shop needed. Changes (server/match/bot.js header, META §1.5): refresh-vs-buy by the shop odds
pairs, the freeze for an unaffordable third copy, merges before level-ups, a committed focus / second bond, armour-aware of completing held pairs, the freeze for an unaffordable third copy, merges before level-ups, a committed focus /
DPS, bounty picks from the exposure model, item carriers by effect, a tactician's 援军 inside its range. Same seeds second bond (a teammate's main bond read from its bond strip), armour-aware DPS, bounty picks by expected value from
(1–40) old → new, the match's default rehearsal (3): the exposure model, item carriers by effect, 坎诺特 banking its interest capital, a tactician's 援军 inside its range.
Same seeds (1–40) old → new, the match's default rehearsal (3):
| config | wins | rounds passed | LP left | leaks / match | bounty enemies leaked | merges / bot | elites at R13 | | config | wins | rounds passed | LP left | leaks / match | bounty enemies leaked | merges / bot | elites at R13 |
|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|
| 独立 标准 (1 AI) | 35 → 35 / 40 | 8.80 → 8.80 | 29.9 → 28.8 | 2.5 → 2.9 | 1 → 1 | 1.40 → 2.23 | – (R7: 0.7 → 1.0) | | 独立 标准 (1 AI) | 35 → 35 / 40 | 8.80 → 8.80 | 29.9 → 28.6 | 2.5 → 3.3 | 1 → 1 | 1.40 → 2.25 | – (R7: 0.7 → 1.0) |
| 独立 绝境 (1 AI) | 19 → 28 / 40 | 13.05 → 13.47 | 14.3 → 16.4 | 26.5 → 19.6 | 10 → 5 of 80 picks | 2.33 → 4.17 | 1.8 → 2.2 | | 独立 绝境 (1 AI) | 19 → 26 / 40 | 13.05 → 13.35 | 14.3 → 16.6 | 26.5 → 19.7 | 10 → 8 of 80 picks | 2.33 → 4.10 | 1.8 → 2.2 |
| 同盟 标准 (4 AI) | 40 → 40 / 40 | 14.00 → 14.00 | 110.0 → 113.5 | 39.4 → 32.8 | 6 → 5 | 2.59 → 4.17 | 1.9 → 2.3 | | 同盟 标准 (4 AI) | 40 → 40 / 40 | 14.00 → 14.00 | 110.0 → 112.2 | 39.4 → 33.1 | 6 → 6 | 2.59 → 4.10 | 1.9 → 2.3 |
| 同盟 绝境 (4 AI) | 5 → 11 / 40 | 11.85 → 12.22 | 5.1 → 10.9 | 224.6 → 215.2 | 155 → 142 of ≈ 310 picks | 2.00 → 3.05 | 2.3 → 2.4 | | 同盟 绝境 (4 AI) | 5 → 12 / 40 | 11.85 → 12.32 | 5.1 → 12.7 | 224.6 → 212.6 | 155 → 148 of ≈ 310 picks | 2.00 → 2.97 | 2.3 → 2.8 |
绝境 leaks per alive bot in R12 / R13: solo 8.0 / 9.2 → 5.7 / 4.9, co-op 20.5 / 20.7 → 17.6 / 17.8. Decision time per 绝境 leaks per alive bot in R12 / R13: solo 8.0 / 9.2 → 5.7 / 4.8, co-op 20.5 / 20.7 → 18.5 / 15.8. Co-op bounty leaks
bot prep is unchanged (one thread, back to back, seeds 1–6 — solo 绝境 / co-op 绝境; the rehearsal included): wall clock are bound by the drafts, not the pick: in 223 of the 312 co-op 绝境 bounty picks no card still on offer had a kill
p50 / p95 87 / 175 → 82 / 191 ms and 84 / 172 → 80 / 157 ms, CPU 125 / 315 → 120 / 305 ms and 92 / 198 → 90 / 180 ms; chance ≥ 0.5 (solo 22 of 80), and only 5 picks (solo 0) took a card below 0.5 while one ≥ 0.5 was on offer — re-measure
the heuristics alone (CPU) 30 / 74 → 26 / 64 ms and 17 / 41 → 16 / 35 ms (the lineup search tries identical pieces after the bounty-half fix (player report #2). Strategies played alone (solo 绝境, seeds 201–220, forced with `--band`):
once and reuses the lineup across refreshes); a 机变 pick 0.07 → 0.14 ms (p50). The tuning sweeps used seeds 101–148 坎诺特 9 → 14 wins of 20, 昆图斯 9 → 11, 杜宾 8 → 9 (keeping an unused 教鞭 vs dropping it: 11 vs 11 wins over 24 杜宾
with the rehearsal off; the table above is a separate seed range. matches — kept, as it costs nothing but a hand slot). Decision time per bot prep is unchanged (one thread, back to back
on a quiet host, seeds 1–6, solo 绝境 / co-op 绝境; the rehearsal included): wall clock p50 / p95 94 / 191 → 89 / 205 ms
and 86 / 175 → 83 / 162 ms, CPU 131 / 314 → 124 / 329 ms and 97 / 209 → 91 / 188 ms; the heuristics alone (CPU) 31 / 74
→ 28 / 70 ms and 17 / 39 → 17 / 38 ms (the lineup search tries identical pieces once and reuses the lineup across
refreshes); a 机变 pick 0.07 → 0.15 ms (p50). Over the 40-seed A/B the summed prep CPU per match moved −6 % … +2 %. The
tuning sweeps used seeds 101–148 with the rehearsal off; the tables above are separate seed ranges.
--- ---
+18 -11
View File
@@ -172,9 +172,11 @@ The rest of this section is the legacy server-run mode
### 1.5 AI player (bot.js) ### 1.5 AI player (bot.js)
Buys toward a full board first (the cap is 8 from R1; leftover funds are lost), completes the merges it can afford, Buys toward a full board first (the cap is 8 from R1; leftover funds are lost), completes the merges it can afford,
then levels on a curve (L2 ≈ R3, L3 ≈ R5, L4 ≈ R7, L5 ≈ R10, L6 ≈ R12 — the competent curve of docs/BALANCE.md; free then levels on a curve (L2 ≈ R3, L3 ≈ R5, L4 ≈ R7, L5 ≈ R10, L6 ≈ R12 — the competent curve of docs/BALANCE.md; free
levels always), then spends the rest. It builds around a **focus core bond** (owned members × 10 + members the shop levels always), then spends the rest (a band that keeps its leftover funds — 坎诺特 利滚利 — holds its interest capital
can still bring at its level + banked layers + 6 for last round's focus, − 6 when a teammate already builds it: the of 5 back from refreshes, full-board buys that complete nothing, items and spare level-ups). It builds around a
pool is shared) and a **second bond** (most owned members, next threshold within reach); its keepers are the deployed **focus core bond** (owned members × 10 + members the shop can still bring at its level + banked layers + 6 for last
round's focus, − 6 when it is a teammate's main core bond on that teammate's bond strip — humans included: the pool is
shared) and a **second bond** (most owned members, next threshold within reach); its keepers are the deployed
operators, the focus / second members and chess of tier ≥ shop level − 1. With a full board a purchase must be merge operators, the focus / second members and chess of tier ≥ shop level − 1. With a full board a purchase must be merge
progress or a lineup upgrade **and** be worth more than the refreshes its price would pay for: a refresh is worth progress or a lineup upgrade **and** be worth more than the refreshes its price would pay for: a refresh is worth
Σ over the held pairs of P(the refresh shows the third copy) × the value of an elite (the shop's copy-weighted odds Σ over the held pairs of P(the refresh shows the third copy) × the value of an elite (the shop's copy-weighted odds
@@ -186,18 +188,23 @@ share of a dealer's damage that gets through the round's DEF / RES — `mitigate
the wave). The deployed set maximizes unit value + activated bond tiers (exact counting via `computeBonds`; every the wave). The deployed set maximizes unit value + activated bond tiers (exact counting via `computeBonds`; every
deployed focus member counts toward the next threshold). Items by what they do: equipment on the strongest deployed deployed focus member counts toward the next threshold). Items by what they do: equipment on the strongest deployed
damage dealers (survival items on blockers first, bond signature items on a member), 信标 on a bench single (never the damage dealers (survival items on blockers first, bond signature items on a member), 信标 on a bench single (never the
lineup when a bench single exists), 拟态物质 on a pair, 博士投影 on the strongest normal operator, 突变细胞 on the weakest lineup when a bench single exists), 拟态物质 on a pair, 博士投影 (both qualities — neither takes an elite) on the strongest
normal one below VI, 身份牌 / 通讯机 / 寻呼模块 on a focus member; 画卷 copies the most valuable deployed operator; 教鞭 / normal operator, 突变细胞 on the least valuable normal operator below 6阶 (deployed or benched — never an elite, never one
“神秘顾客” are used only after a perfect battle (otherwise dropped / destroyed for funds). 机变: a bounty card is scored of a merge pair, never one already carrying a cell), 身份牌 / 通讯机 / 寻呼模块 on a focus member; 画卷 copies the most
by its expected payout minus its expected leaks × the value of an LP (2 + 20 / LP), the kill chance from the exposure valuable deployed operator; 教鞭 / “神秘顾客” are used after a perfect battle and otherwise kept in the hand — a bot drops
model below for that one enemy against the own board as it stands (its HP × the round's multiplier, DEF / RES, speed, a kept one only when it needs the hand slot (“神秘顾客” then pays its fund), a human's seat under AI 托管 never. 机变: a
route kind) — the bot never takes a card its board cannot beat while a beatable one is offered; tactic cards by what bounty card is scored by its expected payout minus its expected leaks × the value of an LP (2 + 20 / LP), the kill
chance from the exposure model below for that one enemy against the own board as it stands (its HP × the round's
multiplier, DEF / RES, speed, route kind) — an expected-value comparison: it prefers the card with the best payout
minus expected LP loss and takes one it is unlikely to beat only when nothing better is offered or the pay difference
is large (in co-op 绝境 most remaining bounty leaks come from drafts with no beatable card at all); tactic cards by what
they act on (a 盟誓 / 驰援 card on its bonds, 升华, …). Placement uses the round's enemy preview: every route is traced they act on (a 盟誓 / 驰援 card on its bonds, 升华, …). Placement uses the round's enemy preview: every route is traced
over the own board (ground: the stage's device-aware ground paths; flyers: through their checkpoints) and weighted by over the own board (ground: the stage's device-aware ground paths; flyers: through their checkpoints) and weighted by
its enemies; an exposure model (time on each tile × DPS against the round's armour of the units covering it, blocker its enemies; an exposure model (time on each tile × DPS against the round's armour of the units covering it, blocker
hold time, anti-air only on flying routes) is maximized greedily (blockers, then damage dealers, then healers); a hold time, anti-air only on flying routes) is maximized greedily (blockers, then damage dealers, then healers); a
tactician's 援军 goes on a tile of the tactician's attack range, and a tile the server's `g.move` refuses is skipped for tactician's 援军 goes on a tile of the tactician's attack range (`PlayerState.summonRange` when the server has it, else
the next best one (legality is never duplicated: the planner reads `board.js legalTiles` on the deploy map). Boss the owner's range grid), and a tile the server's `g.move` refuses is skipped for the next best one (the planner reads
`board.js legalTiles` on the deploy map; `g.move` is the judge). Boss
rounds: the player's boss-field template (`Match.bossWaves`) is mapped onto the own board (rows −7, the right player rounds: the player's boss-field template (`Match.bossWaves`) is mapped onto the own board (rows −7, the right player
of a pair mirrored, only the routes that end on its half) and the leader counts as 10 tough enemies with a 30 s dwell of a pair mirrored, only the routes that end on its half) and the leader counts as 10 tough enemies with a 30 s dwell
on its first tiles, so the damage dealers reach stationary leaders. on its first tiles, so the damage dealers reach stationary leaders.
+12 -2
View File
@@ -19,7 +19,8 @@
// bot and round: LP, shop level, deployed units, active bonds and Σ bond tiers, activated layers, board value (Σ shop // bot and round: LP, shop level, deployed units, active bonds and Σ bond tiers, activated layers, board value (Σ shop
// price of the deployed chess, an elite = goldenCopies copies), elites deployed, items equipped, funds lost at the prep // price of the deployed chess, an elite = goldenCopies copies), elites deployed, items equipped, funds lost at the prep
// end (leftover funds are lost, PlayerState.endPrep), leaks (bounty leaks apart) and the bounty cards taken — with the // end (leftover funds are lost, PlayerState.endPrep), leaks (bounty leaks apart) and the bounty cards taken — with the
// bot's own kill-chance estimate when the bot has one (bot.bountyKillChance), for a calibration table. // bot's own kill-chance estimate when the bot has one (bot.bountyKillChance) for a calibration table, and the best
// estimate among the draft's cards (was a likelier card on offer?).
// Decision time: every bot prep runs in one scheduler callback in virtual time (Match.scheduleBotPrep); the tool times // Decision time: every bot prep runs in one scheduler callback in virtual time (Match.scheduleBotPrep); the tool times
// those callbacks (Match.later on the instance; wall clock and process CPU time) and splits off the rehearsal battles' // those callbacks (Match.later on the instance; wall clock and process CPU time) and splits off the rehearsal battles'
// stepping (Battle.step of the rehearsal fields 'r:<pid>') — "heuristics" = the prep minus the rehearsal. A 机变 pick is // stepping (Battle.step of the rehearsal fields 'r:<pid>') — "heuristics" = the prep minus the rehearsal. A 机变 pick is
@@ -124,8 +125,10 @@ function runOne(cfg, seed) {
// the bot's own kill-chance estimate (when this bot version has one), for the calibration table // the bot's own kill-chance estimate (when this bot version has one), for the calibration table
const c0 = performance.now(); const c0 = performance.now();
const p = typeof bot.bountyKillChance === 'function' ? bot.bountyKillChance(m, ps, card) : null; const p = typeof bot.bountyKillChance === 'function' ? bot.bountyKillChance(m, ps, card) : null;
// the best estimate among the draft's bounty cards still untaken (was a likelier one on offer?)
const best = p == null ? null : Math.max(p, ...m.sp.cards.filter((c) => c && c.kind === 'bounty' && m.sp.taken[c.idx] == null).map((c) => bot.bountyKillChance(m, ps, c)));
calMs += performance.now() - c0; calMs += performance.now() - c0;
rec.bounties.push({ round: m.round, pid: ps.playerId, enemyKey: card.enemyKey, count: card.count, tier: card.tier, payout: card.payout, battles: card.rounds, p }); rec.bounties.push({ round: m.round, pid: ps.playerId, enemyKey: card.enemyKey, count: card.count, tier: card.tier, payout: card.payout, battles: card.rounds, p, best });
} }
return apply(ps, idx); return apply(ps, idx);
}; };
@@ -299,6 +302,13 @@ function compare(runs, names) {
console.log('\n| run | bounty kill-chance estimate (bot.bountyKillChance) → share of bounty enemies actually killed, picks per bin |'); console.log('\n| run | bounty kill-chance estimate (bot.bountyKillChance) → share of bounty enemies actually killed, picks per bin |');
console.log('|---|---|'); console.log('|---|---|');
runs.forEach((rs, i) => { if (calib[i].some((b) => b.n)) console.log(`| ${names[i]} | ${calib[i].filter((b) => b.n).map((b) => `p ${b.lo}–${Math.min(1, b.hi)}: ${(100 * (1 - b.leaked / Math.max(1, b.units))).toFixed(0)} % (${b.n})`).join(' · ')} |`); }); runs.forEach((rs, i) => { if (calib[i].some((b) => b.n)) console.log(`| ${names[i]} | ${calib[i].filter((b) => b.n).map((b) => `p ${b.lo}–${Math.min(1, b.hi)}: ${(100 * (1 - b.leaked / Math.max(1, b.units))).toFixed(0)} % (${b.n})`).join(' · ')} |`); });
// the picks against what the draft offered (records with `best`)
console.log('\n| config | run | bounty picks | p < 0.5 picked while a p ≥ 0.5 card was offered | drafts with no card p ≥ 0.5 |');
console.log('|---|---|---|---|---|');
for (const c of cfgs) runs.forEach((rs, i) => {
const b = rs.filter((r) => r.config === c).flatMap((r) => r.bounties).filter((x) => x.best != null);
if (b.length) console.log(`| ${c} | ${names[i]} | ${b.length} | ${b.filter((x) => x.p < 0.5 && x.best >= 0.5).length} | ${b.filter((x) => x.best < 0.5).length} |`);
});
} }
console.log('\n| config | run | bot preps | prep CPU ms p50 / p95 | heuristics CPU ms p50 / p95 | Σ prep CPU s | Σ heuristics CPU s | per match: prep CPU s |'); console.log('\n| config | run | bot preps | prep CPU ms p50 / p95 | heuristics CPU ms p50 / p95 | Σ prep CPU s | Σ heuristics CPU s | per match: prep CPU s |');
console.log('|---|---|---|---|---|---|---|---|'); console.log('|---|---|---|---|---|---|---|---|');