结构(旧 game/core+engine → 新 engine/ 域): - engine/kernel/ 时钟/随机/插件协议/fxqueue/timesense/format/urgency/guide/names(原 core) - engine/narrative/ legacy/报告/列传/谱系/年轴(原 core 叙事族) - engine/runtime/ World/creation/pcgen/ApiFacade/capabilities/pluginManager/boot/clocks + Systems/*(12 系统) - engine/sim/ Market(未来 WorldSim 同行) - 旧 game/core、engine/systems、engine/world.ts 等路径全部废弃(无 re-export 兼容层) 验证:35 套件/967 测试全绿(金钟罩三档零漂移=纯搬迁无行为变化) typecheck 0 error
196 lines
6.2 KiB
TypeScript
196 lines
6.2 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { World } from '../src/renderer/game/engine/runtime/World'
|
|
import {
|
|
giftNpc,
|
|
marryNpcFamily,
|
|
makePeace,
|
|
arrangeWedding,
|
|
diplomacyTick
|
|
} from '../src/renderer/game/engine/runtime/Systems/diplomacy'
|
|
import { yearStartMarriage } from '../src/renderer/game/engine/runtime/Systems/marriage'
|
|
import { resetSaveBus, attachLogSink } from './world.helpers'
|
|
|
|
function baseWorld(seed: string): World {
|
|
const w = World.create({ seed, surname: '池', familyName: '池家', motto: 'm', difficulty: 'normal' })
|
|
resetSaveBus()
|
|
attachLogSink(w)
|
|
return w
|
|
}
|
|
|
|
describe('marriage 婚配生育', () => {
|
|
it('媒人只撮合合适人选并完成夫妻绑定', () => {
|
|
const w = baseWorld('mar-a')
|
|
// 造一对适婚人
|
|
const man = w.state.members['x3']
|
|
const woman = w.state.members['x5']
|
|
man.bornYear = w.state.year - 20
|
|
woman.bornYear = w.state.year - 18
|
|
w.state.year = 2 // 保证媒人会走
|
|
yearStartMarriage(w)
|
|
const paired = man.spouseId === woman.id || woman.spouseId === man.id
|
|
// 同父兄妹(x1 之父)实为同父,应绝不互配
|
|
expect(paired).toBe(false)
|
|
})
|
|
|
|
it('姻亲生育年首发生,血统与代数正确', () => {
|
|
const w = baseWorld('mar-b')
|
|
w.state.year = 2
|
|
yearStartMarriage(w)
|
|
const newborns = Object.values(w.state.members).filter(
|
|
(c) => c.bornYear === 2 && c.generation === 2
|
|
)
|
|
// 概率性事件不做硬性:至少系统不崩且无非法代数
|
|
for (const nb of newborns) {
|
|
expect(nb.generation).toBe(2)
|
|
expect(nb.alive).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('双胞胎概率出现时登记双新生儿', () => {
|
|
const w = baseWorld('mar-c')
|
|
w.state.year = 2
|
|
let twinSeen = false
|
|
for (let i = 0; i < 30 && !twinSeen; i++) {
|
|
// 复用同一开局状态重跑不同 rng? 不能; 直接跑 240 月统计
|
|
}
|
|
for (let i = 0; i < 240; i++) w.advanceMonth()
|
|
// 有 duo born in same month among same parents -> twin 可接受未出现
|
|
expect(w.state.members).toBeTruthy()
|
|
})
|
|
|
|
it('寡妇再醮候选均为适龄异性', () => {
|
|
const w = baseWorld('mar-d')
|
|
const wife = w.state.members['x2']
|
|
w.state.members['x1'].alive = false
|
|
expect(w.isWidowed(wife.id)).toBe(true)
|
|
const cands = w.marriageCandidatesOf(wife.id)
|
|
expect(cands.length).toBeGreaterThan(0)
|
|
for (const c of cands) {
|
|
expect(c.gender).not.toBe(wife.gender)
|
|
expect(w.ageOf(c)).toBeGreaterThanOrEqual(16)
|
|
expect(w.ageOf(c)).toBeLessThanOrEqual(46)
|
|
}
|
|
})
|
|
|
|
it('同父或同母不可婚(双防线)', () => {
|
|
const w = baseWorld('mar-e')
|
|
const a = w.state.members['x3']
|
|
const b = w.state.members['x5']
|
|
a.fatherId = 'x1'
|
|
b.fatherId = 'x1'
|
|
expect(w.marryTo(a.id, b.id)).toBe(false)
|
|
a.fatherId = undefined
|
|
b.fatherId = undefined
|
|
a.motherId = 'x2'
|
|
b.motherId = 'x2'
|
|
expect(w.marryTo(a.id, b.id)).toBe(false)
|
|
})
|
|
|
|
it('自由婚配成功绑定(叔父与续弦寡妇)', () => {
|
|
const w = baseWorld('mar-f')
|
|
w.state.members['x1'].alive = false // 家主离世令 x2 为寡
|
|
const a = w.state.members['x4']
|
|
const b = w.state.members['x2']
|
|
expect(w.marryTo(a.id, b.id)).toBe(true)
|
|
expect(a.spouseId).toBe(b.id)
|
|
expect(b.spouseId).toBe(a.id)
|
|
})
|
|
})
|
|
|
|
describe('diplomacy 外交', () => {
|
|
it('赠礼换算关系并按上限封顶', () => {
|
|
const w = baseWorld('dip-a')
|
|
const npc = w.state.npcFamilies['n-xuanying']
|
|
npc.relation = 95
|
|
const before = npc.relation
|
|
expect(giftNpc(w, npc.id, 120)).toBe(true)
|
|
expect(npc.relation).toBe(100)
|
|
expect(w.state.family.stones).toBe(800 - 120)
|
|
})
|
|
|
|
it('赠礼不足预算时拒绝且分文不动', () => {
|
|
const w = baseWorld('dip-b')
|
|
w.state.family.stones = 20
|
|
expect(giftNpc(w, 'n-xuanying', 120)).toBe(false)
|
|
expect(w.state.family.stones).toBe(20)
|
|
})
|
|
|
|
it('寻衅有一年冷却', () => {
|
|
const w = baseWorld('dip-c')
|
|
const npc = w.state.npcFamilies['n-nulei']
|
|
const before = npc.relation
|
|
expect(w.tauntNpc(npc.id)).toBe(true)
|
|
expect(npc.relation).toBe(before - 20)
|
|
expect(w.tauntNpc(npc.id)).toBe(false)
|
|
w.state.year += 1
|
|
expect(w.tauntNpc(npc.id)).toBe(true)
|
|
})
|
|
|
|
it('联姻要求 25 以上关系且一家一亲', () => {
|
|
const w = baseWorld('dip-d')
|
|
const npc = w.state.npcFamilies['n-danxin']
|
|
npc.relation = 20
|
|
w.state.members['x3'].bornYear = 1 - 19
|
|
expect(marryNpcFamily(w, npc.id)).toBe(false)
|
|
npc.relation = 50
|
|
expect(marryNpcFamily(w, npc.id)).toBe(true)
|
|
expect(npc.allied).toBe(true)
|
|
expect(marryNpcFamily(w, npc.id)).toBe(false)
|
|
})
|
|
|
|
it('关系月度向中立漂移靠拢', () => {
|
|
const w = baseWorld('dip-e')
|
|
const npc = w.state.npcFamilies['n-xuanying']
|
|
npc.relation = 40
|
|
let drifted = 0
|
|
for (let i = 0; i < 12; i++) {
|
|
const before = npc.relation
|
|
diplomacyTick(w)
|
|
if (npc.relation < before) drifted++
|
|
}
|
|
expect(drifted).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('敌对过深时潜在袭击条件:冷却与关系门槛', () => {
|
|
const w = baseWorld('dip-f')
|
|
const npc = w.state.npcFamilies['n-nulei']
|
|
npc.relation = -80
|
|
w.state.year = 10
|
|
w.state.family.flag['raidCD-n-nulei'] = 10
|
|
let fired = false
|
|
for (let i = 0; i < 36; i++) {
|
|
diplomacyTick(w)
|
|
if (w.state.pendingEvent?.startsWith('ev-raid-')) {
|
|
fired = true
|
|
w.state.pendingEvent = undefined
|
|
}
|
|
}
|
|
expect(fired).toBe(false) // 冷却年内不动
|
|
w.state.year = 20
|
|
for (let i = 0; i < 200; i++) {
|
|
diplomacyTick(w)
|
|
if (w.state.pendingEvent?.startsWith('ev-raid-')) {
|
|
fired = true
|
|
break
|
|
}
|
|
}
|
|
expect(fired).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('genealogy 谱系结构', () => {
|
|
it('夫妻联组与单身分离', () => {
|
|
const w = baseWorld('gen-a')
|
|
const w2 = w.state
|
|
const rows = computeGenealogyLocal(w)
|
|
const g1 = rows.find((r) => r.gen === 1)
|
|
expect(g1).toBeTruthy()
|
|
expect(g1!.units.some((u) => u.parents.some((p) => p?.id === 'x1'))).toBe(true)
|
|
})
|
|
})
|
|
|
|
import { computeGenealogy } from '../src/renderer/game/engine/narrative/genealogy'
|
|
function computeGenealogyLocal(w: World) {
|
|
return computeGenealogy(w)
|
|
}
|