Files
ChronicleOfTheImmortalClan/tests/world-matrix.test.ts
T
thzxx 586fa643b1 v0.1.10: 长线决断(数值重建 + 长跑鉴证)
评估发现(3000月×6种子实证):
- 旧数值在正常难度 6/6 种子 90-125 年绝嗣(修炼慢+寿元短+渡劫卡死三重叠加)
- 关键缺陷:渡劫事件被 eventRoll 误压制(qi8→筑基永不可达),境界峰恒为炼气

重建(data/ 数值 + engine 修复):
- 修炼提速:基率 0.5+感知0.1→1.0+感知0.18;pacing qi 1→2.2(各境界等比提)
- 修为曲线:qi expGrowth 1.35→1.10(9层累计工期<28年)
- 寿元放宽:凡人75 / 炼气150 / 筑基220 / 金丹340 / 元婴520 / 化神850
- 失败跌损收窄 40-65%→26-44%;衰减点 55→65 岁;突破概率全线+0.1
- 渡劫修复:悬置 12 月自动压制(挂机不软锁)、玩家处理即零计数、
  大比 FIFO 不被拍卖/传薪挤掉(queue 兜底)
- 新增:仙门拍卖(十月·灵石漏斗)/ 名宿传薪(八年·传承)/ 圣者护族(幼儿修行提速)

鉴证:
- 金钟罩升 3 档(47/100/180 年 × 3 seed = 9 指纹固化)
- longevity 套件(200 年不败/渡劫晋升/拍卖传薪)
- rebuild 套件(速率/工期/寿元/跌损/渡劫率)
- 测试 881→905;typecheck/build 通过
2026-08-23 11:13:48 +08:00

