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
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
enemy); 信标 went on the best operator (it destroys its carrier) and a level-up could spend the funds a third copy in
the shop needed. Changes (server/match/bot.js header, META §1.5): refresh-vs-buy by the shop odds of completing held
pairs, the freeze for an unaffordable third copy, merges before level-ups, a committed focus / second bond, armour-aware
DPS, bounty picks from the exposure model, item carriers by effect, a tactician's 援军 inside its range. Same seeds
(1–40) old → new, the match's default rehearsal (3):
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
old score ignored the enemy); 信标 went on the best operator (it destroys its carrier) and a level-up could spend the
funds a third copy in the shop needed. Changes (server/match/bot.js header, META §1.5): refresh-vs-buy by the shop odds
of completing held pairs, the freeze for an unaffordable third copy, merges before level-ups, a committed focus /
second bond (a teammate's main bond read from its bond strip), armour-aware DPS, bounty picks by expected value from
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 |
|---|---|---|---|---|---|---|---|
| 独立 标准 (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) | 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 |
| 同盟 标准 (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) | 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 |
| 独立 标准 (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 → 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 → 112.2 | 39.4 → 33.1 | 6 → 6 | 2.59 → 4.10 | 1.9 → 2.3 |
| 同盟 绝境 (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
bot prep is unchanged (one thread, back to back, seeds 1–6 — solo 绝境 / co-op 绝境; the rehearsal included): wall clock
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;
the heuristics alone (CPU) 30 / 74 → 26 / 64 ms and 17 / 41 → 16 / 35 ms (the lineup search tries identical pieces
once and reuses the lineup across refreshes); a 机变 pick 0.07 → 0.14 ms (p50). The tuning sweeps used seeds 101–148
with the rehearsal off; the table above is a separate seed range.
绝境 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
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
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
after the bounty-half fix (player report #2). Strategies played alone (solo 绝境, seeds 201–220, forced with `--band`):
坎诺特 9 → 14 wins of 20, 昆图斯 9 → 11, 杜宾 8 → 9 (keeping an unused 教鞭 vs dropping it: 11 vs 11 wins over 24 杜宾
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)
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
levels always), then spends the rest. It builds around a **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 a teammate already builds it: the
pool is shared) and a **second bond** (most owned members, next threshold within reach); its keepers are the deployed
levels always), then spends the rest (a band that keeps its leftover funds — 坎诺特 利滚利 — holds its interest capital
of 5 back from refreshes, full-board buys that complete nothing, items and spare level-ups). It builds around a
**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
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
@@ -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
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
lineup when a bench single exists), 拟态物质 on a pair, 博士投影 on the strongest normal operator, 突变细胞 on the weakest
normal one below VI, 身份牌 / 通讯机 / 寻呼模块 on a focus member; 画卷 copies the most valuable deployed operator; 教鞭 /
“神秘顾客” are used only after a perfect battle (otherwise dropped / destroyed for funds). 机变: a 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) — the bot never takes a card its board cannot beat while a beatable one is offered; tactic cards by what
lineup when a bench single exists), 拟态物质 on a pair, 博士投影 (both qualities — neither takes an elite) on the strongest
normal operator, 突变细胞 on the least valuable normal operator below 6阶 (deployed or benched — never an elite, never one
of a merge pair, never one already carrying a cell), 身份牌 / 通讯机 / 寻呼模块 on a focus member; 画卷 copies the most
valuable deployed operator; 教鞭 / “神秘顾客” are used after a perfect battle and otherwise kept in the hand — a bot drops
a kept one only when it needs the hand slot (“神秘顾客” then pays its fund), a human's seat under AI 托管 never. 机变: a
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
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
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
the next best one (legality is never duplicated: the planner reads `board.js legalTiles` on the deploy map). Boss
tactician's 援军 goes on a tile of the tactician's attack range (`PlayerState.summonRange` when the server has it, else
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
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.
+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
// 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
// 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
// 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
@@ -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
const c0 = performance.now();
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;
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);
};
@@ -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('|---|---|');
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('|---|---|---|---|---|---|---|---|');