diff --git a/AGENTS.md b/AGENTS.md index 1cd0fff..76a791c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # AGENTS.md 仙途家族志 · 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` 核对改动面与预期一致。 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`(世界循环参数唯一权威),调平衡不改引擎流程。 - 发布产物 `release/`、`out/` 均已 gitignore,勿入库。 diff --git a/package.json b/package.json index 1c4410d..5df134e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "chronicle-of-the-immortal-clan", "productName": "仙途家族志", - "version": "0.1.29", + "version": "0.1.30", "description": "修仙 · 家族 · 经营 · 战斗 模拟器", "main": "./out/main/index.js", "author": "MetonaTeam", diff --git a/src/renderer/game/data/builtin-mods.ts b/src/renderer/game/data/builtin-mods.ts new file mode 100644 index 0000000..c966b62 --- /dev/null +++ b/src/renderer/game/data/builtin-mods.ts @@ -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) +} diff --git a/src/renderer/game/engine/kernel/plugin.ts b/src/renderer/game/engine/kernel/plugin.ts index d203351..5adf51a 100644 --- a/src/renderer/game/engine/kernel/plugin.ts +++ b/src/renderer/game/engine/kernel/plugin.ts @@ -66,6 +66,7 @@ export interface PluginStatus { protected: boolean description: string dependencies?: string[] + conflicts?: string[] } export type PluginChange = { id: string; action: 'install' | 'remove' | 'enable' | 'disable' } diff --git a/src/renderer/game/engine/plugin-public.ts b/src/renderer/game/engine/plugin-public.ts index 86bf02d..a348562 100644 --- a/src/renderer/game/engine/plugin-public.ts +++ b/src/renderer/game/engine/plugin-public.ts @@ -14,6 +14,14 @@ * 必须消耗时必须声明并随 0.1.x 基线重算。 */ 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 { SystemDef } from './runtime/capabilities' export { registerPluginFactory } from './runtime/World' diff --git a/src/renderer/game/engine/runtime/ApiFacade.ts b/src/renderer/game/engine/runtime/ApiFacade.ts index 49dc1e2..a5ba2ed 100644 --- a/src/renderer/game/engine/runtime/ApiFacade.ts +++ b/src/renderer/game/engine/runtime/ApiFacade.ts @@ -137,7 +137,7 @@ export class GameFacade { about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } { return { title: '仙途家族志', - version: '0.1.29', + version: '0.1.30', modules: this.world.systemList().length, systems: this.world.systemList().filter((s) => s.enabled).length, plugins: this.world.pluginList().length, diff --git a/src/renderer/game/engine/runtime/Systems/cultivation.ts b/src/renderer/game/engine/runtime/Systems/cultivation.ts index cb04966..74eaaab 100644 --- a/src/renderer/game/engine/runtime/Systems/cultivation.ts +++ b/src/renderer/game/engine/runtime/Systems/cultivation.ts @@ -89,7 +89,10 @@ export function cultivationTick(w: World): void { const used = Math.min(supply, demand) inv.lingcao = supply - used // 0.1.27 修为↔市场回环:修行耗草同时抽走世界池(池浅→价涨→温度→era 全链) - if (w.state.worldSim) new WorldSim(w).tradeSettle('lingcao', -used) + // W1(0.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 herbFactor = 0.6 + 0.4 * ratio if (ratio < 0.5 && w.state.family.flag['herbWarnYear'] !== w.state.year) { diff --git a/src/renderer/game/engine/runtime/Systems/diplomacy.ts b/src/renderer/game/engine/runtime/Systems/diplomacy.ts index b91eb03..446c3b4 100644 --- a/src/renderer/game/engine/runtime/Systems/diplomacy.ts +++ b/src/renderer/game/engine/runtime/Systems/diplomacy.ts @@ -16,6 +16,13 @@ export function diplomacyTick(w: World): void { // 姿态调制:扩张好勇、隐忍避战、结盟之势休兵 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) + // 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×盟数叠爆 const dyns = (w.state.worldSim?.npcDyn ?? {}) as Record }> for (const ally of Object.values(s.npcFamilies)) { diff --git a/src/renderer/game/engine/runtime/Systems/events.ts b/src/renderer/game/engine/runtime/Systems/events.ts index a084344..2f94c9e 100644 --- a/src/renderer/game/engine/runtime/Systems/events.ts +++ b/src/renderer/game/engine/runtime/Systems/events.ts @@ -349,7 +349,12 @@ export function eventRoll(w: World): void { // 仙门拍卖(每年十月掷币;时代调制:盛世频仍/末法难逢) if (s.month === 10 && w.sysEnabled('production')) { 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)) { s.family.flag[key] = true fire(w, 'ev-auction') diff --git a/src/renderer/game/engine/runtime/pluginManager.ts b/src/renderer/game/engine/runtime/pluginManager.ts index 097ee88..18c16f4 100644 --- a/src/renderer/game/engine/runtime/pluginManager.ts +++ b/src/renderer/game/engine/runtime/pluginManager.ts @@ -192,7 +192,8 @@ export class PluginManager { enabled: rt.status.enabled, protected: !!p.protected, description: p.description, - dependencies: p.dependencies + dependencies: p.dependencies, + conflicts: p.conflicts } }) } diff --git a/src/renderer/ui/panels/SettingsPanel.tsx b/src/renderer/ui/panels/SettingsPanel.tsx index ff0c16e..83789ac 100644 --- a/src/renderer/ui/panels/SettingsPanel.tsx +++ b/src/renderer/ui/panels/SettingsPanel.tsx @@ -1,5 +1,6 @@ import { useState } from 'react' import { useGameStore } from '../store' +import { BUILTIN_MODS } from '../../game/data/builtin-mods' import { examplePlugin } from '../../game/engine/runtime/demo-plugins' import { ensureFx } from '../fx' import { SYSTEM_DEFS } from '../../game/engine/runtime/capabilities' @@ -57,6 +58,20 @@ export default function SettingsPanel() { >导出当前MOD集 发现 {modsList.length} 个候选包 +
+ 内置仓库(官方示范包): + {BUILTIN_MODS.map((m) => ( + + ))} +
{modsList.length > 0 && (
{modsList.map((name) => ( @@ -93,6 +108,8 @@ export default function SettingsPanel() { {p.name} v{p.version}{p.author ? ` · ${p.author}` : ''} · {p.kind} · {p.description} + {p.dependencies?.length ? `(依赖:${p.dependencies.join('、')})` : ''} + {p.conflicts?.length ? `(冲突:${p.conflicts.join('、')})` : ''}
@@ -269,7 +286,7 @@ export default function SettingsPanel() { · 「外交」与四邻结好联姻;仇雠之族隔岁来犯,打得赢名望大涨,打不赢蚀钱伤丁。
· 「史书」自动记述繁华与凋零——百年之后,后人翻开这一卷家族志,见代代薪火、历历雪泥。
-
版本 0.1.29 · Chronicle of the Immortal Clan
+
版本 0.1.30 · Chronicle of the Immortal Clan
) diff --git a/src/renderer/ui/screens/NewGame.tsx b/src/renderer/ui/screens/NewGame.tsx index d32556c..bf99339 100644 --- a/src/renderer/ui/screens/NewGame.tsx +++ b/src/renderer/ui/screens/NewGame.tsx @@ -10,6 +10,8 @@ export default function NewGame() { const [familyName, setFamilyName] = useState('林家') const [motto, setMotto] = useState('耕读传家,术法继世') 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 [randoming, setRandoming] = useState(false) @@ -43,6 +45,10 @@ export default function NewGame() { setMotto(e.target.value)} maxLength={20} />
+ +
+ {activeMods.length > 0 ? activeMods.map((m) => m.name).join('、') : '(无 —— 纯粹天下)'} +
{( diff --git a/tests/audit-regression.test.ts b/tests/audit-regression.test.ts index 5f74250..bb60a27 100644 --- a/tests/audit-regression.test.ts +++ b/tests/audit-regression.test.ts @@ -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') }) }) diff --git a/tests/clock.test.ts b/tests/clock.test.ts index 4070b90..37ba8bc 100644 --- a/tests/clock.test.ts +++ b/tests/clock.test.ts @@ -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> = { - '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> = { - '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 = [ diff --git a/tests/facade-registry.test.ts b/tests/facade-registry.test.ts index dd95910..6f472ce 100644 --- a/tests/facade-registry.test.ts +++ b/tests/facade-registry.test.ts @@ -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') }) }) diff --git a/tests/matrix-combat-rel-0.1.30.test.ts b/tests/matrix-combat-rel-0.1.30.test.ts new file mode 100644 index 0000000..eb99b88 --- /dev/null +++ b/tests/matrix-combat-rel-0.1.30.test.ts @@ -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) + }) +}) diff --git a/tests/matrix-continuity-0.1.30.test.ts b/tests/matrix-continuity-0.1.30.test.ts new file mode 100644 index 0000000..e92e678 --- /dev/null +++ b/tests/matrix-continuity-0.1.30.test.ts @@ -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) + }) +}) diff --git a/tests/matrix-deep-0.1.30.test.ts b/tests/matrix-deep-0.1.30.test.ts new file mode 100644 index 0000000..d35f430 --- /dev/null +++ b/tests/matrix-deep-0.1.30.test.ts @@ -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) + }) +}) diff --git a/tests/matrix-depth-0.1.30.test.ts b/tests/matrix-depth-0.1.30.test.ts new file mode 100644 index 0000000..f0e07d4 --- /dev/null +++ b/tests/matrix-depth-0.1.30.test.ts @@ -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) + }) +}) diff --git a/tests/matrix-economy-new-0.1.30.test.ts b/tests/matrix-economy-new-0.1.30.test.ts new file mode 100644 index 0000000..86e0ac7 --- /dev/null +++ b/tests/matrix-economy-new-0.1.30.test.ts @@ -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 })?.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) + } + }) +}) diff --git a/tests/matrix-final-circle-0.1.30.test.ts b/tests/matrix-final-circle-0.1.30.test.ts new file mode 100644 index 0000000..84d5e24 --- /dev/null +++ b/tests/matrix-final-circle-0.1.30.test.ts @@ -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') + }) +}) diff --git a/tests/matrix-mod-cycle-0.1.30.test.ts b/tests/matrix-mod-cycle-0.1.30.test.ts new file mode 100644 index 0000000..7b50d79 --- /dev/null +++ b/tests/matrix-mod-cycle-0.1.30.test.ts @@ -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() + }) +}) diff --git a/tests/matrix-npc-life-0.1.30.test.ts b/tests/matrix-npc-life-0.1.30.test.ts new file mode 100644 index 0000000..82bee74 --- /dev/null +++ b/tests/matrix-npc-life-0.1.30.test.ts @@ -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 } + 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 }> }).npcDyn + for (const d of Object.values(dyns)) { + for (const v of Object.values(d.relationsWithOthers ?? {})) { + expect(v).toBeGreaterThanOrEqual(-100) + expect(v).toBeLessThanOrEqual(100) + } + } + }) +}) diff --git a/tests/matrix-peak30-0.1.30.test.ts b/tests/matrix-peak30-0.1.30.test.ts new file mode 100644 index 0000000..66fe257 --- /dev/null +++ b/tests/matrix-peak30-0.1.30.test.ts @@ -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) + }) +}) diff --git a/tests/matrix-rising-0.1.30.test.ts b/tests/matrix-rising-0.1.30.test.ts new file mode 100644 index 0000000..c052d1b --- /dev/null +++ b/tests/matrix-rising-0.1.30.test.ts @@ -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) + }) +}) diff --git a/tests/matrix-rule-world-0.1.30.test.ts b/tests/matrix-rule-world-0.1.30.test.ts new file mode 100644 index 0000000..6c171e5 --- /dev/null +++ b/tests/matrix-rule-world-0.1.30.test.ts @@ -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') + }) +})