|
| 1 | +/* ========================================================= |
| 2 | + scripts/battlepower-audit.js — 轻量 Battle Power 审计 |
| 3 | + 每张卡在同等级(Lv50)下打遍其他 23 张卡,统计长期综合胜率, |
| 4 | + 与 Battle Power 排名做 Spearman 秩相关检查。 |
| 5 | + 不需要 ML / Solver / 大规模世界模型 —— 两万多场简单循环就够。 |
| 6 | + ========================================================= */ |
| 7 | +'use strict'; |
| 8 | +const { CARDS, RARITY_LIST } = require('../src/cards.js'); |
| 9 | +const { buildUnit, battlePower } = require('../src/power.js'); |
| 10 | +const { simulate } = require('../src/battle.js'); |
| 11 | + |
| 12 | +const LEVEL = 50; |
| 13 | +const SEEDS_PER_MATCH = 40; |
| 14 | + |
| 15 | +function winRate(cardA, cardB, seeds) { |
| 16 | + let pts = 0; |
| 17 | + for (let s = 0; s < seeds; s++) { |
| 18 | + const r = simulate(cardA, LEVEL, cardB, LEVEL, 330000 + s * 15485863 + cardA.rarity * 1000 + cardB.rarity); |
| 19 | + if (r.winner === 0) pts += 1; |
| 20 | + else if (r.winner === 1) pts += 0; |
| 21 | + else pts += 0.5; |
| 22 | + } |
| 23 | + return pts / seeds; |
| 24 | +} |
| 25 | + |
| 26 | +// 汇总每张卡的长期综合胜率(对 23 个对手取平均) |
| 27 | +const rows = CARDS.map((c) => { |
| 28 | + let total = 0; |
| 29 | + for (const o of CARDS) { |
| 30 | + if (o.id === c.id) continue; |
| 31 | + total += winRate(c, o, SEEDS_PER_MATCH); |
| 32 | + } |
| 33 | + return { card: c, bp: battlePower(buildUnit(c, LEVEL)), wr: total / (CARDS.length - 1) }; |
| 34 | +}); |
| 35 | + |
| 36 | +// 按胜率与按 BP 分别排名 → Spearman 秩相关 |
| 37 | +const byWr = [...rows].sort((a, b) => b.wr - a.wr); |
| 38 | +const byBp = [...rows].sort((a, b) => b.bp - a.bp); |
| 39 | +const rankWr = new Map(byWr.map((r, i) => [r.card.id, i + 1])); |
| 40 | +const rankBp = new Map(byBp.map((r, i) => [r.card.id, i + 1])); |
| 41 | + |
| 42 | +const n = rows.length; |
| 43 | +const d2 = rows.reduce((s, r) => s + (rankWr.get(r.card.id) - rankBp.get(r.card.id)) ** 2, 0); |
| 44 | +const rho = 1 - 6 * d2 / (n * (n * n - 1)); |
| 45 | + |
| 46 | +console.log(`========== Battle Power 审计(Lv${LEVEL} 循环赛 × ${SEEDS_PER_MATCH} 场/对)==========`); |
| 47 | +console.log('rank | card | rarity | BP | 胜率%'); |
| 48 | +byWr.forEach((r, i) => { |
| 49 | + const bpRank = rankBp.get(r.card.id); |
| 50 | + const mark = bpRank === i + 1 ? ' ' : `(BP#${bpRank})`; |
| 51 | + console.log( |
| 52 | + String(i + 1).padStart(4) + ' | ' + r.card.id.padEnd(23) + ' | ' + |
| 53 | + RARITY_LIST[r.card.rarity].padEnd(14) + ' | ' + String(r.bp).padStart(7) + |
| 54 | + ' | ' + (r.wr * 100).toFixed(1).padStart(5) + ' ' + mark |
| 55 | + ); |
| 56 | +}); |
| 57 | +console.log(`\nSpearman ρ(胜率排名 vs BP 排名)= ${rho.toFixed(4)}`); |
| 58 | + |
| 59 | +const THRESHOLD = 0.85; |
| 60 | +const ok = rho >= THRESHOLD; |
| 61 | +console.log(`检查:ρ ≥ ${THRESHOLD} → ${ok ? '通过' : '失败'}`); |
| 62 | +process.exit(ok ? 0 : 1); |
0 commit comments