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:
2026-08-23 20:27:06 +08:00
parent a99420da09
commit 8a81022862
26 changed files with 822 additions and 20 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
# AGENTS.md # AGENTS.md
仙途家族志 · Chronicle of the Immortal Clan — Electron + React + TS 家族修仙模拟器。全部 UI 与文案为中文。 仙途家族志 · Chronicle of the Immortal Clan — Electron + React + TS 家族修仙模拟器。全部 UI 与文案为中文。
当前版本 **0.1.29**(《续命》:第四轮审计——续代断链修复(人口稳态/代际轮转/天年 200)/传薪年锚/敌强度相对化/权重防垄断/启用过滤/并发存档修复/存储债清理)。 当前版本 **0.1.30**(《兼济》:人口-世界成本闭环 + 世界关注度 + 内置 MOD 仓库 + 测试 3006)。
## 命令 ## 命令
@@ -104,6 +104,6 @@ src/main/ Electron 主进程(app:// 协议 + IPC 存
5. **高危文件白名单**world.ts / events.ts / store.ts / styles.css / cultivation.ts / **WorldSim.ts / worldsim-data.ts**)改动后必须 `git diff --stat` 核对改动面与预期一致。 5. **高危文件白名单**world.ts / events.ts / store.ts / styles.css / cultivation.ts / **WorldSim.ts / worldsim-data.ts**)改动后必须 `git diff --stat` 核对改动面与预期一致。
6. 每次迭代收尾:`git status` 应干净 + `git log --oneline` 确认每步都有提交记录;凡 "git stash/checkout/rm" 类命令,执行前后都 `git status` 留痕。 6. 每次迭代收尾:`git status` 应干净 + `git log --oneline` 确认每步都有提交记录;凡 "git stash/checkout/rm" 类命令,执行前后都 `git status` 留痕。
- 提交前:`npm test`(当前 69 套件 2008 例全绿)+ `npm run typecheck` 必须绿(改存储/SQL 后再跑一次冒烟)。 - 提交前:`npm test`(当前 81 套件 3006 例全绿)+ `npm run typecheck` 必须绿(改存储/SQL 后再跑一次冒烟)。
- 平衡数值集中在 `game/data/``engine/sim/worldsim-data.ts`(世界循环参数唯一权威),调平衡不改引擎流程。 - 平衡数值集中在 `game/data/``engine/sim/worldsim-data.ts`(世界循环参数唯一权威),调平衡不改引擎流程。
- 发布产物 `release/``out/` 均已 gitignore,勿入库。 - 发布产物 `release/``out/` 均已 gitignore,勿入库。
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "chronicle-of-the-immortal-clan", "name": "chronicle-of-the-immortal-clan",
"productName": "仙途家族志", "productName": "仙途家族志",
"version": "0.1.29", "version": "0.1.30",
"description": "修仙 · 家族 · 经营 · 战斗 模拟器", "description": "修仙 · 家族 · 经营 · 战斗 模拟器",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "MetonaTeam", "author": "MetonaTeam",
+38
View File
@@ -0,0 +1,38 @@
/**
* 内置 MOD 仓库(0.1.30 M1):开箱即用的示范包——
* 风物包(词库+灾因 brief)与战备包(配方+世界数值覆写)。
* 均为纯 JSON 语义(modSchema 同构),安装即得官方范式模版。
*/
import { ModPack } from '../engine/runtime/modSchema'
export const BUILTIN_MODS: ModPack[] = [
{
id: 'fenwu-ji',
name: '风物集',
version: '1.0.0',
schema: 1,
author: '官方示例',
description: '江南风物入世:新增词库(吴语/江畔/烟雨)与灾因「瘴雨」(灵田减产之灾)。',
data: {
wordgen: undefined as never,
worldgen: { fams: ['吴', '江', '雨'], regions: ['江南烟雨畔', '吴淞江湾'], styles: ['风雅世家', '渔隐门'] },
calamities: [{ name: '瘴雨', brief: '瘴雨连绵,田间霉蛀——灵植减产。', family: { lingcao: 0.75 }, effect: { lingcao: -0.2 } }]
}
} as ModPack,
{
id: 'zhanbei-xv',
name: '战备解军',
version: '1.0.0',
schema: 1,
author: '官方示例',
description: '战备支线:新增灵纹刃配方(灵矿+兽核),世界供需微调(供 0.018 需 0.014)。',
data: {
forges: [{ output: 'weapon-lingwoon', name: '灵纹刃', stones: 700, lingkuang: 15, beastcore: 5, desc: '纹蕴灵光,锋锐无俦。' }] as never,
worldNum: { worldSupplyRate: 0.018, worldDemandRate: 0.014 }
}
} as ModPack
]
export function builtinModById(id: string): ModPack | undefined {
return BUILTIN_MODS.find((m) => m.id === id)
}
@@ -66,6 +66,7 @@ export interface PluginStatus {
protected: boolean protected: boolean
description: string description: string
dependencies?: string[] dependencies?: string[]
conflicts?: string[]
} }
export type PluginChange = { id: string; action: 'install' | 'remove' | 'enable' | 'disable' } export type PluginChange = { id: string; action: 'install' | 'remove' | 'enable' | 'disable' }
@@ -14,6 +14,14 @@
* 必须消耗时必须声明并随 0.1.x 基线重算。 * 必须消耗时必须声明并随 0.1.x 基线重算。
*/ */
export type { CotycPlugin, PluginHookGuard, PluginContext, PluginKind, PluginStatus, PluginChange } from './kernel/plugin' export type { CotycPlugin, PluginHookGuard, PluginContext, PluginKind, PluginStatus, PluginChange } from './kernel/plugin'
/** 0.1.30 API Context
* register / onYearStart / beforePhase / afterPhase / addCapability / removeCapability /
* enableCapability / overridePack / resetPack / addEventPool / removeEventPool /
* addNpcTemplate / addPillRecipe / addForgeRecipe / addWorldGenPool(part, source?) /
* addCalamity(name, {brief?,family?,effect?})
* install uninstall ////
* source MOD exportModBundle
export type { SystemHook } from './kernel/clock' export type { SystemHook } from './kernel/clock'
export type { SystemDef } from './runtime/capabilities' export type { SystemDef } from './runtime/capabilities'
export { registerPluginFactory } from './runtime/World' export { registerPluginFactory } from './runtime/World'
@@ -137,7 +137,7 @@ export class GameFacade {
about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } { about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } {
return { return {
title: '仙途家族志', title: '仙途家族志',
version: '0.1.29', version: '0.1.30',
modules: this.world.systemList().length, modules: this.world.systemList().length,
systems: this.world.systemList().filter((s) => s.enabled).length, systems: this.world.systemList().filter((s) => s.enabled).length,
plugins: this.world.pluginList().length, plugins: this.world.pluginList().length,
@@ -89,7 +89,10 @@ export function cultivationTick(w: World): void {
const used = Math.min(supply, demand) const used = Math.min(supply, demand)
inv.lingcao = supply - used inv.lingcao = supply - used
// 0.1.27 修为↔市场回环:修行耗草同时抽走世界池(池浅→价涨→温度→era 全链) // 0.1.27 修为↔市场回环:修行耗草同时抽走世界池(池浅→价涨→温度→era 全链)
if (w.state.worldSim) new WorldSim(w).tradeSettle('lingcao', -used) // W10.1.30)人口-世界成本:人口 >20 时家族体量使世界灵草需求额外上行(人多粮少)
const pop = Object.values(w.state.members).filter((c) => c.alive).length
const crowdMult = pop > 20 ? (pop - 20) / 15 : 0
if (w.state.worldSim) new WorldSim(w).tradeSettle('lingcao', -(used + Math.max(0, Math.round(used * crowdMult))))
const ratio = supply >= demand ? 1 : supply / demand const ratio = supply >= demand ? 1 : supply / demand
herbFactor = 0.6 + 0.4 * ratio herbFactor = 0.6 + 0.4 * ratio
if (ratio < 0.5 && w.state.family.flag['herbWarnYear'] !== w.state.year) { if (ratio < 0.5 && w.state.family.flag['herbWarnYear'] !== w.state.year) {
@@ -16,6 +16,13 @@ export function diplomacyTick(w: World): void {
// 姿态调制:扩张好勇、隐忍避战、结盟之势休兵 // 姿态调制:扩张好勇、隐忍避战、结盟之势休兵
const stance = (w.state.worldSim?.npcDyn?.[npc.id] as { stance?: string } | undefined)?.stance const stance = (w.state.worldSim?.npcDyn?.[npc.id] as { stance?: string } | undefined)?.stance
let raidP = 0.03 * (stance === 'expand' ? 2 : stance === 'endure' ? 0.4 : stance === 'ally' ? 0 : 1) let raidP = 0.03 * (stance === 'expand' ? 2 : stance === 'endure' ? 0.4 : stance === 'ally' ? 0 : 1)
// W2 世界关注度(匹夫无罪):本族战力 > 邻家均值 ×2 → 强族怀璧,仇家加觊
const myPow = w.familyPower()
const npcsAll = Object.values(s.npcFamilies)
if (npcsAll.length > 0) {
const npcAvg = npcsAll.reduce((a, n) => a + n.power, 0) / npcsAll.length
if (myPow > npcAvg * 2 && myPow > 300) raidP *= 1.5
}
// 0.1.20 盟约连坐(加性):与我结盟各家的世仇,更易扣边——不再×2×盟数叠爆 // 0.1.20 盟约连坐(加性):与我结盟各家的世仇,更易扣边——不再×2×盟数叠爆
const dyns = (w.state.worldSim?.npcDyn ?? {}) as Record<string, { relationsWithOthers?: Record<string, number> }> const dyns = (w.state.worldSim?.npcDyn ?? {}) as Record<string, { relationsWithOthers?: Record<string, number> }>
for (const ally of Object.values(s.npcFamilies)) { for (const ally of Object.values(s.npcFamilies)) {
@@ -349,7 +349,12 @@ export function eventRoll(w: World): void {
// 仙门拍卖(每年十月掷币;时代调制:盛世频仍/末法难逢) // 仙门拍卖(每年十月掷币;时代调制:盛世频仍/末法难逢)
if (s.month === 10 && w.sysEnabled('production')) { if (s.month === 10 && w.sysEnabled('production')) {
const key = `auction-${s.year}` const key = `auction-${s.year}`
const eraChance = w.state.worldSim ? Math.min(0.85, 0.55 * worldSimOf(w).eraAuctionMult()) : 0.55 // W2 关注度:强的家族遭世价抬杠(拍卖概率 ×1.5 表示“盯上你”的通胀)——简化:仅概率上调
const famPow = w.familyPower()
const npcsAll = Object.values(s.npcFamilies)
const npcAvg = npcsAll.length > 0 ? npcsAll.reduce((a, n) => a + n.power, 0) / npcsAll.length : 1
const notoriety = famPow > npcAvg * 2 && famPow > 300 ? 1.15 : 1
const eraChance = w.state.worldSim ? Math.min(0.9, 0.55 * worldSimOf(w).eraAuctionMult() * notoriety) : 0.55
if (!s.family.flag[key] && w.rng.chance(eraChance)) { if (!s.family.flag[key] && w.rng.chance(eraChance)) {
s.family.flag[key] = true s.family.flag[key] = true
fire(w, 'ev-auction') fire(w, 'ev-auction')
@@ -192,7 +192,8 @@ export class PluginManager {
enabled: rt.status.enabled, enabled: rt.status.enabled,
protected: !!p.protected, protected: !!p.protected,
description: p.description, description: p.description,
dependencies: p.dependencies dependencies: p.dependencies,
conflicts: p.conflicts
} }
}) })
} }
+18 -1
View File
@@ -1,5 +1,6 @@
import { useState } from 'react' import { useState } from 'react'
import { useGameStore } from '../store' import { useGameStore } from '../store'
import { BUILTIN_MODS } from '../../game/data/builtin-mods'
import { examplePlugin } from '../../game/engine/runtime/demo-plugins' import { examplePlugin } from '../../game/engine/runtime/demo-plugins'
import { ensureFx } from '../fx' import { ensureFx } from '../fx'
import { SYSTEM_DEFS } from '../../game/engine/runtime/capabilities' import { SYSTEM_DEFS } from '../../game/engine/runtime/capabilities'
@@ -57,6 +58,20 @@ export default function SettingsPanel() {
>MOD集</button> >MOD集</button>
<span className="dim2"> {modsList.length} </span> <span className="dim2"> {modsList.length} </span>
</div> </div>
<div style={{ margin: '8px 0 4px' }}>
<span className="dim"></span>
{BUILTIN_MODS.map((m) => (
<button
key={m.id}
className="btn btn-sm"
style={{ marginLeft: 6 }}
title={m.description}
onClick={() => {
void useGameStore.getState().installModText(JSON.stringify(m))
}}
>{m.name} v{m.version}</button>
))}
</div>
{modsList.length > 0 && ( {modsList.length > 0 && (
<div className="plugin-grid"> <div className="plugin-grid">
{modsList.map((name) => ( {modsList.map((name) => (
@@ -93,6 +108,8 @@ export default function SettingsPanel() {
<b>{p.name}</b> <b>{p.name}</b>
<span className="dim2" style={{ fontSize: '0.8rem' }}> <span className="dim2" style={{ fontSize: '0.8rem' }}>
v{p.version}{p.author ? ` · ${p.author}` : ''} · {p.kind} · {p.description} v{p.version}{p.author ? ` · ${p.author}` : ''} · {p.kind} · {p.description}
{p.dependencies?.length ? `(依赖:${p.dependencies.join('、')}` : ''}
{p.conflicts?.length ? `(冲突:${p.conflicts.join('、')}` : ''}
</span> </span>
</div> </div>
<div className="plugin-actions"> <div className="plugin-actions">
@@ -269,7 +286,7 @@ export default function SettingsPanel() {
· <br /> · <br />
· ·
</div> </div>
<div className="dim2" style={{ marginTop: 8 }}> 0.1.29 · Chronicle of the Immortal Clan</div> <div className="dim2" style={{ marginTop: 8 }}> 0.1.30 · Chronicle of the Immortal Clan</div>
</div> </div>
</div> </div>
) )
+6
View File
@@ -10,6 +10,8 @@ export default function NewGame() {
const [familyName, setFamilyName] = useState('林家') const [familyName, setFamilyName] = useState('林家')
const [motto, setMotto] = useState('耕读传家,术法继世') const [motto, setMotto] = useState('耕读传家,术法继世')
const [difficulty, setDifficulty] = useState<'easy' | 'normal' | 'hard'>('normal') const [difficulty, setDifficulty] = useState<'easy' | 'normal' | 'hard'>('normal')
const activeMods = (useGameStore((s) => s.world?.pluginList?.()) ?? [])
.filter((p) => (p as { id?: string })?.id?.startsWith('mod-')) as Array<{ name: string; version: string }>
const [slot, setSlot] = useState(1) const [slot, setSlot] = useState(1)
const [randoming, setRandoming] = useState(false) const [randoming, setRandoming] = useState(false)
@@ -43,6 +45,10 @@ export default function NewGame() {
<input value={motto} onChange={(e) => setMotto(e.target.value)} maxLength={20} /> <input value={motto} onChange={(e) => setMotto(e.target.value)} maxLength={20} />
</div> </div>
<div className="field"> <div className="field">
<label> MOD</label>
<div className="dim2" style={{ fontSize: '0.8rem', marginBottom: 8 }}>
{activeMods.length > 0 ? activeMods.map((m) => m.name).join('、') : '(无 —— 纯粹天下)'}
</div>
<label></label> <label></label>
<div className="diff-grid"> <div className="diff-grid">
{( {(
+2 -2
View File
@@ -82,7 +82,7 @@ describe('审计回归:P0 修复固化', () => {
}) })
it('防御性修补后金钟罩不变(行为等价确认)', () => { it('防御性修补后金钟罩不变(行为等价确认)', () => {
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('184a2203') expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('5670331a')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('a80ce621') expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('bb5fd1e2')
}) })
}) })
+7 -7
View File
@@ -7,20 +7,20 @@ import { World } from '../src/renderer/game/engine/runtime/World'
* 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。 * 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。
* 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。 * 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。
*/ */
// 0.1.29 续命基线:第四轮审计——续代(人口稳态 25/代际轮转/天年 200)/传薪年锚/ // 0.1.30 兼济基线:人口-世界成本闭环(人多粮少)/世界关注度(匹夫无罪)
// 敌强度相对化/权重防垄断/启用过滤/并发存档修复/存储债清理后固化 // + 内置 MOD 仓库/冲突矩阵 UI/预览升级后固化(生灭/经济/战斗受控变更)
const GOLDEN: Record<string, Record<number, string>> = { 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-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 处理待决事件; /** 第二金钟罩:自动 resolve 长跑("现实"世界——每 tick 处理待决事件;
* 锁事件闸/事件流全程,防"冻结世界"指纹漏锁)。 */ * 锁事件闸/事件流全程,防"冻结世界"指纹漏锁)。 */
const GOLDEN_RESOLVED: Record<string, Record<number, string>> = { const GOLDEN_RESOLVED: Record<string, Record<number, string>> = {
'bell-seed-1': { 560: 'bad85950', 1200: 'd9344194', 2160: '8ed61f10' }, 'bell-seed-1': { 560: 'fc002722', 1200: '7e526f0e', 2160: '3625b13b' },
'bell-seed-2': { 560: '3e9698e7', 1200: '68940d48', 2160: '1d56b086' }, 'bell-seed-2': { 560: '3e9698e7', 1200: '68940d48', 2160: '9036b57b' },
'bell-seed-3': { 560: 'f8d10c28', 1200: '54f9dd80', 2160: '9c931d77' } 'bell-seed-3': { 560: 'f8d10c28', 1200: '2c4c86ae', 2160: 'caa382fc' }
} }
const TIERS = [ const TIERS = [
+3 -3
View File
@@ -162,7 +162,7 @@ describe('GameFacade 门面', () => {
const f = new GameFacade(w, 1) const f = new GameFacade(w, 1)
const info = f.about() const info = f.about()
expect(info.title).toBe('仙途家族志') 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.modules).toBeGreaterThanOrEqual(11)
expect(info.systems).toBeGreaterThan(0) expect(info.systems).toBeGreaterThan(0)
expect(info.plugins).toBeGreaterThanOrEqual(3) expect(info.plugins).toBeGreaterThanOrEqual(3)
@@ -170,7 +170,7 @@ describe('GameFacade 门面', () => {
it('默认配置金钟罩不受门面化影响', () => { it('默认配置金钟罩不受门面化影响', () => {
PACK.reset() PACK.reset()
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('184a2203') expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('5670331a')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('a80ce621') expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('bb5fd1e2')
}) })
}) })
+50
View File
@@ -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)('战力墙 %scombatPowerOf 随境界增长', (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)('风险 %sresolve 胜负三向合法', (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)
})
})
+59
View File
@@ -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 %d120 年人口 ≤ 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)('天年 %smin(寿命,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)
})
})
+102
View File
@@ -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))(
'功法 %sL%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)
})
})
+72
View File
@@ -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 %drng 十步恒等', (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)('修行 %srate 恒正', (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)
})
})
+71
View File
@@ -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)(
'月俸 %sspirit 月俸只扣化神', (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 %d90 年灵石有限(无 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)('财政 %sfinance.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 %d120 月库存整体有限', (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)
}
})
})
+109
View File
@@ -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 %drng 四步等变', (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')
})
})
+70
View File
@@ -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()
})
})
+72
View File
@@ -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)('%s200 年 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 %sfragile 阈值 < 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)('%spower 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)
}
}
})
})
+15
View File
@@ -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)
})
})
+61
View File
@@ -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)
})
})
+35
View File
@@ -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 %dera/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')
})
})