Skip to content

Commit 1828ab8

Browse files
author
NBL Agent
committed
feat(v7): 60 canonical presets-v7, product gate, benchmark and evidence scripts
* content/presets-v7.json(+embed): 60 Generator v7 cards (12 rarities x5) with Naming V3 names reused verbatim and index-aligned with presets-v6; every card is produced by the final generator (no hand-authored BP or strength) and its BattlePower v4 value is frozen into presentation.power. * scripts/gate-v7-product.js + tests/v7-product-gate.test.js: deterministic CI regression covering geometry invariants, 100-vs-40, Lv40 XC vs Lv100 C, Lv70 XC/Lv70 XS vs Lv100 C suspense windows, same-level extreme rarity, iso-power/solver convergence, seed-structure invariance, multi-axis smoke, BattlePower content-only, legacy deterministic reproduction, Naming freeze and preset diversity. Runs 21 checks, all green. * scripts/benchmark-gen-v7.js, scripts/audit-v7-presets.js and scripts/diagnose-v7-mechanism-ranking.js add performance, preset-matrix and model-ranking diagnostics. * package.json: verify:release now runs verify + gate:v7-product + diversity; gate:v6-strength moves to diagnostics:legacy (kept, not deleted). * index.html/static-check.js: load and audit the V7 runtime modules while the product default stays on V6.
1 parent 16cb06d commit 1828ab8

15 files changed

Lines changed: 22264 additions & 6 deletions

.github/workflows/verify.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ jobs:
2020
with:
2121
node-version: 22
2222

23-
# v3 (Simple Outside, Deep Inside): 普通 PR 只跑快速 verify;
24-
# 正式 push 到 main 必须跑 verify:release(verify + diversity 3000 样本)。
23+
# V7 产品门禁:普通 PR 只跑快速 verify;正式 push 到 main 必须跑 verify:release(verify + gate:v7-product + diversity)。
2524
# 旧 v1.2.3 的 health-metrics / adjacent-rarity / BattlePower / matched-rarity /
2625
# Similar-BP 公平性 gate 已移出 release 阻断,仅保留在 npm run diagnostics:legacy。
2726
- name: Verify (PR fast gate)
@@ -31,3 +30,4 @@ jobs:
3130
- name: Verify Release (main release gate)
3231
if: github.event_name == 'push'
3332
run: npm run verify:release
33+

content/presets-v7.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

content/presets-v7.json

Lines changed: 21782 additions & 0 deletions
Large diffs are not rendered by default.

index.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,21 +85,31 @@ <h1>数值对战实验室</h1>
8585
<script src="src/gen-v5.js"></script>
8686
<script src="src/budget-v6.js"></script>
8787
<script src="src/strength-geometry-v7.js"></script>
88+
<script src="calibration/reference-world-v7.js"></script>
89+
<script src="calibration/strength-model-v7.js"></script>
90+
<script src="src/style-genome-v7.js"></script>
91+
<script src="src/strength-model-v7.js"></script>
92+
<script src="src/solver-v7.js"></script>
8893
<script src="src/budget-price.js"></script>
8994
<script src="src/gen-v6.js"></script>
95+
<script src="src/gen-v7.js"></script>
9096
<script src="src/behavior.js"></script>
9197
<script src="src/battlepower-model.js"></script>
9298
<script src="src/battlepower.js"></script>
9399
<script src="src/battlepower-v2.js"></script>
94100
<script src="src/battlepower-v3.js"></script>
101+
<script src="calibration/battlepower-v4.js"></script>
102+
<script src="src/battlepower-v4.js"></script>
95103
<script src="src/numerical-knowledge.js"></script>
96104
<script src="src/card-ui.js"></script>
97105
<script src="src/card-browser.js"></script>
98106
<script src="content/presets-v4.js"></script>
99107
<script src="content/presets-v5.js"></script>
100108
<script src="content/presets-v6.js"></script>
109+
<script src="content/presets-v7.js"></script>
101110
<script src="src/presets.js"></script>
102111
<script src="src/presets-v6.js"></script>
112+
<script src="src/presets-v7.js"></script>
103113
<script src="src/app.js"></script>
104114
</body>
105115
</html>

