// ★★★★ 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(' ')); } }