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 通过
This commit is contained in:
2026-08-23 11:13:48 +08:00
parent f5a2c1b68a
commit 586fa643b1
18 changed files with 448 additions and 67 deletions
+2 -2
View File
@@ -82,7 +82,7 @@ describe('审计回归:P0 修复固化', () => {
})
it('防御性修补后金钟罩不变(行为等价确认)', () => {
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('9af71ecb')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('ebfec4a4')
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('a3223c77')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('195b8aaa')
})
})
+3 -3
View File
@@ -18,7 +18,7 @@ function baseWorld(seed: string): World {
}
describe('年龄边界矩阵', () => {
it.each([5, 7, 8, 15, 16, 17, 55, 56, 70].map((a) => [a] as const))('%i 岁修炼速率不越界0.4-1×)', (a) => {
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 }
@@ -26,8 +26,8 @@ describe('年龄边界矩阵', () => {
const r = monthlyRate(w, c)
expect(r).toBeGreaterThan(0)
expect(r).toBeLessThanOrEqual(10)
if (a < 8) expect(r).toBeLessThan(2)
if (a > 55) expect(r).toBeLessThan(2)
if (a < 8) expect(r).toBeLessThan(3)
if (a > 65) expect(r).toBeLessThan(4) // 0.1.10 衰减点 65+
})
})
+30 -12
View File
@@ -1,25 +1,34 @@
import { describe, expect, it } from 'vitest'
import { stateFingerprint, longRun } from './fingerprint.helper'
import { World } from '../src/renderer/game/engine/world'
/**
* 金钟罩:固定种子长跑 560 月的状态指纹。
* 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。
* 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。
*/
// 0.1.8 基线:特征加成(自律/精研/急躁/云游生效)+ 飞升窗口永续 + market 利己折扣后重新固化
// 0.1.6 GreenClock 重构行为等价;0.1.7 插件门面化零漂移)
const GOLDEN: Record<string, string> = {
'bell-seed-1': '9af71ecb',
'bell-seed-2': '63cede89',
'bell-seed-3': 'ebfec4a4'
// 0.1.10 基线:长线数值重建(修炼提速/寿元放宽/渡劫修复)后三档固化为 47/100/180 年。
// 0.1.8修复批次后为 9af71ecb/63cede89/ebfec4a40.1.10 有意变更数值后按三档重算。
const GOLDEN: Record<string, Record<number, string>> = {
'bell-seed-1': { 560: 'a3223c77', 1200: '55aead92', 2160: 'ae2d9fb7' },
'bell-seed-2': { 560: '3b8dcde4', 1200: '27becabd', 2160: '12d7e520' },
'bell-seed-3': { 560: '195b8aaa', 1200: 'a19e1a90', 2160: '2d767a64' }
}
describe('金钟罩 · 长跑确定性指纹', () => {
for (const [seed, hash] of Object.entries(GOLDEN)) {
it(`${seed} 560 月指纹 === ${hash}`, () => {
const w = longRun(seed)
expect(stateFingerprint(w.state)).toBe(hash)
})
const TIERS = [
[560, '47 年(4 个十年)'],
[1200, '100 年'],
[2160, '180 年']
] as const
describe('金钟罩 · 长跑确定性指纹(三档:47/100/180 年)', () => {
for (const [seed, tiers] of Object.entries(GOLDEN)) {
for (const [months, label] of TIERS) {
it(`${seed} ${label}指纹 === ${tiers[months]}`, () => {
const w = run(seed, months)
expect(stateFingerprint(w.state)).toBe(tiers[months])
})
}
}
it('同 seed 两次长跑指纹一致(无状态污染)', () => {
@@ -28,3 +37,12 @@ describe('金钟罩 · 长跑确定性指纹', () => {
expect(a).toBe(b)
})
})
function run(seed: string, months: number): ReturnType<typeof longRun> {
const w = World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
for (let i = 0; i < months; i++) {
if (w.state.gameOver) break
w.advanceMonth()
}
return w
}
+4 -2
View File
@@ -307,9 +307,11 @@ describe('pacing 节奏', () => {
for (const major of MAJOR_ORDER) {
const rate = masteryRateOfMajor(major)
expect(rate).toBeGreaterThan(0)
expect(rate).toBeLessThanOrEqual(1)
// 0.1.10 提速后 qi=2.2 属设计,仅验证仍低于高境界速率
}
expect(MAJOR_RATE['spirit']).toBeLessThan(MAJOR_RATE['qi'])
// 高境界低于低境界
expect(MAJOR_RATE['spirit']).toBeLessThan(MAJOR_RATE['foundation'])
expect(MAJOR_RATE['nascent']).toBeLessThan(MAJOR_RATE['core'])
})
})
+2 -2
View File
@@ -170,7 +170,7 @@ describe('GameFacade 门面', () => {
it('默认配置金钟罩不受门面化影响', () => {
PACK.reset()
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('9af71ecb')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('ebfec4a4')
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('a3223c77')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('195b8aaa')
})
})
+139
View File
@@ -0,0 +1,139 @@
import { describe, expect, it } from 'vitest'
import { World } from '../src/renderer/game/engine/world'
import { applyEventChoice } from '../src/renderer/game/engine/systems/events'
import { resetSaveBus, attachLogSink } from './world.helpers'
const SEEDS = ['long-a', 'long-b', 'long-c']
function runYearWithActions(w: World, years: number): void {
const months = years * 12
for (let i = 0; i < months; i++) {
if (w.state.gameOver) break
if (w.state.pendingEvent) {
applyEventChoice(w, w.state.pendingEvent, 0)
w.state.pendingEvent = undefined
}
w.advanceMonth()
}
}
function highestMajor(w: World): number {
const order = ['mortal', 'qi', 'foundation', 'core', 'nascent', 'spirit']
let max = 0
for (const c of Object.values(w.state.members)) {
if (!c.alive) continue
const idx = order.indexOf(c.realm.major)
if (idx > max) max = idx
}
return max
}
describe('长线存活验收(玩家代理)', () => {
it.each(SEEDS.map((s) => [s] as const))('%s200 年内未因修炼瓶颈灭族', (seed) => {
const w = World.create({ seed, surname: '岳', familyName: '岳家', motto: 'm', difficulty: 'normal' })
resetSaveBus()
attachLogSink(w)
runYearWithActions(w, 200)
const alive = Object.values(w.state.members).filter((c) => c.alive).length
// 若灭族,必须发生在非常晚期(>120Y),且修炼已达高位——即非"卡死"
if (w.state.gameOver) {
expect(w.state.gameOver.year).toBeGreaterThan(120)
}
expect(alive).toBeGreaterThanOrEqual(0)
})
it.each(SEEDS.map((s) => [s] as const))('%s:有成员达到筑基或以上(修炼不卡死)', (seed) => {
const w = World.create({ seed, surname: '岳', familyName: '岳家', motto: 'm', difficulty: 'normal' })
resetSaveBus()
attachLogSink(w)
runYearWithActions(w, 120)
expect(highestMajor(w)).toBeGreaterThanOrEqual(2)
})
it('玩家代理 300 年内不卡在炼气(存在高境界成员或已登高位后陨落)', () => {
const w = World.create({ seed: 'long-g', surname: '岳', familyName: '岳家', motto: 'm', difficulty: 'normal' })
resetSaveBus()
attachLogSink(w)
runYearWithActions(w, 300)
const reached = w.state.chronicle.some((e) => e.category === 'breakthrough' && /筑基|金丹|元婴|化神/.test(e.text))
if (w.state.gameOver) {
expect(reached).toBe(true) // 若灭族,必曾登高位——非修炼卡死
} else {
expect(highestMajor(w)).toBeGreaterThanOrEqual(1)
}
})
})
describe('渡劫悬置回退', () => {
it('悬置 12 月自动压制(挂机不软锁)', () => {
const w = World.create({ seed: 'trib-hang', surname: '袁', familyName: '袁家', motto: 'm', difficulty: 'normal' })
resetSaveBus()
attachLogSink(w)
const c = w.state.members['x5']
c.realm = { major: 'qi', minor: 8 }
c.realmProgress = 100
c.mind = 9
c.perception = 9
let suppressedOnce = false
for (let i = 0; i < 30; i++) {
w.advanceMonth()
// 压制发生时事件被清 & delay 被设
if (!w.state.pendingEvent && c.tribDelayYear && (c.tribDelayYear ?? 0) >= 1 && !c.tribPendingMonths) suppressedOnce = true
if (c.tribDelayYear === undefined && !c.tribPendingMonths && !w.state.pendingEvent?.startsWith('ev-trib-')) {
// 可能刚 fire 于本月
}
}
// 至少发生过一次压制(不软锁)
expect(suppressedOnce).toBe(true)
})
it('玩家处理渡劫后确实晋升(qi8→筑基)', () => {
const w = World.create({ seed: 'trib-handle', surname: '袁', familyName: '袁家', motto: 'm', difficulty: 'normal' })
resetSaveBus()
attachLogSink(w)
const c = w.state.members['x5']
c.realm = { major: 'qi', minor: 8 }
c.realmProgress = 100
c.mind = 9
c.perception = 9
for (let i = 0; i < 60; i++) {
if (w.state.pendingEvent) {
applyEventChoice(w, w.state.pendingEvent, 0) // 选"硬渡"
w.state.pendingEvent = undefined
}
w.advanceMonth()
if (c.realm.major === 'foundation') break
}
expect(c.realm.major).toBe('foundation')
})
})
describe('拍卖与传薪', () => {
it('拍卖三分支统一不破', () => {
for (let i = 0; i < 10; i++) {
const w = World.create({ seed: `auc-${i}`, surname: '封', familyName: '封家', motto: 'm', difficulty: 'normal' })
resetSaveBus()
attachLogSink(w)
applyEventChoice(w, 'ev-auction', 0)
expect(w.state.family.stones).toBeGreaterThanOrEqual(500)
}
})
it('名宿传薪事件可触发', () => {
const w = World.create({ seed: 'legacy-x', surname: '危', familyName: '危家', motto: 'm', difficulty: 'normal' })
w.state.year = 12
const c = w.state.members['x1']
c.bornYear = 1 - 70
c.realm = { major: 'foundation', minor: 1 }
let fired = false
for (let i = 0; i < 48; i++) {
if (w.state.pendingEvent === 'ev-legacypass') {
fired = true
break
}
if (w.state.pendingEvent) w.state.pendingEvent = undefined
w.advanceMonth()
}
expect(fired).toBe(true)
})
})
+3 -3
View File
@@ -85,9 +85,9 @@ describe('PluginCore 插件协议', () => {
})
it('默认管线金钟罩不受插件层影响', () => {
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('9af71ecb')
expect(stateFingerprint(longRun('bell-seed-2').state)).toBe('63cede89')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('ebfec4a4')
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('a3223c77')
expect(stateFingerprint(longRun('bell-seed-2').state)).toBe('3b8dcde4')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('195b8aaa')
})
it('facade 插件查询与 about.plugins', () => {
+115
View File
@@ -0,0 +1,115 @@
import { describe, expect, it } from 'vitest'
import { World } from '../src/renderer/game/engine/world'
import { monthlyRate } from '../src/renderer/game/engine/systems/cultivation'
import { MAJORS } from '../src/renderer/game/data/realms'
import { applyEventChoice } from '../src/renderer/game/engine/systems/events'
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('0.1.10 数值重建验收', () => {
it('修炼速率比 0.1.9 基线快(感知7 qi 期 ≥1.5/月)', () => {
const w = baseWorld('pace-a')
const c = w.state.members['x5']
c.realm = { major: 'qi', minor: 0 }
c.perception = 7
c.roots = { grade: 3, primary: '火', secondary: [] }
c.techniqueId = undefined
const r = monthlyRate(w, c)
expect(r).toBeGreaterThanOrEqual(1.5)
})
it('qi 层间累计修为窗口 < 30 年(感知7+凡品+无功法)', () => {
const w = baseWorld('pace-b')
const c = w.state.members['x5']
c.realm = { major: 'qi', minor: 0 }
c.perception = 7
c.roots = { grade: 2, primary: '火', secondary: [] }
c.state = 'idle'
const r = monthlyRate(w, c)
// 9 层总月数 = 100/r × 9(近似)
const months = (100 / r) * 9
expect(months).toBeLessThan(30 * 12)
})
it('寿元梯度放宽:凡人75 → 化神850', () => {
expect(MAJORS['mortal'].lifespan).toBe(75)
expect(MAJORS['qi'].lifespan).toBe(150)
expect(MAJORS['foundation'].lifespan).toBe(220)
expect(MAJORS['core'].lifespan).toBe(340)
expect(MAJORS['nascent'].lifespan).toBe(520)
expect(MAJORS['spirit'].lifespan).toBe(850)
})
it('小层突破失败跌损收窄(26-44%而非40-65%', () => {
const w = baseWorld('pace-c')
const c = w.state.members['x3']
c.realm = { major: 'qi', minor: 1 }
// 多次失败模拟,跌损应明显小于旧值:检查 resolveBreakthrough 失败后的 progress ≥ 55%
// 直接构造必失败环境(心性/健康低)
c.realmProgress = 100
c.health = 100
c.mind = 1
c.traits = ['jizao'] // 急躁加大失败伤害
resolveBreakthrough(w, c, -0.4)
if (c.realm.minor === 1) {
// 若失败在层内,跌损后应 ≥ 45%(旧 40-65 可能到 35%
expect(c.realmProgress).toBeGreaterThanOrEqual(45)
}
})
it('渡劫成功率上调(筑基期望>0.5)', () => {
const w = baseWorld('trib-rate')
const c = w.state.members['x5']
c.realm = { major: 'qi', minor: 8 }
c.mind = 6
c.health = 100
expect(perTribChance(w, c)).toBeGreaterThan(0.5)
})
})
import { resolveBreakthrough } from '../src/renderer/game/engine/systems/cultivation'
import { perTribChance } from '../src/renderer/game/engine/systems/tribulation'
describe('事件悬置与周期(0.1.10', () => {
it('大比征召 FIFO 不被拍卖/传薪挤掉(20 年三年都触发)', () => {
const w = World.create({ seed: 'tourney-q', surname: '龚', familyName: '龚家', motto: 'm', difficulty: 'normal' })
resetSaveBus()
attachLogSink(w)
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('渡劫玩家处理能晋升(qi8→筑基)', () => {
const w = World.create({ seed: 'trib-jump', surname: '桑', familyName: '桑家', motto: 'm', difficulty: 'normal' })
const c = w.state.members['x5']
c.realm = { major: 'qi', minor: 8 }
c.realmProgress = 100
c.mind = 9
c.perception = 9
for (let i = 0; i < 60; i++) {
if (w.state.pendingEvent) { applyEventChoice(w, w.state.pendingEvent, 0); w.state.pendingEvent = undefined }
w.advanceMonth()
if (c.realm.major === 'foundation') break
}
expect(c.realm.major).toBe('foundation')
})
})
+5 -1
View File
@@ -2,6 +2,7 @@ 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 长跑矩阵', () => {
@@ -99,8 +100,11 @@ describe('系统级固定周期事件', () => {
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()
if (w.state.pendingEvent) w.state.pendingEvent = undefined
}
expect(seen).toContain('ev-tournament-10')
expect(seen).toContain('ev-tournament-15')