package.json

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,21 @@
66
"scripts": {
77
"test": "node --test tests/*.test.js",
88
"verify": "node scripts/generate-catalog.js && node --test tests/*.test.js && node scripts/static-check.js",
9-
"verify:release": "npm run verify && npm run gate:v6-strength && npm run diversity",
9+
"verify:release": "npm run verify && npm run gate:v7-product && npm run diversity",
10+
"gate:v7-product": "node scripts/gate-v7-product.js",
1011
"gate:v6-strength": "node scripts/gate-v6-strength.js",
12+
"reality:v7": "node scripts/build-v7-battle-graph.js 15 12 100 && node scripts/fit-v7-empirical-strength.js && node scripts/calibrate-strength-model-v7.js && node scripts/audit-v7-seed-dispersion.js && node scripts/audit-v7-product-strength.js 40 8 && node scripts/audit-stat-sensitivity-v7.js 7 8 8 3 && node scripts/audit-battlepower-v4.js",
13+
"build:v7-graph": "node scripts/build-v7-battle-graph.js 15 12 100",
14+
"fit:v7-empirical": "node scripts/fit-v7-empirical-strength.js",
15+
"calibrate:v7-strength": "node scripts/calibrate-strength-model-v7.js",
16+
"audit:v7-geometry": "node scripts/audit-v7-geometry.js",
17+
"audit:v7-product": "node scripts/audit-v7-product-strength.js 40 8",
18+
"audit:v7-seed-dispersion": "node scripts/audit-v7-seed-dispersion.js",
19+
"audit:v7-sensitivity": "node scripts/audit-stat-sensitivity-v7.js 7 8 8 3",
20+
"audit:v7-battlepower": "node scripts/audit-battlepower-v4.js",
21+
"audit:v7-presets": "node scripts/audit-v7-presets.js",
22+
"benchmark:v7": "node scripts/benchmark-gen-v7.js 1000",
23+
"migrate:presets-v7": "node scripts/migrate-presets-v7.js",
1124
"audit:v6-strength": "node scripts/audit-v6-strength.js",
1225
"audit:v6-budget": "node scripts/audit-v6-budget.js",
1326
"audit:v6-presets": "node scripts/audit-v6-presets.js",
@@ -22,7 +35,7 @@
2235
"audit:presets-v5": "node scripts/audit-presets-v5.js",
2336
"audit:empirical-strength": "node scripts/empirical-strength-audit.js 1200 3000",
2437
"gen-mc": "node scripts/gen-mc.js",
25-
"diagnostics:legacy": "node scripts/health-metrics.js --n 3000 && node scripts/adjacent-rarity-matrix.js --seeds 48 --battles 8 && node scripts/matched-rarity-test.js --seeds 48 --battles 8 && node scripts/power-calibration.js --sample 600 --battles 14 --rarityN 48 && node scripts/similar-bp-test.js --battles 14 --pairs 120",
38+
"diagnostics:legacy": "node scripts/gate-v6-strength.js && node scripts/health-metrics.js --n 3000 && node scripts/adjacent-rarity-matrix.js --seeds 48 --battles 8 && node scripts/matched-rarity-test.js --seeds 48 --battles 8 && node scripts/power-calibration.js --sample 600 --battles 14 --rarityN 48 && node scripts/similar-bp-test.js --battles 14 --pairs 120",
2639
"diversity": "node scripts/diversity-audit.js 3000",
2740
"diversity:v4": "node scripts/diversity-audit-v4.js 10000",
2841
"calibration:v4": "node scripts/power-v4-calibration.js 72 1600",

qa/v7-performance.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"schemaVersion": 1,
3+
"generatorVersion": 7,
4+
"methodology": {
5+
"cards": 1000,
6+
"rarities": 12,
7+
"levels": 6,
8+
"runtime": "solver+model only; no battles, AI, opponent, Monte Carlo or BattlePower",
9+
"os": "win32",
10+
"node": "v24.15.0"
11+
},
12+
"totalMs": 15281.13,
13+
"meanMs": 15.281,
14+
"medianMs": 11.498,
15+
"p95Ms": 42.556,
16+
"p99Ms": 45.305,
17+
"minMs": 0.49,
18+
"maxMs": 50.053,
19+
"gates": {
20+
"medianUnder10ms": false,
21+
"p95Under25ms": false
22+
}
23+
}

