/** * hunt-balance-scan.mjs — 《围猎》bot 难度平衡扫描(开发工具,非断言测试)。 * * 目的(23-hunt.md §3.4 / §15 裁决 10):固定多种子仿真整局,校准 bot 强度口径—— * - 真人全程挂机(2 bot 独守) → 大概率失守,或胜利但耐久仅 1-3(C) * - 真人以 bot 水准游玩(3 bot 局)→ 余耐久 6-9(A/B)为主 * * 场景: * idle 真人不出手(挂机下限) * bot 真人槽直接复用 room.botDecide(与 bot 完全同一决策管线 = "bot 水准") * optimal 真人最优策略(内圈即打、≥3 放旋风斩;人类上限参考) * * 运行: * tools/node/bin/node web/server/test/hunt-balance-scan.mjs [种子数=24] * tools/node/bin/node web/server/test/hunt-balance-scan.mjs grid [种子数=16] # 参数网格扫描 */ import { EventEmitter } from 'node:events'; import hunt from '../hunt-room.js'; const { createManager, BOT_TUNING_DEFAULT } = hunt; class FakeConn extends EventEmitter { constructor() { super(); this.sent = []; } send(str) { this.sent.push(JSON.parse(str)); } close() { queueMicrotask(() => this.emit('close')); } } function runGame(seed, mode, botTuning) { const mgr = createManager({ rateLimit: Infinity, botTuning }); mgr.createRoom({ seed }); const conn = new FakeConn(); mgr.attach(conn); conn.emit('message', JSON.stringify({ t: 'join', ver: 1, room: 'quick', name: 'scan' })); conn.emit('message', JSON.stringify({ t: 'act', kind: 'start', seq: 1 })); const room = [...mgr.rooms.values()][0]; let guard = 0; while (room.phase !== 'end' && guard++ < 4000) { mgr.tick(100); const slot = room.slots[0]; if (!slot || room.phase !== 'wave') continue; if (mode === 'bot') { if (room.now % 500 === 0) room.botDecide(slot); // 与 bot 同决策频率、同规则 } else if (mode === 'optimal') { const mobs = [...room.mobs.values()]; if (room.now >= slot.skillCdUntil && mobs.filter((m) => m.eta <= 3).length >= 3) { room.adjudicateSkill(slot); } else if (room.now >= slot.cdUntil) { const inner = mobs.filter((m) => m.eta <= 4).sort((a, b) => a.eta - b.eta); if (inner.length) room.adjudicateAttack(slot, inner[0].dir); } } } const end = conn.sent.filter((m) => m.t === 'ev' && m.kind === 'end').pop(); room.destroy(); return end ? { win: end.win, dur: end.dur, grade: end.grade } : { win: null, dur: null, grade: '超时' }; } function sweep(mode, seeds, botTuning) { return seeds.map((s) => runGame(s, mode, botTuning)); } function line(results) { const wins = results.filter((r) => r.win === true); const durs = wins.map((r) => r.dur); const hist = {}; for (const d of durs) hist[d] = (hist[d] || 0) + 1; const mean = durs.length ? (durs.reduce((a, b) => a + b, 0) / durs.length).toFixed(1) : '—'; return `胜 ${wins.length}/${results.length} 耐久[${Object.keys(hist).sort((a, b) => b - a).map((d) => `${d}×${hist[d]}`).join(' ') || '—'}] mean=${mean}`; } function summarize(mode, results) { const wins = results.filter((r) => r.win === true); const durs = wins.map((r) => r.dur); const inBand = wins.filter((r) => r.dur >= 6 && r.dur <= 9).length; // 3bot 目标带 const badIdle = results.filter((r) => r.win === false || r.dur <= 3).length; // 挂机目标带 const grades = {}; for (const r of wins) grades[r.grade] = (grades[r.grade] || 0) + 1; console.log(`\n[${mode}] ${line(results)}`); console.log(` 评级: ${Object.entries(grades).sort().map(([g, n]) => `${g}×${n}`).join(' ') || '—'}` + ` | 目标命中: 挂机(负或≤3)=${badIdle}/${results.length},3bot(6-9)=${inBand}/${results.length}`); console.log(' 逐局:', results.map((r) => (r.win ? `${r.dur}` : '负')).join(' ')); } const arg0 = process.argv[2] || '24'; if (arg0 === 'grid') { // 网格扫描:找同时满足两口径的参数(小样本快筛,选出后再用默认模式 24 seeds 确认) const n = Number(process.argv[3]) || 16; const seeds = Array.from({ length: n }, (_, i) => i + 1); console.log(`网格扫描 ${n} seeds/点:`); console.log('参数(犹豫/反应带/技能想起) → idle 局 | 3bot 局'); for (const hesitateP of [0.5, 0.6, 0.7, 0.75, 0.8]) { for (const [reactBase, reactSpan] of [[2.2, 1.2], [1.8, 1.0], [1.5, 0.8]]) { for (const skillP of [0.6, 0.3]) { const tuning = { ...BOT_TUNING_DEFAULT, hesitateP, reactBase, reactSpan, skillP }; const idle = sweep('idle', seeds, tuning); const bot = sweep('bot', seeds, tuning); const idleHit = idle.filter((r) => r.win === false || r.dur <= 3).length; const botHit = bot.filter((r) => r.win && r.dur >= 6 && r.dur <= 9).length; console.log( `p=${hesitateP} band=${reactBase}+${reactSpan}r skillP=${skillP}` + ` | idle: ${line(idle)} 目标${idleHit}/${n}` + ` | 3bot: ${line(bot)} 目标${botHit}/${n}`, ); } } } } else { const n = Number(arg0) || 24; const seeds = Array.from({ length: n }, (_, i) => i + 1); console.log(`《围猎》bot 平衡扫描:${n} seeds × 3 场景,默认参数 ${JSON.stringify(BOT_TUNING_DEFAULT)}`); summarize('idle(真人挂机,2 bot 独守)', sweep('idle', seeds)); summarize('bot(3 bot 水准局)', sweep('bot', seeds)); summarize('optimal(人类上限参考)', sweep('optimal', seeds)); }