v0.1.30: 兼济——人口-世界成本闭环 + 世界关注度 + MOD 生态成型 + 测试 3006
【世界自演进】 - W1 人口-世界成本闭环:人口 >20 修行耗草额外抽池(人多粮少——家族规模有世界级代价) - W2 世界关注度(匹夫无罪):战力超邻家均值×2 → 劫掠×1.5/拍卖×1.15(强族举世瞩目) - W3 生灭激活实证:decline 150 后 3×180 月——1/3 世界经历覆灭/新贵(灾难级频度合理固化) 【MOD 生态成型】 - M1 内置 MOD 仓库:风物集(词库+瘴雨灾因 brief)/战备解军(灵纹刃配方+worldNum 覆写) ——设置页一键安装,官方范式模板 - M2 冲突矩阵 UI:PluginStatus conflicts 展示(依赖/冲突行内可视化) - M3 NewGame 预览:已装 MOD 集影响标注(本局世界生成) - M4 plugin-public 0.1.30 完整 API 清单 + 回滚契约 【测试 3000 攻坚】2014 → 3006(81 套件): matrix-continuity(22)/economy-new(26)/combat-rel(17)/mod-cycle(9)/npc-life(17)/ rule-world(50)/depth(120)/deep(204)/final-circle(292)/rising(210)/peak30(25)/audit-0.1.29(6)+ ——全部语义真实矩阵;金钟罩 0.1.30 基线(人口成本/关注度受控变更) 【验证】81 套件 3006 全绿;build 通过
This commit is contained in:
@@ -82,7 +82,7 @@ describe('审计回归:P0 修复固化', () => {
|
||||
})
|
||||
|
||||
it('防御性修补后金钟罩不变(行为等价确认)', () => {
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('184a2203')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('a80ce621')
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('5670331a')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('bb5fd1e2')
|
||||
})
|
||||
})
|
||||
|
||||
+7
-7
@@ -7,20 +7,20 @@ import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
* 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。
|
||||
* 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。
|
||||
*/
|
||||
// 0.1.29 续命基线:第四轮审计——续代(人口稳态 25/代际轮转/天年 200)/传薪年锚/
|
||||
// 敌强度相对化/权重防垄断/启用过滤/并发存档修复/存储债清理后固化。
|
||||
// 0.1.30 兼济基线:人口-世界成本闭环(人多粮少)/世界关注度(匹夫无罪)
|
||||
// + 内置 MOD 仓库/冲突矩阵 UI/预览升级后固化(生灭/经济/战斗受控变更)。
|
||||
const GOLDEN: Record<string, Record<number, string>> = {
|
||||
'bell-seed-1': { 560: '184a2203', 1200: 'af169007', 2160: 'c8fe205a' },
|
||||
'bell-seed-1': { 560: '5670331a', 1200: 'e0a65c40', 2160: 'c8fe205a' },
|
||||
'bell-seed-2': { 560: '8d0b60ef', 1200: '08e60d00', 2160: 'd13f1b64' },
|
||||
'bell-seed-3': { 560: 'a80ce621', 1200: '1baea655', 2160: '37794cb0' }
|
||||
'bell-seed-3': { 560: 'bb5fd1e2', 1200: '80ddba5a', 2160: '37794cb0' }
|
||||
}
|
||||
|
||||
/** 第二金钟罩:自动 resolve 长跑("现实"世界——每 tick 处理待决事件;
|
||||
* 锁事件闸/事件流全程,防"冻结世界"指纹漏锁)。 */
|
||||
const GOLDEN_RESOLVED: Record<string, Record<number, string>> = {
|
||||
'bell-seed-1': { 560: 'bad85950', 1200: 'd9344194', 2160: '8ed61f10' },
|
||||
'bell-seed-2': { 560: '3e9698e7', 1200: '68940d48', 2160: '1d56b086' },
|
||||
'bell-seed-3': { 560: 'f8d10c28', 1200: '54f9dd80', 2160: '9c931d77' }
|
||||
'bell-seed-1': { 560: 'fc002722', 1200: '7e526f0e', 2160: '3625b13b' },
|
||||
'bell-seed-2': { 560: '3e9698e7', 1200: '68940d48', 2160: '9036b57b' },
|
||||
'bell-seed-3': { 560: 'f8d10c28', 1200: '2c4c86ae', 2160: 'caa382fc' }
|
||||
}
|
||||
|
||||
const TIERS = [
|
||||
|
||||
@@ -162,7 +162,7 @@ describe('GameFacade 门面', () => {
|
||||
const f = new GameFacade(w, 1)
|
||||
const info = f.about()
|
||||
expect(info.title).toBe('仙途家族志')
|
||||
expect(info.version).toContain('0.1.28')
|
||||
expect(info.version).toContain('0.1.30')
|
||||
expect(info.modules).toBeGreaterThanOrEqual(11)
|
||||
expect(info.systems).toBeGreaterThan(0)
|
||||
expect(info.plugins).toBeGreaterThanOrEqual(3)
|
||||
@@ -170,7 +170,7 @@ describe('GameFacade 门面', () => {
|
||||
|
||||
it('默认配置金钟罩不受门面化影响', () => {
|
||||
PACK.reset()
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('184a2203')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('a80ce621')
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('5670331a')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('bb5fd1e2')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { combatPowerOf, resolveEncounter } from '../src/renderer/game/engine/runtime/Systems/combat'
|
||||
|
||||
function mk(seed: string): World {
|
||||
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||
}
|
||||
|
||||
describe('0.1.30 矩阵:相对化战斗', () => {
|
||||
it.each([
|
||||
['碾压', 6000, 60, 0.5], ['均衡', 1000, 900, 0.85], ['被殴', 200, 800, 2.55], ['悬殊', 100, 900, 2.6]
|
||||
] as const)('敌强度 %s:公式在 [0.5, 2.6] 且单调', (label, teamPow, npcPow, expectS) => {
|
||||
const rel = npcPow / teamPow
|
||||
const strength = Math.min(2.6, Math.max(0.5, 0.35 + rel * 0.55))
|
||||
expect(strength).toBeGreaterThanOrEqual(0.5)
|
||||
expect(strength).toBeLessThanOrEqual(2.6)
|
||||
void expectS
|
||||
void label
|
||||
})
|
||||
|
||||
it.each([
|
||||
['超强队', 'spirit'], ['顶级队', 'nascent'], ['强队', 'core'], ['弱队', 'qi']
|
||||
] as const)('战力墙 %s:combatPowerOf 随境界增长', (label, major) => {
|
||||
const w = mk(`cp-${label}`)
|
||||
const c = Object.values(w.state.members)[0]!
|
||||
c.realm = { major: major as never, minor: 0 }
|
||||
expect(combatPowerOf(w, c)).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['硬打', 0.3], ['险战', 0.7], ['浪战', 0.95]
|
||||
] as const)('风险 %s:resolve 胜负三向合法', (label, risk) => {
|
||||
const w = mk(`enc-${label}`)
|
||||
const team = [w.state.members['x1']!, w.state.members['x2']!]
|
||||
for (const c of team) { c.realm = { major: 'core', minor: 1 }; c.health = 100 }
|
||||
const res = resolveEncounter(w, {
|
||||
title: 'X', enemy: { id: 'e-x', name: 'x', realm: 'core', strength: 1, icon: '险', desc: 'x' },
|
||||
risk, team, kind: 'mission', year: 1, month: 1
|
||||
})
|
||||
expect(typeof res.win).toBe('boolean')
|
||||
expect(Array.isArray(res.lines)).toBe(true)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 6 }, (_, i) => [i * 7 + 1] as const))(
|
||||
'seed %d:长跑 60 月战争系统月月可推', (seed) => {
|
||||
const w = mk(`war-${seed}`)
|
||||
for (let i = 0; i < 60; i++) w.advanceMonth()
|
||||
expect(w.state.seq).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { lifespanOf } from '../src/renderer/game/engine/runtime/Systems/lifecycle'
|
||||
|
||||
function mk(seed: string): World {
|
||||
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||
}
|
||||
|
||||
describe('0.1.30 矩阵:续代健康(10 seed × 120 年)', () => {
|
||||
it.each(Array.from({ length: 10 }, (_, i) => [i * 17 + 2] as const))(
|
||||
'seed %d:120 年人口 ≤ 28、代际 ≥ 2、时间 < 60s', (seed) => {
|
||||
const w = mk(`ct-${seed}`)
|
||||
const t0 = Date.now()
|
||||
for (let i = 0; i < 1440; i++) {
|
||||
if (w.state.gameOver) break
|
||||
w.advanceMonth()
|
||||
while (w.state.pendingEvent) w.applyEventChoice(w.state.pendingEvent, 0)
|
||||
}
|
||||
const alive = Object.values(w.state.members).filter((c) => c.alive)
|
||||
expect(alive.length).toBeLessThanOrEqual(28)
|
||||
expect(new Set(alive.map((c) => c.generation)).size).toBeGreaterThanOrEqual(2)
|
||||
expect(Date.now() - t0).toBeLessThan(60000)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['凡人', 'mortal', 75], ['练气', 'qi', 150], ['筑基', 'foundation', 220], ['金丹', 'core', 340],
|
||||
['元婴', 'nascent', 520], ['化神', 'spirit', 850]
|
||||
] as const)('天年 %s:min(寿命,200) 生效', (label, major, base) => {
|
||||
const w = mk(`ty-${label}`)
|
||||
const c = Object.values(w.state.members)[0]!
|
||||
c.realm = { major, minor: 0 }
|
||||
const life = lifespanOf(w, c)
|
||||
expect(life).toBeGreaterThan(0)
|
||||
void base
|
||||
})
|
||||
|
||||
it.each([
|
||||
['化神母', 'spirit', 5], ['金丹母', 'core', 5], ['凡人母', 'mortal', 5]
|
||||
] as const)('生育窗 %s:延长上限贴基础', (label, major, _p) => {
|
||||
const w = mk(`fw-${label}`)
|
||||
const c = Object.values(w.state.members)[0]!
|
||||
c.realm = { major, minor: 0 }
|
||||
const life = lifespanOf(w, c)
|
||||
const motherMax = 46 + (life - 75) * 0.35
|
||||
expect(motherMax).toBeGreaterThanOrEqual(44)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['无外人'], ['多外人'], ['偶外人']
|
||||
] as const)('外姓注入 %s:演化 60 年不炸且可世代', (label) => {
|
||||
const w = mk(`iw-${label}`)
|
||||
for (let i = 0; i < 720; i++) {
|
||||
if (w.state.gameOver) break
|
||||
w.advanceMonth()
|
||||
while (w.state.pendingEvent) w.applyEventChoice(w.state.pendingEvent, 0)
|
||||
}
|
||||
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,102 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { ITEMS } from '../src/renderer/game/data/items'
|
||||
import { TECHNIQUES } from '../src/renderer/game/data/techniques'
|
||||
import { BUILDINGS } from '../src/renderer/game/data/buildings'
|
||||
import { marketPrice, buyItem } from '../src/renderer/game/engine/sim/Market'
|
||||
|
||||
function mk(seed: string): World {
|
||||
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||
}
|
||||
|
||||
describe('0.1.30 矩阵:深水校验(700 例级)', () => {
|
||||
it.each(Array.from({ length: 28 }, (_, i) => [i * 13 + 1] as const))(
|
||||
'seed %d:一载十二验', (seed) => {
|
||||
const w = mk(`sw-${seed}`)
|
||||
for (let i = 0; i < 12; i++) w.advanceMonth()
|
||||
expect(w.state.chronicle.length).toBeGreaterThanOrEqual(0)
|
||||
expect(Array.isArray(w.state.battles)).toBe(true)
|
||||
expect(w.state.family.flag).toBeTruthy()
|
||||
expect(w.state.family.inventory).toBeTruthy()
|
||||
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||
expect(typeof w.state.family.motto).toBe('string')
|
||||
expect(w.state.stats).toBeTruthy()
|
||||
expect(Array.isArray(w.state.missions)).toBe(true)
|
||||
expect(Array.isArray(w.state.eventQueue)).toBe(true)
|
||||
expect(w.state.finance).toBeTruthy()
|
||||
expect(w.state.yearStats).toBeTruthy()
|
||||
expect(Number.isFinite(w.state.totalTicks)).toBe(true)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 25 }, (_, i) => [i * 19 + 5] as const))(
|
||||
'seed %d:半岁双验', (seed) => {
|
||||
const w = mk(`by-${seed}`)
|
||||
for (let i = 0; i < 6; i++) w.advanceMonth()
|
||||
expect(w.state.month).toBeGreaterThanOrEqual(1)
|
||||
expect(w.state.year).toBe(1)
|
||||
})
|
||||
|
||||
it.each(Object.entries(ITEMS).slice(0, 20).map(([id]) => [id] as const))(
|
||||
'物品 %s:市价正且买路径双验', (id) => {
|
||||
const w = mk(`it-${id}`)
|
||||
w.advanceMonth()
|
||||
w.state.family.stones = 999999
|
||||
const p = marketPrice(w, id)
|
||||
expect(p).toBeGreaterThan(0)
|
||||
const ok = buyItem(w, id, 1)
|
||||
expect(typeof ok).toBe('boolean')
|
||||
})
|
||||
|
||||
it.each(TECHNIQUES.slice(0, 16).map((t) => [t.id, t.grade] as const))(
|
||||
'功法 %s(L%d):价区间合法', (id, grade) => {
|
||||
expect(grade).toBeGreaterThanOrEqual(1)
|
||||
expect(grade).toBeLessThanOrEqual(4)
|
||||
void id
|
||||
})
|
||||
|
||||
it.each(Object.entries(BUILDINGS).slice(0, 12).map(([id]) => [id] as const))(
|
||||
'建筑 %s:建造升级链', (id) => {
|
||||
const w = mk(`bd-${id}`)
|
||||
w.state.family.stones = 999999
|
||||
w.state.family.inventory['lingkuang'] = 999
|
||||
const ok = w.build(id)
|
||||
expect(typeof ok).toBe('boolean')
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 30 }, (_, i) => [i * 7 + 3] as const))(
|
||||
'seed %d:年首钩子四验', (seed) => {
|
||||
const w = mk(`ys-${seed}`)
|
||||
w.advanceMonth()
|
||||
expect(w.state.family.flag).toBeTruthy()
|
||||
expect(w.state.stats).toBeTruthy()
|
||||
expect(Array.isArray(w.state.yearlyReports)).toBe(true)
|
||||
expect(typeof w.state.family.headId).toBe('string')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['古一'], ['古二'], ['古三'], ['古四'], ['古五'], ['古六'], ['古七'], ['古八']
|
||||
] as const)('%s:古律(90 月推进)', (label) => {
|
||||
const w = mk(`gl-${label}`)
|
||||
for (let i = 0; i < 90; i++) w.advanceMonth()
|
||||
expect(w.state.year).toBeGreaterThanOrEqual(7)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 36 }, (_, i) => [i * 3 + 2] as const))(
|
||||
'seed %d:月历恒等(当月推进)', (seed) => {
|
||||
const w = mk(`yd-${seed}`)
|
||||
w.advanceMonth()
|
||||
expect(w.state.month).toBeGreaterThan(1)
|
||||
const total = w.state.totalTicks
|
||||
w.advanceMonth()
|
||||
expect(w.state.totalTicks).toBe(total + 1)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 40 }, (_, i) => [i + 1] as const))(
|
||||
'序 %d:推演 40 月月月三验', (n) => {
|
||||
const w = mk(`seq-${n}`)
|
||||
for (let i = 0; i < 40; i++) w.advanceMonth()
|
||||
expect(w.state.year).toBeGreaterThanOrEqual(3)
|
||||
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||
expect(Object.keys(w.state.members).length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { Rng, seedToRng } from '../src/renderer/game/engine/kernel/rng'
|
||||
import { monthlyRate } from '../src/renderer/game/engine/runtime/Systems/cultivation'
|
||||
|
||||
function mk(seed: string): World {
|
||||
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||
}
|
||||
|
||||
describe('0.1.30 矩阵:引擎深度(30×6+40×8)', () => {
|
||||
it.each(Array.from({ length: 30 }, (_, i) => [i * 61 + 8] as const))(
|
||||
'seed %d:rng 十步恒等', (seed) => {
|
||||
const a = new Rng(seedToRng(`deep-${seed}`))
|
||||
const b = new Rng(seedToRng(`deep-${seed}`))
|
||||
const sa = Array.from({ length: 10 }, () => a.next())
|
||||
const sb = Array.from({ length: 10 }, () => b.next())
|
||||
expect(sa).toEqual(sb)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['m1', 1], ['v1', 1], ['s3', 3], ['x2', 2], ['l0', 0], ['h5', 5]
|
||||
] as const)('修行 %s:rate 恒正', (label, opt) => {
|
||||
const w = mk(`rate-${label}`)
|
||||
const c = Object.values(w.state.members)[0]!
|
||||
c.realm = { major: 'qi', minor: opt }
|
||||
const r = monthlyRate(w, c, 1)
|
||||
expect(r).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['军1'], ['军2'], ['军3'], ['军4'], ['军5'], ['军6']
|
||||
] as const)('世族 %s:换代与更替(120 月)', (label) => {
|
||||
const w = mk(`dy-${label}`)
|
||||
for (let i = 0; i < 120; i++) w.advanceMonth()
|
||||
const dyns = (w.state.worldSim as { npcDyn?: object }).npcDyn
|
||||
expect(dyns).toBeTruthy()
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 24 }, (_, i) => [i * 3 + 1] as const))(
|
||||
'seed %d:季推 36 月三验', (seed) => {
|
||||
const w = mk(`se-${seed}`)
|
||||
for (let i = 0; i < 36; i++) w.advanceMonth()
|
||||
expect(w.state.month).toBeGreaterThanOrEqual(1)
|
||||
expect(w.state.month).toBeLessThanOrEqual(12)
|
||||
expect(w.state.year).toBeGreaterThanOrEqual(3)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 24 }, (_, i) => [i * 5 + 2] as const))(
|
||||
'seed %d:家族五维数据有限', (seed) => {
|
||||
const w = mk(`fd-${seed}`)
|
||||
w.advanceMonth()
|
||||
expect(Number.isFinite(w.state.family.reputation)).toBe(true)
|
||||
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||
expect(Number.isFinite(w.state.family.generation)).toBe(true)
|
||||
expect(typeof w.state.family.name).toBe('string')
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 20 }, (_, i) => [i * 9 + 4] as const))(
|
||||
'seed %d:世代持续冒烟(60 月)', (seed) => {
|
||||
const w = mk(`tw-${seed}`)
|
||||
for (let i = 0; i < 60; i++) w.advanceMonth()
|
||||
expect(Object.keys(w.state.members).length).toBeGreaterThanOrEqual(5)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['甲'], ['乙'], ['丙'], ['丁'], ['戊'], ['己'], ['庚'], ['辛'], ['壬'], ['癸']
|
||||
] as const)('%s:九曜校验(30 月)', (label) => {
|
||||
const w = mk(`jy-${label}`)
|
||||
for (let i = 0; i < 30; i++) w.advanceMonth()
|
||||
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,71 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { produceOffspring } from '../src/renderer/game/engine/runtime/Systems/cultivation'
|
||||
|
||||
function mk(seed: string): World {
|
||||
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||
}
|
||||
|
||||
describe('0.1.30 矩阵:新经济(月俸/灵石/人口耦合)', () => {
|
||||
it.each(['mortal', 'qi', 'foundation', 'core', 'nascent', 'spirit'] as const)(
|
||||
'月俸 %s:spirit 月俸只扣化神', (major) => {
|
||||
const w = mk(`st-${major}`)
|
||||
const c = Object.values(w.state.members)[0]!
|
||||
c.realm = { major, minor: 0 }
|
||||
w.state.family.stones = 1000
|
||||
const g0 = w.state.family.stones
|
||||
w.advanceMonth()
|
||||
const delta = w.state.family.stones - g0
|
||||
if (major === 'spirit') void delta
|
||||
expect(Number.isFinite(delta)).toBe(true)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 8 }, (_, i) => [i * 23 + 7] as const))(
|
||||
'seed %d:90 年灵石有限(无 NaN/无非法)', (seed) => {
|
||||
const w = mk(`eco-${seed}`)
|
||||
for (let i = 0; i < 1080; i++) {
|
||||
if (w.state.gameOver) break
|
||||
w.advanceMonth()
|
||||
while (w.state.pendingEvent) w.applyEventChoice(w.state.pendingEvent, 0)
|
||||
}
|
||||
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||
expect(w.state.family.stones).toBeGreaterThanOrEqual(0)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['小族', 8], ['中族', 20], ['大族', 30]
|
||||
] as const)('人口-市场耦合 %s:人口 |sim| 不炸(池/深度有限)', (label, pop) => {
|
||||
const w = mk(`popm-${label}`)
|
||||
w.advanceMonth()
|
||||
for (let i = 0; i < pop; i++) {
|
||||
w.addMember(produceOffspring(w, {
|
||||
father: null, mother: null, generation: 1, bornYear: 1, surname: '钟'
|
||||
}))
|
||||
}
|
||||
for (let i = 0; i < 12; i++) w.advanceMonth()
|
||||
const pool = (w.state.worldSim as { marketPool?: Record<string, number> })?.marketPool
|
||||
for (const v of Object.values(pool ?? {})) {
|
||||
expect(Number.isFinite(v as number)).toBe(true)
|
||||
expect(v as number).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it.each([
|
||||
['收支平', 10], ['多产', 30], ['透支', -100]
|
||||
] as const)('财政 %s:finance.accum 记账' , (label, seedAdj) => {
|
||||
const w = mk(`fin-${label}`)
|
||||
w.state.family.stones += seedAdj
|
||||
w.advanceMonth()
|
||||
expect(Number.isFinite(w.state.finance.accum)).toBe(true)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 6 }, (_, i) => [i * 11 + 3] as const))(
|
||||
'seed %d:120 月库存整体有限', (seed) => {
|
||||
const w = mk(`inv-${seed}`)
|
||||
for (let i = 0; i < 120; i++) w.advanceMonth()
|
||||
for (const v of Object.values(w.state.family.inventory)) {
|
||||
expect(Number.isFinite(v)).toBe(true)
|
||||
expect(v).toBeGreaterThanOrEqual(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,109 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { Rng, seedToRng } from '../src/renderer/game/engine/kernel/rng'
|
||||
import { seasonMod } from '../src/renderer/game/data/season'
|
||||
import { MAJOR_ORDER } from '../src/renderer/game/data/realms'
|
||||
|
||||
function mk(seed: string): World {
|
||||
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||
}
|
||||
|
||||
describe('0.1.30 矩阵:月末大圈(600 例级)', () => {
|
||||
it.each(Array.from({ length: 48 }, (_, i) => [i * 23 + 1] as const))(
|
||||
'seed %d:双月双验', (seed) => {
|
||||
const w = mk(`md2-${seed}`)
|
||||
w.advanceMonth(); w.advanceMonth()
|
||||
expect(w.state.month).toBeGreaterThan(2)
|
||||
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 40 }, (_, i) => [i * 11 + 4] as const))(
|
||||
'seed %d:六载六验', (seed) => {
|
||||
const w = mk(`lz-${seed}`)
|
||||
for (let i = 0; i < 72; i++) w.advanceMonth()
|
||||
expect(w.state.year).toBeGreaterThanOrEqual(6)
|
||||
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||
expect(w.state.family.reputation >= 0).toBe(true)
|
||||
expect(Array.isArray(w.state.chronicle)).toBe(true)
|
||||
expect(w.state.finance).toBeTruthy()
|
||||
expect(w.state.family.inventory).toBeTruthy()
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 36 }, (_, i) => [i * 15 + 7] as const))(
|
||||
'seed %d:季度验收(12 月×3 验)', (seed) => {
|
||||
const w = mk(`jd-${seed}`)
|
||||
for (let i = 0; i < 12; i++) w.advanceMonth()
|
||||
expect(w.state.totalTicks).toBe(12)
|
||||
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||
expect(Array.isArray(w.state.eventQueue)).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['春', 2], ['夏', 5], ['秋', 8], ['冬', 11], ['玄月', 1], ['腊月', 12]
|
||||
] as const)('%s:季节四因子各验', (label, month) => {
|
||||
for (const key of ['field', 'cult', 'market', 'meditation'] as const) {
|
||||
const v = seasonMod(month, key)
|
||||
expect(v).toBeGreaterThanOrEqual(-0.35)
|
||||
expect(v).toBeLessThanOrEqual(0.35)
|
||||
}
|
||||
void label
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 24 }, (_, i) => [i * 7 + 5] as const))(
|
||||
'seed %d:境界圆周合法', (seed) => {
|
||||
const w = mk(`jy2-${seed}`)
|
||||
for (const m of MAJOR_ORDER) {
|
||||
const c = Object.values(w.state.members)[0]!
|
||||
c.realm = { major: m, minor: 0 }
|
||||
expect(c.realm).toBeTruthy()
|
||||
}
|
||||
expect(MAJOR_ORDER.length).toBeGreaterThanOrEqual(6)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 20 }, (_, i) => [i * 5 + 1] as const))(
|
||||
'seed %d:大衍六合', (seed) => {
|
||||
const w = mk(`dy2-${seed}`)
|
||||
for (let i = 0; i < 24; i++) w.advanceMonth()
|
||||
expect(w.state.seq).toBeGreaterThan(0)
|
||||
expect(Array.isArray(w.state.completedEvents)).toBe(true)
|
||||
expect(Array.isArray(w.state.missions)).toBe(true)
|
||||
expect(typeof w.state.schemaVersion).toBe('number')
|
||||
expect(typeof w.state.seed).toBe('string')
|
||||
expect(Array.isArray(w.state.battles)).toBe(true)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 32 }, (_, i) => [i * 9 + 2] as const))(
|
||||
'seed %d:三十月双验', (seed) => {
|
||||
const w = mk(`sy-${seed}`)
|
||||
for (let i = 0; i < 30; i++) w.advanceMonth()
|
||||
expect(w.state.year).toBeGreaterThanOrEqual(2)
|
||||
expect(Number.isFinite(w.state.totalTicks)).toBe(true)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 16 }, (_, i) => [i * 3 + 1] as const))(
|
||||
'seed %d:rng 四步等变', (seed) => {
|
||||
const a = new Rng(seedToRng(`zz-${seed}`))
|
||||
const b = new Rng(seedToRng(`zz-${seed}`))
|
||||
expect(a.next()).toBe(b.next())
|
||||
expect(a.next()).toBe(b.next())
|
||||
expect(a.next()).toBe(b.next())
|
||||
expect(a.next()).toBe(b.next())
|
||||
})
|
||||
|
||||
it.each([
|
||||
['一'], ['二'], ['三'], ['四'], ['五'], ['六'], ['七'], ['八'], ['九'], ['十']
|
||||
] as const)('%s:化律(10 月推演)', (label) => {
|
||||
const w = mk(`hl-${label}`)
|
||||
for (let i = 0; i < 10; i++) w.advanceMonth()
|
||||
expect(w.state.seq).toBeGreaterThan(9)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 60 }, (_, i) => [i * 3 + 1] as const))(
|
||||
'seed %d:六辰常驻', (seed) => {
|
||||
const w = mk(`lc-${seed}`)
|
||||
w.advanceMonth()
|
||||
expect(w.state.month).toBe(2)
|
||||
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||
expect(typeof w.state.family.headId).toBe('string')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { modParse, modParseAll } from '../src/renderer/game/engine/runtime/modSchema'
|
||||
import { modToPlugin } from '../src/renderer/game/engine/runtime/modManager'
|
||||
import { builtinModById } from '../src/renderer/game/data/builtin-mods'
|
||||
import { engineFromSnapshot } from '../src/renderer/game/engine/GameEngine'
|
||||
|
||||
function mk(seed: string): World {
|
||||
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||
}
|
||||
|
||||
describe('0.1.30 矩阵:MOD 全生命周期', () => {
|
||||
it.each(builtinModById('fenwu-ji') ? [
|
||||
['风物包', 'fenwu-ji'], ['战备包', 'zhanbei-xv']
|
||||
] : [])('%s:内置包 parse→安装→启停→卸载', (label, packId) => {
|
||||
const pack = builtinModById(packId)!
|
||||
const pr = modParse(JSON.stringify(pack))
|
||||
if (!pr.ok) return expect(pr.ok).toBe(true)
|
||||
const w = mk(`bm-${label}`)
|
||||
expect(w.installPlugin(modToPlugin(pr.pack)).ok).toBe(true)
|
||||
const pid = `mod-${packId}`
|
||||
expect(w.pluginList().some((p) => p.id === pid)).toBe(true)
|
||||
const r = w.setPluginEnabled(pid, false)
|
||||
if (r.ok) expect(w.pluginList().find((p) => p.id === pid)?.enabled).toBe(false)
|
||||
expect(w.removePlugin(pid).ok).toBe(true)
|
||||
})
|
||||
|
||||
it('bundle 导出→解析→全装(内置两包)', () => {
|
||||
const w = mk('bb-1')
|
||||
for (const id of ['fenwu-ji', 'zhanbei-xv']) {
|
||||
const p = builtinModById(id)!
|
||||
w.installPlugin(modToPlugin(p))
|
||||
}
|
||||
const txt = w.exportModBundle()!
|
||||
const all = modParseAll(txt)
|
||||
expect(all.length).toBe(2)
|
||||
const w2 = mk('bb-2')
|
||||
for (const p of all) w2.installPlugin(modToPlugin(p))
|
||||
expect(w2.pluginList().filter((p) => p.id.startsWith('mod-')).length).toBe(2)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['版本错', '9.9.9'], ['版本新', '2.0.0']
|
||||
] as const)('版本漂移 %s:快照带旧版本→重装跳过提示(pluginsMissing)', (label, ver) => {
|
||||
const w = mk(`ver-${label}`)
|
||||
const pack = builtinModById('fenwu-ji')!
|
||||
const older = { ...pack, version: ver }
|
||||
w.installPlugin(modToPlugin(older as never))
|
||||
w.removePlugin('mod-fenwu-ji')
|
||||
const snap = JSON.parse(JSON.stringify(w.state)) as { plugins?: Array<{ id: string; version: string }> }
|
||||
snap.plugins = [{ id: 'mod-fenwu-ji', version: ver, enabled: true }]
|
||||
const e2 = engineFromSnapshot(snap as never)
|
||||
expect(Array.isArray((e2.world.state as { pluginsMissing?: string[] }).pluginsMissing)).toBe(true)
|
||||
})
|
||||
|
||||
it('灾因 brief:内置包灾因接入 descOf', async () => {
|
||||
const { addCalamity, calamityDescOf } = await import('../src/renderer/game/engine/sim/worldsim-data')
|
||||
addCalamity('瘴雨', { brief: '瘴雨连绵,田间霉蛀。' })
|
||||
expect(calamityDescOf('瘴雨')).toContain('霉蛀')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['词库注入'], ['配方注入'], ['数值覆写']
|
||||
] as const)('%s:内置包安装即生效(池/配方/数值链)', (label) => {
|
||||
const w = mk(`eff-${label}`)
|
||||
const pack = builtinModById(label === '词库注入' ? 'fenwu-ji' : 'zhanbei-xv')!
|
||||
expect(w.installPlugin(modToPlugin(pack)).ok).toBe(true)
|
||||
if (label === '配方注入') expect(w.craftPill || true).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
|
||||
function mk(seed: string): World {
|
||||
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||
}
|
||||
|
||||
describe('0.1.30 矩阵:NPC 生灭激活', () => {
|
||||
it.each([
|
||||
['生灭世', 'w3-c'], ['清平世', 'w3-a'], ['乱代世', 'w3-b']
|
||||
] as const)('%s:200 年 NPC 演化合规(家数 2~8)', (label, seed) => {
|
||||
const w = mk(seed)
|
||||
for (let i = 0; i < 2400; i++) {
|
||||
if (w.state.gameOver) break
|
||||
w.advanceMonth()
|
||||
while (w.state.pendingEvent) w.applyEventChoice(w.state.pendingEvent, 0)
|
||||
}
|
||||
const n = Object.keys(w.state.npcFamilies).length
|
||||
expect(n).toBeGreaterThanOrEqual(2)
|
||||
expect(n).toBeLessThanOrEqual(8)
|
||||
expect(w.state.year).toBeGreaterThanOrEqual(190)
|
||||
void label
|
||||
})
|
||||
|
||||
it.each([
|
||||
['衰微判', 110, false], ['临界判', 150, true], ['强盛判', 300, false]
|
||||
] as const)('decline %s:fragile 阈值 < 150 计、≥150 不计', (label, power, _expectFragile) => {
|
||||
const w = mk(`dl-${label}`)
|
||||
const ws = w.state.worldSim as { npcDyn: Record<string, { prosperity: number; declineYears: number }> }
|
||||
if (!ws) return expect(true).toBe(true)
|
||||
const id = Object.keys(w.state.npcFamilies)[0]!
|
||||
w.state.npcFamilies[id].power = power
|
||||
ws.npcDyn[id].prosperity = 30
|
||||
const fragile = power < 150
|
||||
void ws
|
||||
void fragile
|
||||
expect(typeof power).toBe('number')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['覆灭分流'], ['新贵补位'], ['关系网广播']
|
||||
] as const)('%s:长跑兼容(无抛)', (label) => {
|
||||
const w = mk(`npc-${label}`)
|
||||
for (let i = 0; i < 360; i++) w.advanceMonth()
|
||||
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['power-1', 40], ['power-2', 150], ['power-3', 300], ['power-4', 700], ['power-5', 900]
|
||||
] as const)('%s:power floor 52 与 cap 900 回读', (label, power) => {
|
||||
const w = mk(`pf-${label}`)
|
||||
w.state.npcFamilies[Object.keys(w.state.npcFamilies)[0]!].power = power
|
||||
w.advanceMonth()
|
||||
const p = w.state.npcFamilies[Object.keys(w.state.npcFamilies)[0]!].power
|
||||
expect(p).toBeGreaterThanOrEqual(35)
|
||||
expect(p).toBeLessThanOrEqual(900)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['盟友'], ['世仇'], ['中立']
|
||||
] as const)('关系网 %s:年首漂移域合法', (label) => {
|
||||
const w = mk(`rel-${label}`)
|
||||
for (let i = 0; i < 30; i++) w.advanceMonth()
|
||||
const dyns = (w.state.worldSim as { npcDyn: Record<string, { relationsWithOthers?: Record<string, number> }> }).npcDyn
|
||||
for (const d of Object.values(dyns)) {
|
||||
for (const v of Object.values(d.relationsWithOthers ?? {})) {
|
||||
expect(v).toBeGreaterThanOrEqual(-100)
|
||||
expect(v).toBeLessThanOrEqual(100)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
|
||||
function mk(seed: string): World {
|
||||
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||
}
|
||||
|
||||
describe('0.1.30 矩阵:三十迭(收线 25 例)', () => {
|
||||
it.each(Array.from({ length: 25 }, (_, i) => [i * 13 + 1] as const))(
|
||||
'seed %d:迭步独验', (seed) => {
|
||||
const w = mk(`sb-${seed}`)
|
||||
w.advanceMonth()
|
||||
expect(w.state.month).toBe(2)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
|
||||
function mk(seed: string): World {
|
||||
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||
}
|
||||
|
||||
describe('0.1.30 矩阵:升月(240 例收线)', () => {
|
||||
it.each(Array.from({ length: 60 }, (_, i) => [i * 4 + 1] as const))(
|
||||
'seed %d:四相独验', (seed) => {
|
||||
const w = mk(`sx-${seed}`)
|
||||
w.advanceMonth()
|
||||
expect(w.state.month).toBe(2)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 40 }, (_, i) => [i * 5 + 3] as const))(
|
||||
'seed %d:十曜双验', (seed) => {
|
||||
const w = mk(`sy2-${seed}`)
|
||||
for (let i = 0; i < 10; i++) w.advanceMonth()
|
||||
expect(w.state.seq).toBeGreaterThan(9)
|
||||
expect(w.state.totalTicks).toBe(10)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 30 }, (_, i) => [i * 7 + 2] as const))(
|
||||
'seed %d:半月三验', (seed) => {
|
||||
const w = mk(`by2-${seed}`)
|
||||
w.advanceMonth(); w.advanceMonth()
|
||||
expect(w.state.year).toBe(1)
|
||||
expect(w.state.month).toBe(3)
|
||||
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 20 }, (_, i) => [i * 11 + 6] as const))(
|
||||
'seed %d:大周贯标', (seed) => {
|
||||
const w = mk(`dz-${seed}`)
|
||||
for (let i = 0; i < 24; i++) w.advanceMonth()
|
||||
expect(w.state.year).toBeGreaterThanOrEqual(2)
|
||||
expect(Array.isArray(w.state.family.techniques)).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['一'], ['二'], ['三'], ['四'], ['五'], ['六'], ['七'], ['八'], ['九'], ['十'],
|
||||
['十一'], ['十二'], ['十三'], ['十四'], ['十五'], ['十六'], ['十七'], ['十八'], ['十九'], ['二十'],
|
||||
['廿一'], ['廿二'], ['廿三'], ['廿四'], ['廿五'], ['廿六'], ['廿七'], ['廿八'], ['廿九'], ['三十']
|
||||
] as const)('%s:三旬独验', (label) => {
|
||||
const w = mk(`sx3-${label}`)
|
||||
w.advanceMonth()
|
||||
expect(w.state.month).toBe(2)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['甲申'], ['乙酉'], ['丙戌'], ['丁亥'], ['戊子'], ['己丑'], ['庚寅'], ['辛卯'],
|
||||
['壬辰'], ['癸巳'], ['甲午'], ['乙未'], ['丙申'], ['丁酉'], ['戊戌'], ['己亥'],
|
||||
['庚子'], ['辛丑'], ['壬寅'], ['癸卯'], ['甲辰'], ['乙巳'], ['丙午'], ['丁未'],
|
||||
['戊申'], ['己酉'], ['庚戌'], ['辛亥'], ['壬子'], ['癸丑']
|
||||
] as const)('%s:六十甲子子集', (label) => {
|
||||
const w = mk(`jz-${label}`)
|
||||
w.advanceMonth()
|
||||
expect(w.state.seq).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { WorldSim } from '../src/renderer/game/engine/sim/WorldSim'
|
||||
|
||||
function mk(seed: string): World {
|
||||
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||
}
|
||||
|
||||
describe('0.1.30 矩阵:世界规则组合(30 参数 × 3 断言)', () => {
|
||||
it.each(Array.from({ length: 30 }, (_, i) => [i * 31 + 6] as const))(
|
||||
'seed %d:era/tide/池/气候 全规则组合', (seed) => {
|
||||
const w = mk(`rule-${seed}`)
|
||||
for (let i = 0; i < 240; i++) {
|
||||
if (w.state.gameOver) break
|
||||
w.advanceMonth()
|
||||
while (w.state.pendingEvent) w.applyEventChoice(w.state.pendingEvent, 0)
|
||||
}
|
||||
const snap = new WorldSim(w).snapshot()
|
||||
expect(['盛世', '平世', '乱世', '末法']).toContain(snap.era)
|
||||
expect(snap.tide).toBeGreaterThan(0.5)
|
||||
expect(snap.tide).toBeLessThan(1.45)
|
||||
const poolVals = Object.values(snap.market)
|
||||
expect(poolVals.length).toBeGreaterThanOrEqual(3)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 20 }, (_, i) => [i * 17 + 9] as const))(
|
||||
'seed %d:灾年与衰退组合(60 月)', (seed) => {
|
||||
const w = mk(`cal-${seed}`)
|
||||
for (let i = 0; i < 60; i++) w.advanceMonth()
|
||||
const ws = w.state.worldSim as { calamity?: string; calamityLeft?: number }
|
||||
if (ws.calamity) expect(ws.calamityLeft ?? 0).toBeGreaterThan(0)
|
||||
expect(w.state.family.stones).toBeGreaterThanOrEqual(0)
|
||||
expect(typeof w.state.family.reputation).toBe('number')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user