184 lines
11 KiB
JavaScript
184 lines
11 KiB
JavaScript
// ★★★★ 2026-07-17 第三刀:【全表死物 census —— 「读 0 次 / 改变 0 格」逐个列出】
|
||
// 老板逐字:「把每个进 reasons 的数、每个 key 位、每个 axis,逐个 census:被读几次?改变几格?
|
||
// 读 0 次 或 改变 0 格 的,全部列出来。」
|
||
//
|
||
// ⚠ 口径三条,写死在这里:
|
||
// ① 【改变几格】= 【突变数据点 + 比对引擎自己的产物】,不是复算。
|
||
// ② ★【「改变 0 格」比「读 0 次」更宽,而它才是产品要问的那一问】——
|
||
// 南朝的 lex 被【读了 98,304 次、裁决了 81,920 次】,而它【改变 0 格】(第六刀实测)。
|
||
// ⇒ 一个被读了八万次的东西,可以什么都没买到。故本 census 量的是【买到了什么】。
|
||
// ③ ★★★【每一个读数都必须报它是在哪个 chushen 配置下量的】——
|
||
// 第六刀的验收判词逐字:「一个不说配置的读数,不是读数。」
|
||
// ⚠ 而这一条【本 census 第一版自己就栽了】:默认 chushen 未答 ⇒ 辽三条路全关
|
||
// ⇒ 辽的每一个数据点都报「死」。那不是死,那是【我没让它活】。
|
||
// ⇒ 故:辽跑 3 种(未答/契丹/汉),明跑 2 种(未答/倡優之家),其余五朝无出身维。
|
||
// ★【死】= 在【它自己那一朝的每一种配置下】都改变 0 格。
|
||
import { DYNASTIES, computeRank, computeAxes, AXIS_IDS } from '../src/dynasties.js';
|
||
import { chushenOf } from '../src/chushen.js';
|
||
import { SUB_REGISTRY } from '../src/talents.js';
|
||
|
||
const SUBS = SUB_REGISTRY.map((s) => s.id);
|
||
const WANGS = [0, 29, 30, 54, 55, 74, 75, 100]; // 与 census-kai.mjs 逐字同 —— 门槛 30/55/75 的两侧
|
||
// ★ 2^16 subs 去重成 2,790 个不同的六轴取值(带权 w)——【判决只看六轴,不看 subs】。
|
||
// ⇒ 与 524,288 逐格跑【逐字等价】,而快 20 倍。w 把「条次」还原回「profile 数」。
|
||
const TUPLES = (() => {
|
||
const m = new Map();
|
||
for (let b = 0; b < (1 << 16); b++) {
|
||
const subs = []; for (let i = 0; i < 16; i++) if (b & (1 << i)) subs.push(SUBS[i]);
|
||
const a = computeAxes(subs);
|
||
const k = AXIS_IDS.map((x) => a[x]).join(',');
|
||
m.set(k, (m.get(k) || 0) + 1);
|
||
}
|
||
return [...m].map(([k, w]) => ({ axes: Object.fromEntries(k.split(',').map((v, i) => [AXIS_IDS[i], Number(v)])), w }));
|
||
})();
|
||
const PER_DYN = TUPLES.reduce((s, t) => s + t.w, 0) * WANGS.length; // = 524,288
|
||
|
||
// 每朝要跑的 chushen 配置。★ null 也是一种配置(「这一问还没问出口」),不是缺省值。
|
||
const CONFIGS = {};
|
||
for (const d of DYNASTIES) {
|
||
const dim = chushenOf(d.id);
|
||
CONFIGS[d.id] = dim ? [null, ...dim.values.map((v) => v.id)] : [null];
|
||
}
|
||
|
||
// ★ 存 hash 不存串:156,240 格 × 3.2KB 会把内存吃穿(第一版就是这么死的)。
|
||
const fnv = (s) => { let h = 0x811c9dc5; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193); } return h >>> 0; };
|
||
// 【整页】用户读得到的全部(reasons 的每一个字都在里面)
|
||
const page = (r) => fnv(JSON.stringify(r));
|
||
// 【判词】这一部传写的那一句:你走哪条路、第几阶、什么头衔
|
||
const verdict = (r) => fnv(`${r.channel ? r.channel.id : '-'}|${r.rank.tier}|${r.rank.title}|${r.nearest || '-'}`);
|
||
|
||
function scan(dynId, chushen, over = null) {
|
||
const out = [];
|
||
for (const t of TUPLES) for (const wang of WANGS) {
|
||
const axes = over ? { ...t.axes, ...over.axes } : t.axes;
|
||
const r = computeRank({ axes, wang, chushen, ...(over ? over.extra : null) }, dynId);
|
||
out.push({ w: t.w, p: page(r), v: verdict(r) });
|
||
}
|
||
return out;
|
||
}
|
||
const BASE = {};
|
||
for (const d of DYNASTIES) for (const cs of CONFIGS[d.id]) BASE[`${d.id}|${cs}`] = scan(d.id, cs);
|
||
|
||
const diff = (dynId, cs, now) => {
|
||
const base = BASE[`${dynId}|${cs}`];
|
||
let dv = 0, dp = 0;
|
||
for (let i = 0; i < base.length; i++) {
|
||
if (base[i].v !== now[i].v) dv += base[i].w;
|
||
if (base[i].p !== now[i].p) dp += base[i].w;
|
||
}
|
||
return { dv, dp };
|
||
};
|
||
// 突变一个数据点 → 在【那一朝的每一种 chushen 配置】下重扫 → 逐配置报
|
||
function mutate(dynId, apply, undo) {
|
||
apply();
|
||
const per = CONFIGS[dynId].map((cs) => ({ cs, ...diff(dynId, cs, scan(dynId, cs)) }));
|
||
undo();
|
||
return { per, dv: Math.max(...per.map((p) => p.dv)), dp: Math.max(...per.map((p) => p.dp)) };
|
||
}
|
||
function perturb(dynId, over) {
|
||
const per = CONFIGS[dynId].map((cs) => ({ cs, ...diff(dynId, cs, scan(dynId, cs, over)) }));
|
||
return { per, dv: Math.max(...per.map((p) => p.dv)), dp: Math.max(...per.map((p) => p.dp)) };
|
||
}
|
||
|
||
const rows = [];
|
||
const push = (kind, dyn, what, r, note = '') => rows.push({ kind, dyn, what, ...r, note });
|
||
|
||
for (const d of DYNASTIES) {
|
||
for (const c of d.channels) {
|
||
const x = c.xuan;
|
||
// ── ① 每一个 gate 闸口:删掉它
|
||
for (const k of Object.keys(c.gate || {})) {
|
||
const old = c.gate[k];
|
||
push('gate', d.id, `${c.id} gate{${k}:${old}} 删掉`,
|
||
mutate(d.id, () => { delete c.gate[k]; }, () => { c.gate[k] = old; }));
|
||
// ★★ 闸的【那个数】本身买到了什么?六轴取值只有 40/63/73/79/83(sat 曲线的产物)
|
||
// ⇒ (40,63] 里的任何 need 是【同一道闸】。50 与 55 是不是同一道闸 —— 让引擎自己说。
|
||
if (k !== 'wang' && k !== 'lao') {
|
||
for (const alt of [41, 50, 55, 63]) {
|
||
if (alt === old) continue;
|
||
push('gate-num', d.id, `${c.id} gate{${k}:${old}→${alt}}`,
|
||
mutate(d.id, () => { c.gate[k] = alt; }, () => { c.gate[k] = old; }));
|
||
}
|
||
}
|
||
}
|
||
// ── ② 每一个 key 位:删掉它
|
||
const key = x.key || [];
|
||
for (let i = 0; i < key.length; i++) {
|
||
push('key-pos', d.id, `${c.id} key[${i}]:${key[i]} 删掉`,
|
||
mutate(d.id, () => { x.key = key.filter((_, j) => j !== i); }, () => { x.key = key; }));
|
||
}
|
||
if (key.length) push('key-all', d.id, `${c.id} key=${JSON.stringify(key)} 整条删掉`,
|
||
mutate(d.id, () => { x.key = []; }, () => { x.key = key; }));
|
||
// ── ③ 每一条 kemu
|
||
const kemu = x.kemu || [];
|
||
for (let i = 0; i < kemu.length; i++) {
|
||
push('kemu', d.id, `${c.id} kemu[${i}]:${kemu[i].axis}>=${kemu[i].need} 删掉`,
|
||
mutate(d.id, () => { x.kemu = kemu.filter((_, j) => j !== i); }, () => { x.kemu = kemu; }));
|
||
}
|
||
// ── ④ seat ±1
|
||
const seat = x.seat;
|
||
push('seat', d.id, `${c.id} seat ${seat}→${seat + 1}`,
|
||
mutate(d.id, () => { x.seat = seat + 1; }, () => { x.seat = seat; }));
|
||
// ── ⑤ ceiling:两问分开 —— 它在【择路】那一级读,和它在 tierOf 里【夹】,是两件事
|
||
if (c.ceiling) {
|
||
const ct = c.ceiling.tier;
|
||
if (ct !== 5) push('ceiling', d.id, `${c.id} ceiling.tier ${ct}→5`,
|
||
mutate(d.id, () => { c.ceiling.tier = 5; }, () => { c.ceiling.tier = ct; }));
|
||
// ★★ 【clamp 那一夹】—— 纯算术,不用跑:tier = clamp(seat + 通过条数, 0, ceiling.tier)
|
||
// ⇒ seat + kemu.length 是 tier 的【最大可能值】。它 ≤ ceiling.tier ⇒ 那个 clamp 【永远不夹】。
|
||
const most = (x.seat || 0) + (x.kemu || []).length;
|
||
push('ceiling-clamp', d.id, `${c.id} tierOf 的 clamp(…,0,${ct}):seat+全通=${most}`,
|
||
most > ct ? { per: [], dv: -1, dp: -1 } : { per: [], dv: 0, dp: 0 },
|
||
most > ct ? '夹得住' : '★ 恒不夹(seat+kemu ≤ ceiling)');
|
||
}
|
||
// ── ⑥ 标记位
|
||
for (const f of ['hollow', 'stigma', 'regular']) {
|
||
const old = c[f];
|
||
push('flag', d.id, `${c.id} ${f} ${!!old}→${!old}`,
|
||
mutate(d.id, () => { c[f] = !old; }, () => { c[f] = old; }));
|
||
}
|
||
const as = c.assimilation;
|
||
push('flag', d.id, `${c.id} assimilation ${as || 0}→${as ? 0 : 0.5}`,
|
||
mutate(d.id, () => { c.assimilation = as ? 0 : 0.5; }, () => { c.assimilation = as; }));
|
||
}
|
||
}
|
||
// ── ⑦ 每一条轴:整个钉死成 40(=「这条轴上一项都没点过」)
|
||
for (const d of DYNASTIES) for (const a of AXIS_IDS) {
|
||
push('axis', d.id, `轴 ${a} 全钉成 40(=一项都没点)`, perturb(d.id, { axes: { [a]: 40 } }));
|
||
}
|
||
// ── ⑧ profile 上那几个【引擎的签名收了的】字段:喂值进去
|
||
for (const d of DYNASTIES) for (const [nm, extra] of [
|
||
['lao', { lao: 9999 }], ['soul', { soul: 100 }],
|
||
['counts', { counts: { catchphrase: 9, attitude: 9, timeline: 9 } }], ['profession', { profession: 'poet' }],
|
||
]) push('profile', d.id, `profile.${nm} = ${JSON.stringify(Object.values(extra)[0])}`, perturb(d.id, { extra }));
|
||
|
||
const N = (n) => (n < 0 ? '—' : n.toLocaleString());
|
||
const pad = (s, n) => { s = String(s); let w = 0; for (const ch of s) w += ch.charCodeAt(0) > 0x2000 ? 2 : 1; return s + ' '.repeat(Math.max(1, n - w)); };
|
||
const num = (s, n) => String(s).padStart(n);
|
||
const isDead = (r) => r.dv === 0 && r.dp === 0;
|
||
const onlyPage = (r) => r.dv === 0 && r.dp > 0;
|
||
|
||
console.log(`\n══ 死物 census ══ 每朝每配置 ${N(PER_DYN)} 个 profile(2,790 个六轴取值 × 8 wang,带权 = 524,288)`);
|
||
console.log(`══ 配置:辽 3 种(未答/契丹/汉)、明 2 种(未答/倡優之家)、其余五朝无出身维`);
|
||
console.log(`══ 【改变 0 格】= 这个数据点在整张表上什么都没买到(取【全部配置里最大的那个】,故不冤枉它)\n`);
|
||
console.log(pad('类', 14) + pad('朝', 9) + pad('数据点', 44) + num('判词变', 10) + num('整页变', 10) + ' 备注');
|
||
console.log('─'.repeat(100));
|
||
const want = process.argv.includes('--all') ? () => true : (r) => isDead(r) || onlyPage(r);
|
||
for (const r of rows) {
|
||
if (!want(r)) continue;
|
||
console.log(pad(r.kind, 14) + pad(r.dyn, 9) + pad(r.what, 44) + num(N(r.dv), 10) + num(N(r.dp), 10) +
|
||
' ' + (isDead(r) ? '★★ 死(0 格)' : onlyPage(r) ? '★ 只说话不判事' : '') + (r.note ? ' ' + r.note : ''));
|
||
}
|
||
console.log(`\n──────────────────────────────────────────────`);
|
||
console.log(`数据点总数 ${rows.length}`);
|
||
console.log(`★★【死】改变 0 格 ${rows.filter(isDead).length}`);
|
||
console.log(`★【只改整页不改判词】 ${rows.filter(onlyPage).length} =「它说了一句话,而它什么都没判」`);
|
||
console.log(`活的(改判词) ${rows.filter((r) => r.dv > 0).length}`);
|
||
console.log(`\n★ 逐配置明细(死物在【每一种】配置下都是 0):--verbose`);
|
||
if (process.argv.includes('--verbose')) {
|
||
for (const r of rows) {
|
||
if (!isDead(r) || !r.per.length || r.per.length < 2) continue;
|
||
console.log(pad(r.kind + ' ' + r.dyn + ' ' + r.what, 56) + r.per.map((p) => `${p.cs || '未答'}:${p.dv}/${p.dp}`).join(' '));
|
||
}
|
||
}
|