Files
ChronicleOfTheImmortalClan/tests/pcgen-cultivation.test.ts
T
thzxx 09cbad6449 test: 测试矩阵扩至 161 项(深度审计 + 5 处缺陷修复)
新增测试矩阵(12 文件 / 161 cases):
- rng 12 / data 18 / pcgen+cultivation 20 / economy 12 / combat+missions 24
- marriage+diplomacy 13 / events 12 / world+lifecycle 8(含镜像一致)/ save 4+5 / audit 8

审计带出并修复的真实缺陷:
1. Rng.pick 空数组未报错(防空白数组进而崩溃难查)
2. combatPowerOf 未应用功法悟道品阶战力加成(0.1.2 设计缺口)
3. 重伤者即使不代表伤状态仍按满速修炼(health 进修炼公式)
4. 寡妇/鳏夫无法再婚:候选列表放着人执行层却拒婚(spouseId 死旧契约)
5. isWidowed 参数契约陷阱:传 id 静默 false(升级接受 Character|Id 双签名)
2026-08-23 08:47:21 +08:00

262 lines
9.4 KiB
TypeScript
Raw 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.
import { describe, expect, it } from 'vitest'
import { World } from '../src/renderer/game/engine/world'
import { Rng, seedToRng } from '../src/renderer/game/core/rng'
import { newCharacter, rollAttributes, rollRoots, rollPersonality, traitBonuses, calcLifespan } from '../src/renderer/game/engine/pcgen'
import { monthlyRate, perAttemptChance } from '../src/renderer/game/engine/systems/cultivation'
import { lifespanOf } from '../src/renderer/game/engine/systems/lifecycle'
function rng(): Rng {
return new Rng(seedToRng('pcgen'))
}
describe('pcgen 人物生成', () => {
it('rollAttributes 限制在 1~10', () => {
const r = rng()
for (let i = 0; i < 300; i++) {
const v = rollAttributes(r, 5, 3)
expect(v).toBeGreaterThanOrEqual(1)
expect(v).toBeLessThanOrEqual(10)
}
})
it('rollRoots 品阶在 0~5,副属性与主属性不重复', () => {
const r = rng()
for (let i = 0; i < 50; i++) {
const roots = rollRoots(r)
expect(roots.grade).toBeGreaterThanOrEqual(0)
expect(roots.grade).toBeLessThanOrEqual(5)
expect(roots.secondary.length).toBeLessThanOrEqual(2)
expect(roots.secondary).not.toContain(roots.primary)
expect(new Set(roots.secondary).size).toBe(roots.secondary.length)
}
})
it('父母品阶子代平均贴近父母', () => {
const r = rng()
const dad = newCharacter(rng(), { name: '父', gender: 'male', generation: 1, bornYear: -20, age: 20, realm: { major: 'qi', minor: 1 } })
dad.roots = { grade: 4, primary: '火', secondary: [] }
const mom = newCharacter(rng(), { name: '母', gender: 'female', generation: 1, bornYear: -20, age: 20, realm: { major: 'qi', minor: 1 } })
mom.roots = { grade: 4, primary: '火', secondary: [] }
let near = 0
for (let i = 0; i < 40; i++) {
const child = rollRoots(r, { m: dad, f: mom })
if (child.grade >= 3) near++
}
// 双天品父母子代大概率不劣于玄品
expect(near).toBeGreaterThan(25)
})
it('rollPersonality 返回 1~2 个合法特质', () => {
const r = rng()
for (let i = 0; i < 50; i++) {
const t = rollPersonality(r)
expect(t.length).toBeGreaterThanOrEqual(1)
expect(t.length).toBeLessThanOrEqual(2)
expect(new Set(t).size).toBe(t.length)
}
})
it('traitBonuses 聚合回值在合理区间', () => {
const chara = newCharacter(rng(), { name: '某人', gender: 'male', generation: 1, bornYear: -20, age: 20 })
chara.traits = ['zisheng', 'jingkan'] // 自律+精研
const b = traitBonuses(chara)
expect(b.exp).toBeGreaterThan(1)
expect(b.breakBonus + b.charmBonus).toBeGreaterThanOrEqual(0)
chara.traits = ['xinheng'] // 性狠
expect(traitBonuses(chara).windBonus).toBeGreaterThan(0)
})
it('calcLifespan 随根骨略增长且为正', () => {
const low = calcLifespan('qi', 1)
const high = calcLifespan('qi', 9)
expect(high).toBeGreaterThan(low)
expect(low).toBeGreaterThan(0)
})
})
describe('monthlyRate 修炼速率', () => {
function baseWorld(seed: string): World {
return World.create({ seed, surname: '尹', familyName: '尹家', motto: 'm', difficulty: 'normal' })
}
it('感知越高修得越快', () => {
const w = baseWorld('cult-a')
const c = w.state.members['x5']
c.realm = { major: 'qi', minor: 1 }
c.realmProgress = 0
c.perception = 3
const low = monthlyRate(w, c)
c.perception = 9
const high = monthlyRate(w, c)
expect(high).toBeGreaterThan(low * 1.6)
})
it('闭关显著快于闲居', () => {
const w = baseWorld('cult-b')
const c = w.state.members['x5']
c.realm = { major: 'qi', minor: 1 }
c.state = 'idle'
const idle = monthlyRate(w, c)
c.state = 'meditation'
const med = monthlyRate(w, c)
expect(med).toBeGreaterThan(idle * 1.25)
})
it('负伤大幅减速,重伤近乎停滞', () => {
const w = baseWorld('cult-c')
const c = w.state.members['x5']
c.realm = { major: 'qi', minor: 1 }
c.health = 80
const mild = monthlyRate(w, c)
c.health = 15
const grave = monthlyRate(w, c)
expect(grave).toBeLessThan(mild * 0.6)
})
it('童子启蒙与暮年衰退', () => {
const w = baseWorld('cult-d')
const c = w.state.members['x5']
c.realm = { major: 'qi', minor: 1 }
c.bornYear = w.state.year // 0 岁
const kid = monthlyRate(w, c)
c.bornYear = w.state.year - 70
const elder = monthlyRate(w, c)
const adult = (() => {
const c2 = w.state.members['x4']
c2.bornYear = w.state.year - 30
return monthlyRate(w, c2)
})()
expect(kid).toBeLessThan(adult)
expect(elder).toBeLessThan(adult)
})
it('聚灵阵与洞府对闭关加成', () => {
const w = baseWorld('cult-e')
w.state.family.buildings['juling'] = 2
w.state.family.buildings['dongfu'] = 2
const c = w.state.members['x5']
c.realm = { major: 'qi', minor: 1 }
c.state = 'meditation'
const withB = monthlyRate(w, c)
w.state.family.buildings = { lingtian: 1, zongci: 1 }
const without = monthlyRate(w, c)
expect(withB).toBeGreaterThan(without)
})
it('无功法散修明显慢于有功者', () => {
const w = baseWorld('cult-f')
const c1 = w.state.members['x5']
c1.realm = { major: 'qi', minor: 1 }
c1.techniqueId = 't-qinglian'
const withT = monthlyRate(w, c1)
c1.techniqueId = undefined
const without = monthlyRate(w, c1)
expect(withT).toBeGreaterThan(without)
})
it('大境界修炼效率递减(元婴比炼气慢)', () => {
const w = baseWorld('cult-g')
const c1 = w.state.members['x5']
c1.realm = { major: 'qi', minor: 1 }
const qi = monthlyRate(w, c1)
c1.realm = { major: 'nascent', minor: 0 }
const nas = monthlyRate(w, c1)
expect(nas).toBeLessThan(qi)
})
it('perAttemptChance 受心性与性格影响(勇气)', () => {
const w = baseWorld('cult-h')
const c = w.state.members['x5']
c.realm = { major: 'qi', minor: 8 }
c.realmProgress = 100
c.mind = 3
c.traits = []
const low = perAttemptChance(w, c)
c.mind = 9
c.traits = ['tiangan']
const high = perAttemptChance(w, c)
expect(high).toBeGreaterThan(low)
expect(low).toBeGreaterThan(0.4)
})
it('瓶颈自动尝试存在 6 个月冷却', () => {
const w = baseWorld('cult-i')
const c = w.state.members['x5']
c.realm = { major: 'qi', minor: 1 }
c.realmProgress = 100
c.lastBreakthroughAttempt = w.state.year * 12 + w.state.month - 3
const before = c.realmProgress // 应保持 100(冷却中不自动尝试)
w.advanceMonth()
expect(c.realmProgress).toBeGreaterThanOrEqual(100) // 仍 ≥100(要么未尝试要么成功从0算)
// 未尝试则保持 100;尝试要么成功(realm 变化或者 progress=0
const plausible =
c.realmProgress >= 100 || (c.realmProgress < 100 && c.realm.major === 'qi' && c.realm.minor === 1) || c.realm.major !== 'qi'
expect(plausible).toBe(true)
void before
})
})
describe('lifecycle 寿命', () => {
it('50 岁修士不会自然死亡(远早于寿元)', () => {
const w = World.create({ seed: 'life-a', surname: '秦', familyName: '秦家', motto: 'm', difficulty: 'normal' })
const c = w.state.members['x1']
c.bornYear = w.state.year - 50
for (let i = 0; i < 24; i++) w.advanceMonth()
expect(c.alive).toBe(true)
})
it('寿元临界后逐渐死亡(模拟 60 周年内全部辞世)', () => {
const w = World.create({ seed: 'life-b', surname: '雷', familyName: '雷家', motto: 'm', difficulty: 'easy' })
const c = w.state.members['x5']
c.bornYear = w.state.year - 999 // 深过期
let died = false
for (let i = 0; i < 120 && !died; i++) {
w.advanceMonth()
died = !c.alive
}
expect(died).toBe(true)
expect(c.deathCause === '幼夭' || c.deathCause === '寿元将尽' || c.deathCause === '伤势不治').toBe(true)
})
it('重伤平复机制:伤者每月回复→健康', () => {
const w = World.create({ seed: 'life-c', surname: '盼', familyName: '盼家', motto: 'm', difficulty: 'normal' })
const c = w.state.members['x3']
c.health = 10
c.state = 'wounded'
const h0 = c.health
w.advanceMonth()
expect(c.health).toBeGreaterThan(h0)
// 不断推进最终恢复闲居
for (let i = 0; i < 12; i++) w.advanceMonth()
expect(c.health).toBeGreaterThanOrEqual(95)
if (c.alive && c.health >= 95) expect(c.state).toBe('idle')
})
it('重伤过低小幅增加早逝风险(抽样验证无崩溃且部分死亡)', () => {
const w = World.create({ seed: 'life-d', surname: '荣', familyName: '荣家', motto: 'm', difficulty: 'normal' })
const c = w.state.members['x4']
c.bornYear = w.state.year - 999 // 未到寿限前不会因这点死亡; 直接推到长期
c.health = 1
c.physique = 1
let deaths = 0
for (let i = 0; i < 150; i++) w.advanceMonth()
deaths = !c.alive ? 1 : 0
if (deaths) expect(c.deathCause).toBeTruthy()
})
})
describe('lifespanOf 计算', () => {
it('随大境界与根骨增长', () => {
const w = World.create({ seed: 'ls', surname: '魏', familyName: '魏家', motto: 'm', difficulty: 'normal' })
const c = w.state.members['x3']
c.realm = { major: 'qi', minor: 1 }
c.physique = 4
const qi = lifespanOf(w, c)
c.realm = { major: 'core', minor: 0 }
const core = lifespanOf(w, c)
expect(core).toBeGreaterThan(qi)
c.physique = 8
expect(lifespanOf(w, c)).toBeGreaterThan(qi)
})
})