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 双签名)
This commit is contained in:
2026-08-23 08:47:21 +08:00
parent f38669fa4a
commit 09cbad6449
14 changed files with 1605 additions and 40 deletions
+334
View File
@@ -0,0 +1,334 @@
import { describe, expect, it } from 'vitest'
import {
MAJORS,
MAJOR_ORDER,
MAJOR_NAMES,
describeRealm,
realmTier,
compareRealm,
nextRealm,
maxRealmExp,
basePower,
breakthroughBaseChance,
realmDeathChance,
TECHNIQUE_GRADE_NAMES
} from '../src/renderer/game/data/realms'
import { ELEMENT_LIST, ROOT_GRADES, ROOT_GRADE_NAMES, describeRoots } from '../src/renderer/game/data/elements'
import { ITEMS, ARTIFACT_POWER } from '../src/renderer/game/data/items'
import { TECHNIQUES } from '../src/renderer/game/data/techniques'
import { BUILDINGS } from '../src/renderer/game/data/buildings'
import { MISSIONS, ENEMIES, missionById } from '../src/renderer/game/data/secrets'
import { NPCS, npcById } from '../src/renderer/game/data/npcs'
import { POSTS, POST_ORDER } from '../src/renderer/game/data/posts'
import { EVENTS } from '../src/renderer/game/data/events'
import { MAJOR_RATE, masteryRateOfMajor } from '../src/renderer/game/data/pacing'
import { SURNAME_POOL, MALE_GIVEN, FEMALE_GIVEN } from '../src/renderer/game/core/names'
import { TRAITS } from '../src/renderer/game/data/traits'
describe('realms 境界表', () => {
it('每大境界结构完整(名称/层数/寿元/修为递增进)', () => {
for (const major of MAJOR_ORDER) {
const def = MAJORS[major]
expect(def.name).toBeTruthy()
expect(def.minorLayers).toBeGreaterThanOrEqual(0)
expect(def.lifespan).toBeGreaterThan(0)
expect(def.expBase).toBeGreaterThanOrEqual(0)
expect(def.expGrowth).toBeGreaterThanOrEqual(1)
}
expect(MAJOR_NAMES.mortal).toBe('凡人')
expect(MAJOR_NAMES.spirit).toBe('化神')
})
it('大境界寿元单调递增', () => {
for (let i = 1; i < MAJOR_ORDER.length; i++) {
expect(MAJORS[MAJOR_ORDER[i]].lifespan).toBeGreaterThan(MAJORS[MAJOR_ORDER[i - 1]].lifespan)
}
})
it('describeRealm 映射正确', () => {
expect(describeRealm({ major: 'mortal', minor: 0 })).toBe('凡人')
expect(describeRealm({ major: 'qi', minor: 0 })).toBe('炼气1层')
expect(describeRealm({ major: 'qi', minor: 8 })).toBe('炼气9层')
expect(describeRealm({ major: 'spirit', minor: 2 })).toBe('化神3层')
})
it('realmTier/compareRealm 正确排序', () => {
expect(realmTier({ major: 'mortal', minor: 0 })).toBe(0)
expect(realmTier({ major: 'qi', minor: 0 })).toBe(11)
expect(realmTier({ major: 'core', minor: 0 })).toBe(31)
expect(compareRealm({ major: 'qi', minor: 3 }, { major: 'foundation', minor: 0 })).toBeLessThan(0)
expect(compareRealm({ major: 'qi', minor: 9 }, { major: 'qi', minor: 5 })).toBeGreaterThan(0)
})
it('nextRealm 链完整走完直至化神壁', () => {
let r = { major: 'mortal', minor: 0 } as { major: (typeof MAJOR_ORDER)[number]; minor: number }
r = nextRealm(r)!
expect(r).toEqual({ major: 'qi', minor: 0 })
r = nextRealm(r)!
expect(r).toEqual({ major: 'qi', minor: 1 })
r = { major: 'qi', minor: 8 }
r = nextRealm(r)!
expect(r).toEqual({ major: 'foundation', minor: 0 })
r = { major: 'foundation', minor: 2 }
r = nextRealm(r)!
expect(r).toEqual({ major: 'core', minor: 0 })
r = { major: 'spirit', minor: 2 }
expect(nextRealm(r)).toBeNull()
})
it('maxRealmExp 随层数严格增长', () => {
for (const major of ['qi', 'foundation', 'core', 'nascent'] as const) {
for (let i = 0; i < MAJORS[major].minorLayers - 1; i++) {
expect(maxRealmExp(major, i + 1)).toBeGreaterThan(maxRealmExp(major, i))
}
}
})
it('basePower 战力随境界单调递增', () => {
const seq: { major: (typeof MAJOR_ORDER)[number]; minor: number }[] = [
{ major: 'mortal', minor: 0 },
{ major: 'qi', minor: 0 },
{ major: 'qi', minor: 8 },
{ major: 'foundation', minor: 0 },
{ major: 'core', minor: 0 },
{ major: 'nascent', minor: 0 },
{ major: 'spirit', minor: 0 }
]
for (let i = 1; i < seq.length; i++) {
expect(basePower(seq[i])).toBeGreaterThan(basePower(seq[i - 1]))
}
})
it('突破基础概率与死亡风险在合法区间且递减/递增', () => {
const majors = ['mortal', 'qi', 'foundation', 'core', 'nascent', 'spirit']
for (const major of majors) {
const b = breakthroughBaseChance({ major: major as never, minor: 0 })
expect(b).toBeGreaterThan(0)
expect(b).toBeLessThanOrEqual(0.9)
}
for (let i = 1; i < majors.length; i++) {
expect(breakthroughBaseChance({ major: majors[i] as never, minor: 0 })).toBeLessThan(
breakthroughBaseChance({ major: majors[i - 1] as never, minor: 0 })
)
}
for (const major of majors) {
expect(realmDeathChance({ major: major as never, minor: 0 }, 5)).toBeGreaterThanOrEqual(0)
expect(realmDeathChance({ major: major as never, minor: 0 }, 5)).toBeLessThan(0.06)
}
})
it('品阶名数量一致', () => {
expect(TECHNIQUE_GRADE_NAMES.length).toBe(5)
})
})
describe('elements 灵根', () => {
it('五行与品阶定义齐全', () => {
expect(ELEMENT_LIST.length).toBe(5)
for (const g of [0, 1, 2, 3, 4, 5]) {
expect(ROOT_GRADES[g]).toBeTruthy()
expect(ROOT_GRADES[g].drawWeight).toBeGreaterThan(0)
expect(ROOT_GRADES[g].expBonus).toBeGreaterThan(0)
}
expect(ROOT_GRADE_NAMES.length).toBe(6)
})
it('品阶修为加成单调', () => {
for (let g = 1; g <= 5; g++) {
expect(ROOT_GRADES[g].expBonus).toBeGreaterThan(ROOT_GRADES[g - 1].expBonus)
}
})
it('describeRoots 包含主属性与品阶', () => {
const label = describeRoots({ grade: 3, primary: '火', secondary: ['金', '水'] })
expect(label).toContain('火')
expect(label).toContain('真品灵根')
})
})
describe('items 物品表', () => {
it('物品 id 唯一且价格为正', () => {
const ids = new Set<string>()
for (const [id, item] of Object.entries(ITEMS)) {
expect(ids.has(id)).toBe(false)
ids.add(id)
expect(item.basePrice).toBeGreaterThan(0)
expect(item.name).toBeTruthy()
}
})
it('法宝战力表与法器条目一一对应', () => {
const artifacts = Object.entries(ITEMS).filter(([, v]) => v.kind === 'artifact')
for (const [id] of artifacts) {
expect(ARTIFACT_POWER[id]).toBeGreaterThan(0)
}
expect(Object.keys(ARTIFACT_POWER).length).toBe(artifacts.length)
})
})
describe('techniques 功法表', () => {
it('字段在法律区间且元素合法', () => {
for (const t of TECHNIQUES) {
expect(t.grade).toBeGreaterThanOrEqual(0)
expect(t.grade).toBeLessThanOrEqual(4)
expect(t.expBonus).toBeGreaterThan(0)
expect(t.powerBonus).toBeGreaterThan(0)
expect(ELEMENT_LIST).toContain(t.element)
expect(['剑修', '体修', '丹修', '阵修', '符修', '御灵']).toContain(t.path)
}
})
it('品阶越高综合面板越强(对比均值)', () => {
const avg = (g: number) => {
const pool = TECHNIQUES.filter((t) => t.grade === g)
const exp = pool.reduce((a, t) => a + t.expBonus, 0) / pool.length
const pow = pool.reduce((a, t) => a + t.powerBonus, 0) / pool.length
return exp + pow
}
for (let g = 2; g <= 4; g++) {
expect(avg(g)).toBeGreaterThan(avg(g - 1))
}
})
})
describe('buildings 建筑表', () => {
it('所有建筑定义完整、升级成本递增', () => {
for (const [id, def] of Object.entries(BUILDINGS)) {
expect(def.maxLevel).toBeGreaterThan(0)
expect(def.name).toBeTruthy()
for (let l = 1; l < def.maxLevel; l++) {
const now = def.upgradeCost(l)
const next = def.upgradeCost(l + 1)
expect(next.stones).toBeGreaterThan(now.stones)
expect(next.lingkuang).toBeGreaterThan(now.lingkuang)
if (def.produceTable) {
const p0 = def.produceTable(l)
const p1 = def.produceTable(l + 1)
const key = Object.keys(p0)[0]
expect((p1[key] ?? 0)).toBeGreaterThanOrEqual((p0[key] ?? 0))
}
}
}
})
it('id 无重复', () => {
expect(new Set(Object.keys(BUILDINGS)).size).toBe(Object.keys(BUILDINGS).length)
})
})
describe('secrets 秘境', () => {
it('每个秘境阶段引用存在的敌人、奖励合法', () => {
const enemyIds = new Set(ENEMIES.map((e) => e.id))
for (const m of MISSIONS) {
expect(m.minMembers).toBeGreaterThan(0)
expect(m.minMembers).toBeLessThanOrEqual(m.maxMembers)
expect(m.risk).toBeGreaterThan(0)
expect(m.risk).toBeLessThanOrEqual(1)
expect(m.stages.length).toBeGreaterThan(0)
for (const st of m.stages) {
expect(['event', 'combat', 'resource', 'boss']).toContain(st.kind)
expect(st.months).toBeGreaterThan(0)
if (st.enemyId) expect(enemyIds.has(st.enemyId)).toBe(true)
if (st.loot) {
for (const r of Object.values(st.loot.resources)) {
expect(r[0]).toBeGreaterThanOrEqual(0)
expect(r[1]).toBeGreaterThanOrEqual(r[0])
}
}
}
for (const r of Object.values(m.completionLoot.resources)) {
expect(r[1]).toBeGreaterThan(r[0])
}
}
expect(missionById('m-anmoku').name).toBe('暗墨林')
expect(() => missionById('nope')).toThrow()
})
})
describe('npcs 势力表', () => {
it('四家齐全、growth 为正、leader 境界合法', () => {
expect(NPCS.length).toBe(4)
for (const n of NPCS) {
expect(n.powerGrowth[0]).toBeGreaterThan(0)
expect(n.powerGrowth[1]).toBeGreaterThanOrEqual(n.powerGrowth[0])
expect(MAJOR_ORDER).toContain(n.leaderRealm)
}
expect(npcById('n-nulei').name).toBe('怒雷祝氏')
expect(() => npcById('nope')).toThrow()
})
})
describe('posts 职事', () => {
it('职事完备且加成归一', () => {
expect(POST_ORDER.length).toBe(5)
expect(POSTS['head'].max).toBe(1)
for (const id of POST_ORDER) {
expect(POSTS[id].max).toBeGreaterThan(0)
expect(POSTS[id].effect.value).toBeGreaterThan(0)
}
})
})
describe('events 事件池', () => {
it('事件 id 唯一、结构合法、once 权重边界', () => {
const ids = new Set<string>()
for (const e of EVENTS) {
expect(ids.has(e.id)).toBe(false)
ids.add(e.id)
expect(e.weight).toBeGreaterThan(0)
expect(['daily', 'major', 'fate']).toContain(e.category)
expect(e.options.length).toBeGreaterThan(0)
expect(e.text.length).toBeGreaterThan(5)
for (const o of e.options) {
expect(o.label.length).toBeGreaterThan(0)
}
}
})
it('事件条件引用的建筑/资源存在', () => {
for (const e of EVENTS) {
const cond = e.cond
if (cond?.minBuilding) {
expect(BUILDINGS[cond.minBuilding.id]).toBeTruthy()
}
if (cond?.minResource) {
const id = cond.minResource.id
expect(id === 'stones' || ITEMS[id]).toBeTruthy()
}
if (cond?.relation) {
expect(NPCS.some((n) => n.id === cond.relation!.npcId)).toBe(true)
}
}
})
})
describe('pacing 节奏', () => {
it('全部大境界有修炼速率', () => {
for (const major of MAJOR_ORDER) {
const rate = masteryRateOfMajor(major)
expect(rate).toBeGreaterThan(0)
expect(rate).toBeLessThanOrEqual(1)
}
expect(MAJOR_RATE['spirit']).toBeLessThan(MAJOR_RATE['qi'])
})
})
describe('names 人名池', () => {
it('姓氏/名池非空且无重复', () => {
expect(SURNAME_POOL.length).toBeGreaterThan(20)
expect(MALE_GIVEN.length).toBeGreaterThan(20)
expect(FEMALE_GIVEN.length).toBeGreaterThan(20)
expect(new Set(MALE_GIVEN).size).toBe(MALE_GIVEN.length)
expect(new Set(FEMALE_GIVEN).size).toBe(FEMALE_GIVEN.length)
})
})
describe('traits 秉性', () => {
it('全部特质定义完整、danger 在界', () => {
for (const t of Object.values(TRAITS)) {
expect(t.name).toBeTruthy()
expect(t.danger).toBeGreaterThanOrEqual(0)
expect(t.danger).toBeLessThanOrEqual(1)
}
})
})