scripts/audit-v7-presets.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
'use strict';
2+
// V7 preset reality smoke: real canonical-AI battles across the 60 presets.
3+
// * 12-rarity representative matrix (one card per rarity tier, mirrored pairs,
4+
// multiple fixed Match Seeds) -> the Level x Rarity hierarchy must hold.
5+
// * 3v3 smoke (explicit 1-6 team selection, canonical AI, new Match Seed).
6+
// * Side bias: team A vs team B must stay neutral.
7+
// This is release evidence, NOT part of the CI regression.
8+
const fs=require('node:fs');
9+
const path=require('node:path');
10+
const ROOT=path.resolve(__dirname,'..');
11+
const {emptyBattleCounts,addBattleCounts,scoreMirroredPair,summarizeBattleCounts,wilson95}=require('../src/strength-audit-v6.js');
12+
for(const file of ['kernel','components','rules','content','status-runtime','formula','validator','effects','engine','ai','power','power-v5','battlepower-v3','gen-stats','gen-skills','generator','gen-names','name-generator-v2','name-generator-v3','gen-v2','gen-v3','gen-v4','gen-v5','budget-v6','budget-price','gen-v6','strength-geometry-v7','style-genome-v7','strength-model-v7','solver-v7','gen-v7','battlepower-v4','presets','presets-v6','presets-v7'])require(path.join(ROOT,'src',file+'.js'));
13+
const N=global.NCB;
14+
const cards=N.SYSTEM_PRESETS_V7;
15+
if(cards.length!==60)throw new Error('expected 60 v7 presets');
16+
const seedsPerPair=Number(process.argv[2]||2),roundCap=100;
17+
function fight(a,b,seed){
18+
N.deployCard(a);N.deployCard(b);
19+
const engine=N.createBattle({seed:N.deriveSeed(seed),teamA:[a.id],teamB:[b.id],maxRounds:roundCap});
20+
let guard=0;while(!engine.outcome().ended&&guard++<roundCap*2+40)engine.resolveRound([...N.planAI(engine,'A','canonical'),...N.planAI(engine,'B','canonical')]);
21+
const winner=engine.outcome().winner;return winner==='A'?1:winner==='B'?-1:0;
22+
}
23+
// --- 12-rarity representative matrix ---
24+
const byRarity=new Map();for(const card of cards){if(!byRarity.has(card.rarity))byRarity.set(card.rarity,card);}
25+
const reps=N.RARITY_V2_ORDER.map(rarity=>byRarity.get(rarity));
26+
const matrix=[];
27+
for(let i=0;i<reps.length;i++)for(let j=i+1;j<reps.length;j++){
28+
const low=reps[i],high=reps[j],counts=emptyBattleCounts();
29+
for(let k=0;k<seedsPerPair;k++)addBattleCounts(counts,scoreMirroredPair(low,high,700000+i*1000+j*17+k,fight));
30+
const summary=summarizeBattleCounts(counts);
31+
matrix.push({a:{id:low.id,rarity:low.rarity,level:low.level},b:{id:high.id,rarity:high.rarity,level:high.level},...summary});
32+
}
33+
// higher tier should win the majority of strictly-higher tier pairs (theta monotone)
34+
const decidedMatrix=matrix.filter(row=>row.higherWins!==row.lowerWins);
35+
const hierarchyUpsets=decidedMatrix.filter(row=>{const aTheta=N.targetThetaV7(row.a.level,row.a.rarity),bTheta=N.targetThetaV7(row.b.level,row.b.rarity);return Math.sign(aTheta-bTheta)!==Math.sign(row.higherWins-row.lowerWins);});
36+
// --- 3v3 smoke: two explicit 3-card teams, canonical AI, new Match Seed ---
37+
const teamCards=cards.slice(0,6); // 6 distinct preset cards
38+
function fight3v3(seed){
39+
const teamA=teamCards.slice(0,3),teamB=teamCards.slice(3,6);
40+
for(const card of teamA.concat(teamB))N.deployCard(card);
41+
const engine=N.createBattle({seed:N.deriveSeed(seed),teamA:teamA.map(c=>c.id),teamB:teamB.map(c=>c.id),maxRounds:roundCap});
42+
let guard=0;while(!engine.outcome().ended&&guard++<roundCap*2+40)engine.resolveRound([...N.planAI(engine,'A','canonical'),...N.planAI(engine,'B','canonical')]);
43+
const winner=engine.outcome().winner;
44+
return {winner,rounds:engine.roundIndex,ended:engine.outcome().ended};
45+
}
46+
const threeVsThree=[];let teamAWins=0,teamBWins=0,draws=0;
47+
for(let k=0;k<Math.max(4,seedsPerPair*2);k++){
48+
const r=fight3v3(830000+k);
49+
if(r.winner==='A')teamAWins++;else if(r.winner==='B')teamBWins++;else draws++;
50+
threeVsThree.push(r);
51+
}
52+
const sideBattles=teamAWins+teamBWins,ci=wilson95(teamAWins,sideBattles);
53+
const artifact={schemaVersion:1,generatorVersion:7,methodology:{presets:cards.length,representativeMatrix:{pairs:matrix.length,matchSeedsPerPair:seedsPerPair,mirroredSides:true,canonicalAI:true,roundCap},threeVsThree:{teams:'explicit slots, no autofill',cards:teamCards.map(c=>c.id),matchSeeds:threeVsThree.length}},
54+
matrix,matrixSummary:{pairs:matrix.length,strictlyHigherTierPairs:decidedMatrix.length,hierarchyUpsets:hierarchyUpsets.length,upholdRate:decidedMatrix.length?(decidedMatrix.length-hierarchyUpsets.length)/decidedMatrix.length:1,worstUpsets:hierarchyUpsets.slice(0,5)},
55+
threeVsThree:{samples:threeVsThree,teamAWins,teamBWins,draws,teamAWinRate:sideBattles?teamAWins/sideBattles:0,teamAWilson95:ci,sideBiasPass:ci.low<=.5&&ci.high>=.5,noInfinite:threeVsThree.every(r=>r.ended&&r.rounds<roundCap)}};
56+
const output=path.join(ROOT,'qa/v7-preset-matrix.json');
57+
fs.writeFileSync(output,JSON.stringify(artifact,null,2)+'\n');
58+
console.log(JSON.stringify({output:path.relative(ROOT,output),matrixPairs:matrix.length,hierarchyUpholdRate:artifact.matrixSummary.upholdRate,threeVsThree:{teamAWins,teamBWins,draws,teamAWinRate:artifact.threeVsThree.teamAWinRate,sideBiasPass:artifact.threeVsThree.sideBiasPass}},null,2));