169 lines
6.4 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { World } from '../src/renderer/game/engine/world'
import { stateFingerprint, longRun } from './fingerprint.helper'
import { computeGenealogy } from '../src/renderer/game/core/genealogy'
import { applyEventChoice } from '../src/renderer/game/engine/systems/events'
import { resetSaveBus, attachLogSink } from './world.helpers'
describe('多 seed 长跑矩阵', () => {
it.each([1, 2, 3, 4, 5].map((n) => [`seed-matrix-${n}`] as const))('%s 240 月可存活推进', (seed) => {
const w = World.create({ seed, surname: '官', familyName: '官家', motto: 'm', difficulty: 'normal' })
resetSaveBus()
attachLogSink(w)
for (let i = 0; i < 240; i++) {
if (w.state.gameOver) break
w.advanceMonth()
}
expect(w.state.year).toBeGreaterThan(5)
})
})
describe('各代际矩阵', () => {
it.each([1, 10, 30, 50, 100].map((g) => [g] as const))('第%i 代后谱系与成员状态合法', (g) => {
const w = World.create({ seed: 'gen-' + g, surname: '关', familyName: '关家', motto: 'm', difficulty: 'normal' })
for (const c of Object.values(w.state.members)) c.generation = g
w.state.family.generation = g
const rows = computeGenealogy(w)
expect(rows.every((r: { gen: number }) => r.gen >= 1)).toBe(true)
})
})
describe('继承链矩阵(多路径)', () => {
it('出生顺序长子继位', () => {
const w = World.create({ seed: 'heir-a', surname: '韩', familyName: '韩家', motto: 'm', difficulty: 'normal' })
w.state.members['x1'].alive = false
w.advanceMonth()
expect(w.state.family.headId).toBe('x3') // first-born
})
it('同代男子无后时取高境界', () => {
const w = World.create({ seed: 'heir-b', surname: '韩', familyName: '韩家', motto: 'm', difficulty: 'normal' })
w.state.members['x1'].alive = false
w.state.members['x3'].alive = false
w.state.members['x5'].alive = false
w.state.members['x4'].realm = { major: 'foundation', minor: 0 }
w.state.members['x2'].realm = { major: 'qi', minor: 2 }
w.advanceMonth()
expect(w.state.family.headId).toBe('x4') // 高境界优先
})
})
describe('gameOver 路径守卫', () => {
it.each([
['全员死亡', (w: World) => Object.values(w.state.members).forEach((c) => (c.alive = false))],
['家主死亡且无后人', (w: World) => {
w.state.members['x1'].alive = false
w.state.members['x3'].alive = false
}]
] as const)('%s → 世界终止标记', (_name, fn) => {
const w = World.create({ seed: 'go-' + _name.length, surname: '欧阳', familyName: '欧阳家', motto: 'm', difficulty: 'normal' })
fn(w)
resetSaveBus()
attachLogSink(w)
if (_name.startsWith('全员')) {
// 全员死亡:无活人即 gameover
w.advanceMonth()
expect(w.state.gameOver).toBeTruthy()
} else {
// 有活人(x2 寡)则继承;x2 就是继承人
w.advanceMonth()
expect(w.state.gameOver || w.state.family.headId).toBeTruthy()
}
})
})
describe('系统级固定周期事件', () => {
it('每年必有岁簿(paper 触发统计)', () => {
const w = World.create({ seed: 'paper-a', surname: '吴', familyName: '吴家', motto: 'm', difficulty: 'normal' })
const yearsSeen: number[] = []
w.out.push({
onLog: () => undefined,
onChronicle: () => undefined,
onBattle: () => undefined,
onPendingEvent: () => undefined,
onGameOver: () => undefined,
onYearPaper: (r) => yearsSeen.push(r.year)
})
for (let i = 0; i < 26; i++) w.advanceMonth()
expect(yearsSeen).toContain(1)
expect(yearsSeen).toContain(2)
})
it('每五年大比征召(第10/15/20年)', () => {
const w = World.create({ seed: 'tourn-b', surname: '邵', familyName: '邵家', motto: 'm', difficulty: 'normal' })
const seen: string[] = []
w.out.push({
onLog: () => undefined,
onChronicle: () => undefined,
onBattle: () => undefined,
onPendingEvent: (id) => { if (id.startsWith('ev-tournament-')) seen.push(id) },
onGameOver: () => undefined
})
for (let i = 0; i < 20 * 12; i++) {
if (w.state.pendingEvent) {
applyEventChoice(w, w.state.pendingEvent, 0)
w.state.pendingEvent = undefined
}
w.advanceMonth()
}
expect(seen).toContain('ev-tournament-10')
expect(seen).toContain('ev-tournament-15')
expect(seen).toContain('ev-tournament-20')
})
it('偶数年四邻回声整年单发', () => {
const w = World.create({ seed: 'echo-b', surname: '邱', familyName: '邱家', motto: 'm', difficulty: 'normal' })
const sounds: number[] = []
w.out.push({
onLog: () => undefined,
onChronicle: () => undefined,
onBattle: () => undefined,
onPendingEvent: (id) => { if (id.startsWith('ev-echo-')) sounds.push(Date.now() % 1000) },
onGameOver: () => undefined
})
for (const y of [12, 14, 16]) {
w.state.year = y
w.state.month = 1
w.state.family.flag = {}
w.state.pendingEvent = undefined
// 直接调用 eventRoll 检验年内单发
w.advanceMonth()
if (w.state.pendingEvent) {
sounds.push(1)
w.state.pendingEvent = undefined
}
w.advanceMonth()
if (w.state.pendingEvent) sounds.push(2)
}
// 每偶数年均至少一打一(flag 封缄)
expect(sounds.length).toBeGreaterThanOrEqual(3)
})
})
describe('确定性复跑矩阵', () => {
it.each([1, 2, 3].map((n) => [`det-replay-${n}`] as const))('%s 两次 120 月轨迹一致', (seed) => {
const a = World.create({ seed, surname: '洪', familyName: '洪家', motto: 'm', difficulty: 'normal' })
const b = World.create({ seed, surname: '洪', familyName: '洪家', motto: 'm', difficulty: 'normal' })
resetSaveBus()
attachLogSink(a)
resetSaveBus()
attachLogSink(b)
for (let i = 0; i < 120; i++) {
a.advanceMonth()
b.advanceMonth()
}
a.state.pendingEvent = undefined
b.state.pendingEvent = undefined
const fa = JSON.parse(JSON.stringify({ ...a.state, pendingEvent: undefined }))
const fb = JSON.parse(JSON.stringify({ ...b.state, pendingEvent: undefined }))
expect(JSON.stringify(fa)).toBe(JSON.stringify(fb))
})
})
describe('fingerprint 跨版本稳定性锚点', () => {
it('保存后重放同世界(存档点回复)', () => {
const w = longRun('det-anchor')
expect(stateFingerprint(w.state)).toBe(stateFingerprint(w.state))
})
})