Skip to content

Commit 99d43fa

Browse files
author
NBL Agent
committed
fix(product): 修正伤害公式注释与实力表述,新增轻量 BattlePower 审计
- src/battle.js:基础伤害 ATK²/(ATK+DEF_eff) 注释改准确——同 scale 下随总倍率线性, 单独堆 ATK 边际收益略高于线性(受 DEF 约束,无病态二次增长) - index.html / README.md:实力体系区分为「输入(Level/Rarity/12 属性 决定战斗表现)」 与「评价(Battle Power 不参与战斗,只衡量综合实力)」,移除『战力 × 多属性共同决定』的误导表述 - 新增 scripts/battlepower-audit.js:Lv50 循环赛(每卡 ×23 对手 ×40 场)实证胜率 与 BP 排名做 Spearman 秩相关,ρ=0.9939(门槛 ≥0.85),挂入 npm run verify
1 parent 24ee172 commit 99d43fa

7 files changed

Lines changed: 78 additions & 8 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@
4040

4141
```bash
4242
npm test # node:test 全量测试(零依赖)
43-
npm run verify # 静态检查(文件集合=期望清单)+ 全量测试 + 产品实战验收
43+
npm run verify # 静态检查(文件集合=期望清单)+ 全量测试 + 产品实战验收 + BattlePower 审计
4444
npm run serve # 本地静态服务器 http://127.0.0.1:8774
4545
```
4646

4747
## 修改卡牌/数值后的检查清单
4848

4949
1. `npm test`(BP 排序、确定性、验收匹配全部要绿)
50-
2. `npm run verify`
50+
2. `npm run verify`(含 `scripts/battlepower-audit.js`:Lv50 循环赛胜率与 BP 排名 Spearman 相关 ≥ 0.85)
5151
3. 如果改了 `index.html` 的 script 标签或新增/删除文件,同步更新 `scripts/static-check.js` 的期望清单

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,17 @@
77
- 开始战斗。
88
- 看谁赢。
99

10-
卡牌实力由四件事共同体现:
10+
实力体系分两类:**输入**(决定战斗表现)与**评价**(事后衡量)。
11+
12+
输入 —— 决定战斗表现:
1113

1214
- **Level** — 第一实力维度。Lv100 与 Lv40 之间存在巨大差距(超指数成长)。
1315
- **Rarity** — 第二实力维度。C / C+ / B / B+ / A / A+ / S / SS / SSS / SSS Collector / XS / XS Collector,共 12 档,C→XS Collector 为 6 倍数值差。
16+
- **12 个战斗属性** — 生命 / 攻击 / 防御 / 速度 / 命中 / 闪避 / 暴击率 / 暴击伤害 / 穿透 / 吸血 / 每回合回复 / 波动,是实际战斗内容。
17+
18+
评价 —— 不参与战斗:
19+
1420
- **Battle Power** — 只读最终属性计算的综合数字,帮你快速判断两张卡谁更强。它不参与战斗、不修改伤害、不强制胜者;BP 越高长期平均越强,实力接近时互有胜负。
15-
- **12 个战斗属性** — 生命 / 攻击 / 防御 / 速度 / 命中 / 闪避 / 暴击率 / 暴击伤害 / 穿透 / 吸血 / 每回合回复 / 波动。
1621

1722
## 怎么玩
1823

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<body>
1010

1111
<h1>数值卡牌 · 自动 PK</h1>
12-
<div class="sub">选两张卡 → 调等级 → 开始战斗 → 看谁赢 · 实力由 等级 × 稀有度 × 战力 × 多属性 共同决定</div>
12+
<div class="sub">选两张卡 → 调等级 → 开始战斗 → 看谁赢 · 等级、稀有度与属性决定战斗表现;Battle Power 评价综合实力</div>
1313

1414
<div class="setup">
1515
<div class="panel" id="panelA">

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"type": "commonjs",
77
"scripts": {
88
"test": "node --test tests/*.test.js",
9-
"verify": "node scripts/static-check.js && node --test tests/*.test.js && node scripts/acceptance.js",
9+
"verify": "node scripts/static-check.js && node --test tests/*.test.js && node scripts/acceptance.js && node scripts/battlepower-audit.js",
1010
"serve": "node scripts/serve.js"
1111
}
1212
}

scripts/battlepower-audit.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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);

scripts/static-check.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const EXPECTED_FILES = [
4141
'tests/product-acceptance.test.js',
4242
'scripts/static-check.js',
4343
'scripts/acceptance.js',
44+
'scripts/battlepower-audit.js',
4445
'scripts/serve.js'
4546
].sort();
4647

src/battle.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
return clamp(0.90 + (attacker.acc - defender.eva) * 0.004, 0.45, 0.99);
3434
}
3535

36-
// 基础伤害:ATK 线性贡献 × 防御/穿透修正(无二次增长)
36+
// 基础伤害:ATK²/(ATK+DEF_eff)。
37+
// 当 ATK/DEF 随 Level×Rarity 同比例放大时,整体随总 scale 线性增长(等级体系成立);
38+
// 单独堆 ATK 时边际收益略高于线性(ATK 仍受 DEF 上限约束,无病态二次增长)。
3739
function baseDamage(attacker, defender) {
3840
const defEff = defender.def * Math.max(0.05, 1 - attacker.pen);
3941
return attacker.atk * attacker.atk / (attacker.atk + defEff);
@@ -53,7 +55,7 @@
5355
add(` ${foe.name} 闪避了 ${unit.name} 的攻击!`, 'miss');
5456
return;
5557
}
56-
// 伤害:ATK 线性 × 防御/穿透修正 × 随机波动 × 暴击
58+
// 伤害:ATK²/(ATK+DEF×修正) × 随机波动 × 暴击
5759
let dmg = baseDamage(unit, foe);
5860
dmg *= 1 + (rng() * 2 - 1) * unit.volatility; // 波动
5961
const isCrit = rng() < unit.crit; // 暴击

0 commit comments

Comments
 (0)