Files
admin 0593ff7e62
nginx serve /uploads direct (audio speed) / patch-nginx (push) Failing after 1m34s
数字我 Digital Me:全量入库(大文件走 Git LFS)
2026-10-10 16:24:43 +08:00

184 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ★★★★ 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(' '));
}
}