137 lines
8.8 KiB
JavaScript
137 lines
8.8 KiB
JavaScript
// 共走(两人/多人一起走过同一道大事)的【角色对应关系】单测 —— 纯函数、不需 DB。
|
||
// 证四件事:① 性别识别对真人可靠(后/妃/公主/则天→女,官员→男,显式 sex 可覆盖);
|
||
// ② 女性首发只筛出【真实女性权力角色】、排除男性、去重已认领;③ 二选一是"你 vs 历史"、不编下场;
|
||
// ④ 合传把双证(cite)原样带出,不丢不编。
|
||
// 跑:node --test test/gongzou.test.js
|
||
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { sexOf, pickRoles, guestChoice, weave, femalePath, listNvziDynasties, nvziRoles, nvziZhaoming, nvMing, femaleEnding } from '../src/services/gongzou.js';
|
||
|
||
// 取自 唐·永徽六年「废王立武」真实 cast(people[],各带 zhishi/cite 双证)。
|
||
const CAST = [
|
||
{ ren: '武则天', guan: '太后 → 聖神皇帝', verb: '改國號周', role: 'throne', zhishi: '本太宗才人…载初元年改唐为周、称帝。', cite: '《新唐書》卷四:「壬午,改國號周。」' },
|
||
{ ren: '王皇后', guan: '皇后(王氏)', verb: '被廢', role: 'fell', zhishi: '高宗原配…永徽六年被废。', cite: '《新唐書》卷四:「高宗廢皇后王氏」' },
|
||
{ ren: '褚遂良', guan: '尚書左僕射', verb: '力諫·被貶', role: 'other', zhishi: '太宗顾命…力谏废王立武被贬。', cite: '《新唐書·宰相表》卷61' },
|
||
{ ren: '长孙无忌', guan: '太尉', verb: '力沮·後賜死', role: 'other', zhishi: '太宗元舅、顾命首辅…自缢死。', cite: '《新唐書·宰相表》卷61' },
|
||
];
|
||
|
||
test('★ 性别识别:后/妃/公主/则天→女,官员→男,显式 sex 覆盖', () => {
|
||
assert.equal(sexOf(CAST[0]), 'f', '武则天(则天/太后)应为女');
|
||
assert.equal(sexOf(CAST[1]), 'f', '王皇后(皇后)应为女');
|
||
assert.equal(sexOf(CAST[2]), 'm', '褚遂良(仆射)应为男');
|
||
assert.equal(sexOf(CAST[3]), 'm', '长孙无忌(太尉)应为男');
|
||
assert.equal(sexOf({ ren: '某女官', guan: '尚宮' }), 'f', '女官/尚宫应为女');
|
||
assert.equal(sexOf({ ren: '某人', guan: '太尉', sex: 'f' }), 'f', '显式 sex 必须覆盖头衔启发');
|
||
});
|
||
|
||
test('★★★ 防误判:guan 里【引用】别人的女称,绝不把男性当成女性角色', () => {
|
||
assert.equal(sexOf({ ren: '于琮', guan: '宰相 · 駙馬(尚廣德公主)' }), 'm', '尚公主的驸马是男');
|
||
assert.equal(sexOf({ ren: '仁宗(趙禎)', guan: '皇帝(幼沖·太后垂簾)' }), 'm', '太后垂帘下的皇帝是男');
|
||
assert.equal(sexOf({ ren: '后勝', guan: '齊相' }), 'm', '姓后的齐相是男(裸「后」不判女)');
|
||
assert.equal(sexOf({ ren: '陈后主', guan: '皇帝' }), 'm', '「后主」是男帝');
|
||
// 真女性仍命中:ren 带女称 / guan 开头是本人官职
|
||
assert.equal(sexOf({ ren: '武氏', guan: '宸妃 → 皇后' }), 'f', 'guan 开头本人官职→女');
|
||
assert.equal(sexOf({ ren: '賈后(賈南風)', guan: '皇后' }), 'f', 'guan 皇后→女');
|
||
assert.equal(sexOf({ ren: '太平公主', guan: '鎮國太平公主' }), 'f');
|
||
});
|
||
|
||
test('★★ 女性首发:只筛出真实女性权力角色、排除男性、去重已认领', () => {
|
||
const f = pickRoles(CAST, 'f', []);
|
||
assert.deepEqual(f.map((r) => r.ren), ['武则天', '王皇后'], '女性只应看到武则天/王皇后');
|
||
assert.ok(f.every((r) => r.cite), '每个对位角色必须带 cite 双证');
|
||
|
||
const f2 = pickRoles(CAST, 'f', ['武则天']); // 武则天已被认领
|
||
assert.deepEqual(f2.map((r) => r.ren), ['王皇后'], '已认领的角色不再出现');
|
||
|
||
const m = pickRoles(CAST, 'm', []);
|
||
assert.deepEqual(m.map((r) => r.ren), ['褚遂良', '长孙无忌'], '兄弟一起=筛男性角色(引擎中立,零改动)');
|
||
|
||
const any = pickRoles(CAST, 'any', []);
|
||
assert.equal(any.length, 4, 'any=全体在场');
|
||
});
|
||
|
||
test('★ 二选一是"你 vs 历史"、两个选项、不预告下场', () => {
|
||
const ch = guestChoice({ ren: '武则天', guan: '太后', verb: '改國號周', role: 'throne', sex: 'f' });
|
||
assert.match(ch.prompt, /武则天/);
|
||
assert.match(ch.prompt, /改國號周/, '提示里要带她史上真做的事');
|
||
assert.equal(ch.options.length, 2);
|
||
assert.deepEqual(ch.options.map((o) => o.id), ['go', 'stop']);
|
||
assert.match(ch.prompt, /只给男人留位置/, '女性专门设计:提示要点出"只给男人留位置的朝堂"');
|
||
});
|
||
|
||
test('★★ 女子那条真实的路:从官衔判出 女皇/临朝/后位/宗室/后宫/女官', () => {
|
||
assert.equal(femalePath({ ren: '武则天', guan: '太后 → 聖神皇帝' }).key, 'nvhuang', '称帝→女皇(顶)');
|
||
assert.equal(femalePath({ guan: '太皇太后 · 臨朝稱制' }).key, 'linchao', '临朝称制');
|
||
assert.equal(femalePath({ guan: '皇后(王氏)' }).key, 'houwei', '后位');
|
||
assert.equal(femalePath({ ren: '太平公主', guan: '鎮國太平公主' }).key, 'gongzhu', '天家之女');
|
||
assert.equal(femalePath({ guan: '宸妃 → 皇后' }).key, 'houwei', '已升皇后→后位(皇后先于妃)');
|
||
assert.equal(femalePath({ guan: '尚宮' }).key, 'nvguan', '女官');
|
||
});
|
||
|
||
test('★★ 单独玩(A):nvziRoles 出真实女性角色,带路径/两难/双证/所属大事', () => {
|
||
const { roles } = nvziRoles('tang');
|
||
assert.ok(roles.length >= 3, '唐应有多位女性(武则天/太平公主/王皇后/武氏…)');
|
||
assert.ok(roles.some((r) => r.ren === '武则天' && r.path.key === 'nvhuang'), '武则天=女皇');
|
||
for (const r of roles) {
|
||
assert.equal(r.sex, 'f', '单独玩只出女性');
|
||
assert.ok(r.choice && r.choice.options.length === 2, '每个女子都带两难');
|
||
assert.ok(r.event && r.event.title, '带所属大事');
|
||
}
|
||
});
|
||
|
||
test('★ 单独玩(A):listNvziDynasties 只列有女性的朝、带数量与样本', () => {
|
||
const ds = listNvziDynasties();
|
||
assert.ok(ds.length >= 8, '至少 8 朝有女性角色');
|
||
assert.ok(ds.every((d) => d.count > 0 && d.name), '每朝都有数量与朝名');
|
||
assert.ok(ds.some((d) => d.code === 'tang'), '含唐');
|
||
});
|
||
|
||
test('★★ 女子照命(B):各朝天花板取最高女子路径、用真到顶的女人做证、最高在前', () => {
|
||
const z = nvziZhaoming();
|
||
assert.ok(z.length >= 8, '至少 8 朝');
|
||
const tang = z.find((x) => x.code === 'tang');
|
||
assert.ok(tang && tang.top.path.key === 'nvhuang' && tang.top.ren === '武则天', '唐顶=女皇·武则天');
|
||
assert.equal(z[0].code, 'tang', '女皇那朝(唐)天花板最高、排最前');
|
||
assert.ok(z.every((x) => x.top && x.top.ren && x.top.path), '每朝都有做证的女人+路径');
|
||
});
|
||
|
||
test('★★ 女子照命·个人推演(B):门第/才→各朝女子命,落差随门第变、命被门第压制、明标推演', () => {
|
||
const rich = nvMing({ jiashi: 3, cai: 1 });
|
||
assert.equal(rich.tuiyan, true, '必须明标推演');
|
||
assert.ok(rich.rows.length >= 8);
|
||
const tang = rich.rows.find((r) => r.code === 'tang');
|
||
assert.ok(tang && tang.linchaoReachable, '名门女子·唐(顶=女皇) 应望临朝');
|
||
assert.ok(rich.spread, '门第高各朝有落差');
|
||
assert.ok(rich.rows.every((r) => r.ceiling && r.ceiling.ren), '每朝天花板都有真人做证');
|
||
|
||
const poor = nvMing({ jiashi: 0, cai: 0 });
|
||
assert.ok(poor.rows.every((r) => !r.linchaoReachable), '寒微无才:哪朝都望不到临朝');
|
||
assert.ok(poor.rows.every((r) => r.reach.tier <= 3), '寒微无才:各朝都低 —— 命被门第压制');
|
||
});
|
||
|
||
test('★★ 女主角结局:传世替无名女子立传,到顶者不谎称被抹', () => {
|
||
const e1 = femaleEnding({ ren: '武则天', guan: '太后 → 聖神皇帝' }); // 女皇
|
||
assert.match(e1.chuanshiTop, /本纪|后妃传|留了位置/, '到顶的女子:史书留了名(不谎称她被抹去)');
|
||
assert.match(e1.chuanshiClose, /替她们立/, '也替无数无名女子立传');
|
||
const e2 = femaleEnding({ ren: '某宫人', guan: '才人' }); // 后宫(未到顶)
|
||
assert.match(e2.chuanshiTop, /某氏|某女|没给女子立传/, '未到顶:史书多半没立传');
|
||
assert.ok(e1.huixiang && e2.huixiang, '回响两者都有');
|
||
});
|
||
|
||
test('★★ 合传:双证(cite)原样带出、织成"你们俩一起走过"一句', () => {
|
||
const session = {
|
||
dynName: '唐', hostName: '阿康',
|
||
ev: { year: 655, title: '废王立武', gloss: null },
|
||
hostSeat: { weiLabel: '门下·谏官', roleKey: 'juzhong', roleLabel: '身在局中' },
|
||
hostPick: { text: '附立', end: { tone: 'sheng', word: '受赏' } },
|
||
};
|
||
const seat = { ...CAST[0], sex: 'f' };
|
||
const join = { name: '小敏', seatRen: '武则天', pick: { id: 'go', text: '也走到那一步' } };
|
||
const hz = weave(session, join, seat);
|
||
assert.equal(hz.guest.ren, '武则天');
|
||
assert.equal(hz.guest.truth.cite, CAST[0].cite, '合传必须把真人 cite 双证原样带出');
|
||
assert.match(hz.line, /唐·废王立武/);
|
||
assert.match(hz.line, /阿康/);
|
||
assert.match(hz.line, /小敏是武则天/);
|
||
});
|