Files
ChronicleOfTheImmortalClan/tests/marriage-diplomacy.test.ts
T
thzxx ea9f5c9c5d v0.1.17: 天下为市——世界真炉膛(市场实体化+NPC博弈+灾年持续)
【A 市场实体化(真库存循环)】
- 世界呼吸:池月供给 × 潮汐span - 常驻需求(worldSupplyRate 2% vs worldDemandRate 1.5%)
- NPC 上桌:def.sells/buys 逐月入池出池——池浅采不到→power-15%,池深抛货→市场暖意
- 玩家交易回写:buy 浅池价涨/sell 盈池价跌(tradeSettle 0.35 份额粒度);池深<35% 断供拒单(12% 是枯井)
- marketPrice 统一走 marketMultFor;本地 POOL_BASE/TECH 复刻清除(worldsim-data 单源)

【B 世界格局博弈】
- 战争伤骨:劫掠胜负回写 power(败-18%/胜+6%)+ dead 字段 raidCount/warCooldownYear 启用
- 社交回响:赠礼回礼(30% 灵石/30% 灵草)+ power 微涨;联姻/和约/结盟皆涨 power
- NPC 关系网:relationsWithOthers 填实(同风格亲30~55/异风格隙-35~15)+ 年首±5 漂移;世仇<-50 年首互攻(败方-18%)
- 灾年共苦:灾年 NPC 贸易 ×0.7;tide 对供给 ±0.3 弹性
- 灾年持续 6 月渐退;灾起/灾散快讯(kind 标记)

【UI】
- 天下面板:池深百分比(含断供阈值 tooltip)/本族天下排位/灾年余月
- 快讯响应化:行情见顶抛售5/见底吃进5/灾年救济(50灵石→关系8)
- MarketPanel 法帖价格改 techniquePrice()(硬编码清除)

【测试】994 全绿(38 套件,新增世界炉膛 8 例);金钟罩 0.1.17 基线固化
2026-08-23 14:24:23 +08:00

198 lines
6.3 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)
// 0.1.17 回礼机制:30% 概率回灵石(≤+15)、30% 概率回灵草(不破灵石断言)
expect(w.state.family.stones).toBeGreaterThanOrEqual(800 - 120)
expect(w.state.family.stones).toBeLessThanOrEqual(800 - 120 + 15)
})
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)
}