// office5d 定官模型 单测 —— 2026-07-21 立、07-23 老板「5 维加权铨选、每维可见影响、家世也认科举朝、零游戏味」重写。 // 锁死:①最低=低官 ②才是主力但一选不到顶·顶要叠满 ③家世(门阀朝重·科举朝也认「能读书」非0) ④分量单项也出分·平滑 // ⑤封顶诚实(技术官不越 t3·商极低·文满配到顶) ⑥主业分路 ⑦性格不再白选 ⑧家世·科举朝省一档(寒门难出贵子) // ⑨每维单调推动 ⑩品第档位(parts.pins·非百分比游戏词)。官名全来自双证阶梯,不新编。 import test from 'node:test'; import assert from 'node:assert/strict'; import { computeOffice5D, caiRating, xingRating, fenRating, menContrib } from '../src/office5d.js'; import { computeAxes } from '../src/dynasties.js'; import { subsFromAttrs } from '../src/attributes.js'; const FULL_SC = ['poetic', 'story', 'vivid', 'socratic']; // 才尽量满(本行 wen 三项 + 一项旁才轴) const FULL_XG = ['warm', 'humor', 'playful', 'signature']; // 性格满(行为轴) const FULL_CRED = { zhicheng: 3, jiangxiang: 3, zhiwu: 3 }; // 分量全高(合计 9) function prof({ lei = 'wen', sc = [], xg = [], cred = {}, jiashi = 0, wang = 0, lao = 0, knowledge = 0, zheng = 0 } = {}) { return { professionLei: lei, axes: computeAxes(subsFromAttrs(sc, xg)), credentials: cred, jiashi, wang, lao, knowledge, zhengming: zheng }; } const T = (opts, dyn = 'tang') => computeOffice5D(prof(opts), dyn).rank.tier; test('①最低(只主业) → 低官 t1,不是白身也不是高官', () => { for (const dyn of ['weijin', 'tang', 'song', 'ming']) { const r = computeOffice5D(prof({}), dyn); assert.equal(r.rank.tier, 1, `${dyn}: 只主业应 t1`); assert.ok(r.rank.title && r.rank.title !== '白身', `${dyn}: 应有实职官名`); } }); test('②才是主力但一选不到顶,顶级要叠满', () => { assert.equal(T({}), 1, '空 → t1'); assert.equal(T({ sc: ['poetic'] }), 2, '1擅长 → t2'); assert.equal(T({ sc: ['poetic', 'story'] }), 2, '★ 2擅长仍 t2(不跳)'); assert.equal(T({ sc: FULL_SC }), 3, '★ 才满也只到 t3(刺史级),不到顶'); assert.equal(T({ cred: FULL_CRED }), 2, '★ 分量全高单独 → 仅 t2(不叠不到高位)'); assert.equal(T({ sc: FULL_SC, cred: FULL_CRED }), 4, '才满+分量满 → t4(侍郎)'); assert.equal(T({ sc: FULL_SC, cred: FULL_CRED, wang: 50, lao: 40, knowledge: 30 }), 5, '★ 再+改命满 → t5(顶级需近满配)'); }); test('③家世→级数:门阀朝重、科举朝也认(能读书·非0)、逐档单调', () => { assert.ok(menContrib('weijin', 3) > menContrib('weijin', 2), '门阀 > 士族'); assert.ok(menContrib('weijin', 2) > menContrib('weijin', 1), '士族 > 庶族'); assert.ok(menContrib('tang', 3) > 0, '★★ 唐(科举朝)名门也认——家世好能读书,不再恒 0(老板拍板)'); assert.ok(menContrib('weijin', 3) > menContrib('tang', 3), '★ 同名门:门阀朝(魏晋)远重于科举朝(唐)'); assert.equal(menContrib('tang', 0), 0, '寒门 → 0'); }); test('④分量→档:单项也出分(修掉旧「单项=0」)、平滑单调、满到顶档', () => { assert.equal(fenRating({}), 0, '空 → 0'); assert.ok(fenRating({ zhicheng: 3 }) > 0, '★★ 单项职称正高 → >0(不再白选,老板拍板)'); assert.ok(fenRating({ zhicheng: 3, jiangxiang: 3 }) > fenRating({ zhicheng: 3 }), '两项 > 一项(单调)'); assert.equal(fenRating(FULL_CRED), 4, '三项全高 → 顶档 4'); }); test('⑤封顶诚实:文满配到顶,技术官(医)全叠满也不越 t3,商极低', () => { assert.equal(T({ sc: FULL_SC, cred: FULL_CRED, wang: 50, lao: 40, knowledge: 30 }), 5, '文满配 → 唐顶 t5'); const yi = computeOffice5D(prof({ lei: 'yi', sc: FULL_SC, xg: FULL_XG, cred: FULL_CRED, jiashi: 3, wang: 50, lao: 40, knowledge: 30 }), 'tang'); assert.ok(yi.rank.tier <= 3, '★ 医全叠满也不越 t3(不许医生当宰相)'); const shang = computeOffice5D(prof({ lei: 'shang', sc: FULL_SC, xg: FULL_XG, cred: FULL_CRED, jiashi: 3, wang: 50, lao: 40 }), 'tang'); assert.ok(shang.rank.tier <= 1, '★ 商满配封 t1(古代少入仕途)'); }); test('⑥主业分路:武人在有独立武选的朝(宋)走武阶', () => { const wu = computeOffice5D(prof({ lei: 'wu', sc: ['blunt', 'motivate'], cred: FULL_CRED }), 'song'); assert.ok(/殿直|差使|巡检|砦|武/.test(wu.rank.title + wu.rank.trackName), `武人宋走武选,得:${wu.rank.title}/${wu.rank.trackName}`); }); // ══ 2026-07-23 新增:证明「5 维都有可见影响」(旧版性格/分量单项/科举家世白选) ══ const MID = { sc: ['poetic', 'story'] }; // 才2 → 唐 t2(中位基准,逐维在此加满看推动) test('⑦性格不再白选:中位上性格加满 → 官阶推动(旧版纹丝不动)', () => { assert.equal(T(MID), 2, '中位才2 → t2'); assert.ok(caiRating(computeAxes(subsFromAttrs([], FULL_XG)), 'wen') === 0, '性格不进才轴(才=0)'); assert.ok(xingRating(computeAxes(subsFromAttrs([], FULL_XG))) > 0, '性格进行为轴(>0)'); assert.ok(T({ ...MID, xg: FULL_XG }) > T(MID), '★★ 性格满 → 官阶推动'); }); test('⑧家世·科举朝也影响 + 寒门难出贵子(名门省一档才)', () => { assert.ok(T({ ...MID, jiashi: 3 }, 'tang') > T({ ...MID, jiashi: 0 }, 'tang'), '★ 唐(科举朝):名门 > 寒门'); const mingCai2 = T({ sc: ['poetic', 'story'], jiashi: 3 }, 'tang'); // 名门 + 才2 const hanCai2 = T({ sc: ['poetic', 'story'], jiashi: 0 }, 'tang'); // 寒门 + 才2 const hanCai3 = T({ sc: ['poetic', 'story', 'vivid'], jiashi: 0 }, 'tang'); // 寒门 + 才3 assert.ok(mingCai2 > hanCai2, '同才2:名门高于寒门'); assert.equal(mingCai2, hanCai3, '★★ 名门+才2 = 寒门+才3(寒门要多攒一档才追平——寒门难出贵子)'); }); test('⑨每维单调:中位上分量/改命/问鼎加满都能推动官阶(问鼎独立第6维·非死维)', () => { assert.ok(T({ ...MID, cred: FULL_CRED }) > T(MID), '分量满 → 推动'); assert.ok(T({ ...MID, wang: 50, lao: 40, knowledge: 30 }) > T(MID), '改命满 → 推动'); assert.ok(T({ ...MID, zheng: 3 }) > T(MID), '问鼎满 → 推动'); // ★ 问鼎独立:改命已满时,问鼎再加仍推得动(修「改命满则问鼎死」) const gaiMax = { ...MID, wang: 50, lao: 40, knowledge: 30 }; assert.ok(T({ ...gaiMax, zheng: 3 }) > T(gaiMax), '改命满时问鼎仍推动 → 无死维'); }); test('⑩品第档位:parts.pins 六维俱全(才/性格/分量/门第/改命/问鼎)、随维变、★ 零游戏味(无百分比/进度词)', () => { const p = computeOffice5D(prof({ sc: FULL_SC, cred: FULL_CRED, jiashi: 3 }), 'weijin').parts; for (const k of ['cai', 'xing', 'fen', 'men', 'gai', 'zheng']) { assert.ok(p.pins?.[k] && typeof p.pins[k].label === 'string' && p.pins[k].label, `${k} 有品第文字`); } assert.equal(computeOffice5D(prof({ jiashi: 0 }), 'weijin').parts.pins.men.label, '寒门', '出身寒门品第'); assert.equal(computeOffice5D(prof({ jiashi: 3 }), 'weijin').parts.pins.men.label, '门阀', '出身门阀品第'); assert.ok(!/[%%]|进度|百分|完成度|升级|通关|解锁|成就|养成/.test(JSON.stringify(p)), '★★ 品第绝无百分比/游戏禁词(合规红线)'); });