scripts/benchmark-gen-v7.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use strict';
2+
// Generator v7 runtime performance benchmark.
3+
// Targets from the V7 spec: median < 10 ms and p95 < 25 ms for 1000 cards.
4+
const fs=require('node:fs');
5+
const path=require('node:path');
6+
const ROOT=path.resolve(__dirname,'..');
7+
for(const file of ['kernel','components','rules','content','status-runtime','formula','validator','effects','engine','ai','power','power-v5','battlepower-v3','gen-stats','gen-skills','generator','gen-names','name-generator-v2','name-generator-v3','gen-v2','gen-v3','gen-v4','gen-v5','budget-v6','budget-price','gen-v6','strength-geometry-v7','style-genome-v7','strength-model-v7','solver-v7','gen-v7'])require(path.join(ROOT,'src',file+'.js'));
8+
const N=global.NCB;
9+
const count=Number(process.argv[2]||1000);
10+
const rarities=N.RARITY_V2_ORDER;
11+
const levels=[20,40,50,70,75,100];
12+
const samples=[];
13+
for(let i=0;i<count;i++){
14+
const rarity=rarities[i%rarities.length],level=levels[i%levels.length];
15+
const started=process.hrtime.bigint();
16+
const card=N.generateCardV7({seed:'bench-v7-'+i,rarity,level});
17+
const elapsed=Number(process.hrtime.bigint()-started)/1e6;
18+
if(!card||card.generatorVersion!==7)throw new Error('bad generated card at '+i);
19+
samples.push(elapsed);
20+
}
21+
const sorted=samples.slice().sort((a,b)=>a-b);
22+
const at=p=>sorted[Math.min(sorted.length-1,Math.floor(p*sorted.length))];
23+
const sum=samples.reduce((a,b)=>a+b,0);
24+
const result={schemaVersion:1,generatorVersion:7,methodology:{cards:count,rarities:12,levels:6,runtime:'solver+model only; no battles, AI, opponent, Monte Carlo or BattlePower',os:process.platform,node:process.version},
25+
totalMs:Math.round(sum*100)/100,meanMs:Math.round((sum/samples.length)*1000)/1000,medianMs:Math.round(at(.5)*1000)/1000,p95Ms:Math.round(at(.95)*1000)/1000,p99Ms:Math.round(at(.99)*1000)/1000,minMs:Math.round(sorted[0]*1000)/1000,maxMs:Math.round(sorted.at(-1)*1000)/1000,
26+
gates:{medianUnder10ms:at(.5)<10,p95Under25ms:at(.95)<25}};
27+
const output=path.join(ROOT,'qa/v7-performance.json');
28+
fs.writeFileSync(output,JSON.stringify(result,null,2)+'\n');
29+
console.log(JSON.stringify({output:path.relative(ROOT,output),cards:count,meanMs:result.meanMs,medianMs:result.medianMs,p95Ms:result.p95Ms,gates:result.gates},null,2));
30+
if(!Object.values(result.gates).every(Boolean))process.exitCode=1;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use strict';
2+
// Decisive diagnostic: rank UNSOLVED base cards by the content model, then fight
3+
// them for real. If the model's mechanism valuation is directionally right, its
4+
// predicted ranking must correlate with the real battle ranking (in which case
5+
// the solver's compensation is the problem); if not, the model valuation is.
6+
const fs=require('node:fs'),path=require('node:path');
7+
const ROOT=__dirname;
8+
const {emptyBattleCounts,addBattleCounts,scoreMirroredPair}=require(path.join(ROOT,'src/strength-audit-v6.js'));
9+
for(const f of ['kernel','components','rules','content','status-runtime','formula','validator','effects','engine','ai','power','power-v5','battlepower-v3','gen-stats','gen-skills','generator','gen-names','name-generator-v2','name-generator-v3','gen-v2','gen-v3','gen-v4','gen-v5','budget-v6','budget-price','gen-v6','strength-geometry-v7','style-genome-v7','strength-model-v7','solver-v7','gen-v7'])require(path.join(ROOT,'src',f+'.js'));
10+
const N=global.NCB;
11+
const seeds=Number(process.argv[2]||8);
12+
function fight(a,b,seed){
13+
N.deployCard(a);N.deployCard(b);
14+
const e=N.createBattle({seed:N.deriveSeed(seed),teamA:[a.id],teamB:[b.id],maxRounds:100});
15+
let guard=0;while(!e.outcome().ended&&guard++<240)e.resolveRound([...N.planAI(e,'A','canonical'),...N.planAI(e,'B','canonical')]);
16+
const w=e.outcome().winner;return w==='A'?1:(w==='B'?-1:0);
17+
}
18+
const cards=[];
19+
for(let i=0;i<15;i++){
20+
const seed='base-'+i,genome=N.styleGenomeV7(seed),skel=N.mechanicSkeletonV7(seed);
21+
const card=N.buildUnsolvedCardV7(seed,'A',50,skel,genome);
22+
card.name='base'+i;card.displayName='base'+i;
23+
cards.push({card,predicted:N.predictThetaV7(card),victory:skel.victoryPath});
24+
}
25+
const theta=Object.fromEntries(cards.map(r=>[r.card.id,0])),games=Object.fromEntries(cards.map(r=>[r.card.id,0]));
26+
for(let i=0;i<cards.length;i++)for(let j=i+1;j<cards.length;j++){
27+
const counts=emptyBattleCounts();
28+
for(let k=0;k<seeds;k++)addBattleCounts(counts,scoreMirroredPair(cards[i].card,cards[j].card,440000+i*100+j*7+k,fight));
29+
const wins=counts.higherWins+counts.lowerWins+counts.draws;
30+
theta[cards[i].card.id]+=counts.higherWins+counts.draws/2;games[cards[i].card.id]+=wins;
31+
theta[cards[j].card.id]+=counts.lowerWins+counts.draws/2;games[cards[j].card.id]+=wins;
32+
}
33+
const rows=cards.map(r=>({victory:r.victory,predicted:r.predicted,actual:Math.log((theta[r.card.id]+.5)/(games[r.card.id]-theta[r.card.id]+.5))}));
34+
const mean=a=>a.reduce((x,y)=>x+y,0)/a.length;
35+
const corr=(a,b)=>{const ma=mean(a),mb=mean(b);let n=0,da=0,db=0;for(let i=0;i<a.length;i++){const x=a[i]-ma,y=b[i]-mb;n+=x*y;da+=x*x;db+=y*y;}return n/Math.sqrt(da*db);};
36+
const rank=v=>{const s=v.map((x,i)=>({x,i})).sort((a,b)=>a.x-b.x);const r=Array(v.length);s.forEach((e,i)=>r[e.i]=i);return r;};
37+
console.log('corr(predicted, actual) UNSOLVED:',corr(rows.map(r=>r.predicted),rows.map(r=>r.actual)).toFixed(3));
38+
console.log('spearman(predicted, actual) UNSOLVED:',corr(rank(rows.map(r=>r.predicted)),rank(rows.map(r=>r.actual))).toFixed(3));
39+
console.log('predicted spread:',(Math.max(...rows.map(r=>r.predicted))-Math.min(...rows.map(r=>r.predicted))).toFixed(2));
40+
console.log('actual spread:',(Math.max(...rows.map(r=>r.actual))-Math.min(...rows.map(r=>r.actual))).toFixed(2));
41+
for(const r of rows.slice().sort((a,b)=>a.actual-b.actual))console.log(r.victory.padEnd(9)+' predicted='+r.predicted.toFixed(2).padStart(7)+' actual='+r.actual.toFixed(2).padStart(7));

0 commit comments

Comments
 (0)