Files
ChronicleOfTheImmortalClan/tests/balance-edge.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

195 lines
7.4 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { World } from '../src/renderer/game/engine/world'
import { monthlyRate } from '../src/renderer/game/engine/systems/cultivation'
import { combatPowerOf } from '../src/renderer/game/engine/systems/combat'
import { resolveBreakthrough } from '../src/renderer/game/engine/systems/cultivation'
import { computeLegacy, resolveLegacy } from '../src/renderer/game/core/legacy'
import { yearAxis } from '../src/renderer/game/core/yearaxis'
import { buildBiography } from '../src/renderer/game/core/biography'
import { computeGenealogy } from '../src/renderer/game/core/genealogy'
import { marketPrice, buyItem, sellItem } from '../src/renderer/game/engine/market'
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('年龄边界矩阵', () => {
it.each([5, 7, 8, 15, 16, 17, 55, 56, 70].map((a) => [a] as const))('%i 岁修炼速率不越界', (a) => {
const w = baseWorld(`age-${a}`)
const c = w.state.members['x5']
c.realm = { major: 'qi', minor: 1 }
c.bornYear = 1 - a
const r = monthlyRate(w, c)
expect(r).toBeGreaterThan(0)
expect(r).toBeLessThanOrEqual(10)
if (a < 8) expect(r).toBeLessThan(3)
if (a > 65) expect(r).toBeLessThan(4) // 0.1.10 衰减点 65+
})
})
describe('修为边界矩阵', () => {
it.each([-5, 0, 0.1, 49.9, 99.9, 100, 150].map((p) => [p] as const))('进度 %p 被夹在 0-100', (p) => {
const w = baseWorld(`rp-${p}`)
const c = w.state.members['x5']
c.realm = { major: 'qi', minor: 1 }
c.realmProgress = p
w.advanceMonth()
expect(c.realmProgress).toBeGreaterThanOrEqual(0)
expect(c.realmProgress).toBeLessThanOrEqual(100)
})
it('进度 100 且冷却中不自动冲击', () => {
const w = baseWorld('rp-cooldown')
const c = w.state.members['x5']
c.realm = { major: 'qi', minor: 1 }
c.realmProgress = 100
c.lastBreakthroughAttempt = (w.state.year * 12 + w.state.month) - 2
w.advanceMonth()
expect(c.realmProgress).toBeGreaterThanOrEqual(100)
})
})
describe('气血边界矩阵', () => {
it.each([-10, 0, 1, 29, 30, 50, 99, 100, 130].map((h) => [h] as const))('气血 %h 不越 0-100', (h) => {
const w = baseWorld(`hp-${h}`)
const c = w.state.members['x3']
c.health = h
w.advanceMonth()
expect(c.health).toBeGreaterThanOrEqual(0)
expect(c.health).toBeLessThanOrEqual(100)
})
})
describe('库存边界矩阵', () => {
it.each(['lingcao', 'lingkuang', 'beastcore', 'pill-qiyuan', 'pill-ningyuan', 'weapon-qi'].map((i) => [i] as const))(
'库存 %s 交易不为负',
(id) => {
const w = baseWorld(`inv-${id}`)
const before = w.state.family.inventory[id] ?? 0
w.advanceMonth()
expect((w.state.family.inventory[id] ?? 0) - before).toBeGreaterThanOrEqual(-20)
}
)
})
describe('金钱边界矩阵', () => {
it.each([0, 1, 49, 50, 799, 800, 100000].map((s) => [s] as const))('灵石 %s 下买卖不穿仓', (s) => {
const w = baseWorld(`money-${s}`)
const fam = w.state.family
fam.stones = s
const price = marketPrice(w, 'lingcao') * 5
const bought = buyItem(w, 'lingcao', 5)
expect(bought).toBe(s >= price)
expect(fam.stones).toBeGreaterThanOrEqual(0)
if (sellItem(w, 'lingcao', 1)) {
expect(fam.stones).toBeGreaterThanOrEqual(0)
}
})
})
describe('传承与谱系矩阵', () => {
it.each(['x1', 'x2', 'x3', 'x4', 'x5'].map((id) => [id] as const))('%s 列传可生成且不以空结尾', (id) => {
const w = baseWorld(`bio-${id}`)
const lines = buildBiography(w.state, id)
expect(lines.length).toBeGreaterThan(0)
expect(lines[lines.length - 1].label).toBeTruthy()
})
it.each([0, 50, 200].map((y) => [y] as const))('谱系在 %i 年后仍可构建(不抛)', (y) => {
const w = baseWorld(`gen-${y}`)
w.state.year = 1 + y
const rows = computeGenealogy(w)
expect(Array.isArray(rows)).toBe(true)
})
it.each([1, 3, 10, 100].map((y) => [y] as const))('年轴在 %i 年跨度下聚合', (y) => {
const w = baseWorld(`axis-${y}`)
w.state.year = y
w.state.chronicle.push({ id: 'c1', year: Math.max(1, y - 2), month: 1, category: 'breakthrough', text: 'xx', important: false })
const cells = yearAxis(w.state)
expect(cells.length).toBeGreaterThan(0)
})
})
describe('境界战力矩阵', () => {
it.each([
['mortal', 0], ['qi', 0], ['qi', 8], ['foundation', 0], ['core', 0], ['nascent', 0], ['spirit', 0]
] as const)('%s/%i 战力为正且渐强', (major, minor) => {
const w = baseWorld(`pw-${major}`)
const c = w.state.members['x4']
c.realm = { major, minor }
const p = combatPowerOf(w, c)
expect(p).toBeGreaterThan(0)
if (minor > 0 && major === 'qi') {
const c0 = w.state.members['x5']
c0.realm = { major: 'qi', minor: 0 }
expect(p).toBeGreaterThan(combatPowerOf(w, c0))
}
})
it.each(Object.keys(marketStub()).map((it) => [it] as const))('装备 %s 战力增益为正', (item) => {
const w = baseWorld(`eq-${item}`)
const c = w.state.members['x4']
const p0 = combatPowerOf(w, c)
c.equipment = item
expect(combatPowerOf(w, c)).toBeGreaterThan(p0)
})
})
function marketStub(): Record<string, number> {
return { 'weapon-fan': 1, 'weapon-qi': 2, 'weapon-ling': 3, 'weapon-fa': 4 }
}
describe('突破边界矩阵', () => {
it.each([
['mortal', 'qi'], ['qi', 'foundation'], ['foundation', 'core'], ['core', 'nascent'], ['nascent', 'spirit']
] as const)('%s → %s 成功或失败均不破世界', (from, to) => {
const w = baseWorld(`bt-${from}`)
const c = w.state.members['x3']
c.realm = { major: from, minor: from === 'mortal' ? 0 : 8 }
c.realmProgress = 100
c.mind = 8
resolveBreakthrough(w, c, 0.4)
expect(c.realm.major === from || c.realm.major === to).toBe(true)
expect(Number.isNaN(c.realmProgress)).toBe(false)
})
})
describe('评分与结局矩阵', () => {
it.each([0, 30, 55, 85, 120, 160, 200].map((v) => [v] as const))('虚弱档 %i 总评下仍可裁断', (v) => {
const w = baseWorld(`legacy-${v}`)
w.state.stats.repPeak = v
w.state.stats.maxRealmIdx = v
w.state.year = v + 1
w.state.stats.feishengCount = 0
const arch = resolveLegacy(w.state)
expect(arch.title.length).toBeGreaterThan(0)
expect(arch.dims.total).toBeGreaterThan(0)
})
it.each(['renXing', 'daoXing', 'weiMing', 'xiangHuo'].map((k) => [k] as const))('四维 %s 分轴单调', (k) => {
const w = baseWorld(`dim-${k}`)
const d0 = computeLegacy(w.state)
if (k === 'weiMing') w.state.stats.repPeak += 30
if (k === 'renXing') w.state.stats.popPeak += 10
if (k === 'daoXing') w.state.stats.maxRealmIdx += 10
if (k === 'xiangHuo') w.state.year += 50
const d1 = computeLegacy(w.state)
expect((d1 as never as Record<string, number>)[k]).toBeGreaterThan((d0 as never as Record<string, number>)[k])
})
})
describe('跨难度矩阵', () => {
it.each(['easy', 'normal', 'hard'].map((d) => [d] as const))('%s 难度 60 月无崩溃', (d) => {
const w = World.create({ seed: 'diff-' + d, surname: '萧', familyName: `${d}家`, motto: 'm', difficulty: d as never })
resetSaveBus()
attachLogSink(w)
for (let i = 0; i < 60; i++) w.advanceMonth()
expect(w.state.family.stones).toBeGreaterThanOrEqual(0)
})
})