diff --git a/docs/BALANCE.md b/docs/BALANCE.md index bf8feb70..2fa1d35b 100644 --- a/docs/BALANCE.md +++ b/docs/BALANCE.md @@ -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. --- diff --git a/docs/META.md b/docs/META.md index d73ee975..3e843e8a 100644 --- a/docs/META.md +++ b/docs/META.md @@ -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. diff --git a/tools/botbench.mjs b/tools/botbench.mjs index 9cef7525..a39fba65 100644 --- a/tools/botbench.mjs +++ b/tools/botbench.mjs @@ -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:') — "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('|---|---|---|---|---|---|---|---|');