v0.1.24: 寰宇初构——世界种子生成器 + 插件时轮锚点(1034 全绿)

【世界种子生成器(NPC 数量不再定死)】
- sim/worldgen.ts:generateWorld(seed) 确定性塑造天下——NPC 4~8 家随机
  (老牌世家模板 + 词库组合新贵,名字去重)、初始关系网(1-2世仇+1盟友)、
  开局 era 三态、区域风味、市场偏移 ±15%
- 独立派生 rng(seed::worldgen)——World.rng 主序列零消耗:同 seed 世界可重放、
  玩法随机序列不受生成器移动(金钟罩玩法序保护)
- state.worldGen 落档(normalize 旧档按 seed 重放=同世界,存档兼容)
- creation 初始化 npcFamilies/initSim 关系网/era/池偏置全走 worldgen——开局即恩怨
- 局内补位目标 = 开局家数(4~8),生灭闭环持续

【插件深化(0.1.24 第二组钩子)】
- PluginContext.beforePhase/afterPhase(时轮锚点:phase 前后挂勾,卸载全摘)
- PluginContext.addNpcTemplate(世界模板注入——词库插件化)
- plugin-public 文档同步;示例 Plugin 契约升级

【测试适配(世界名单随机化)】
- 静态 npc id 假设全面改动态取家(anyNpc helper 共享);
- 难度梯度用例改「同 seed 不同难度」比较;worldgen 确定性/范围/去重/关系对称 7 例

【测试】1034 全绿(45 套件);金钟罩 0.1.24 基线固化(worldgen 随机世界)+ worldAnnals 等;
build 通过
This commit is contained in:
2026-08-23 16:15:22 +08:00
parent 04ef80faa5
commit ec25bf92fc
28 changed files with 382 additions and 72 deletions
+4 -3
View File
@@ -11,9 +11,9 @@ describe('0.1.20 万世无谬', () => {
w.state.pendingEvent = undefined // 清月度事件闸(测试隔离)
fire(w, 'ev-legacypass') // 普通占位
expect(w.state.pendingEvent).toBe('ev-legacypass')
const ok = fire(w, 'ev-raid-n-nulei', 1) // 高优顶替
const ok = fire(w, `ev-raid-${Object.keys(w.state.npcFamilies)[0]}`, 1) // 高优顶替
expect(ok).toBe(true)
expect(w.state.pendingEvent).toBe('ev-raid-n-nulei')
expect(w.state.pendingEvent).toBe(`ev-raid-${Object.keys(w.state.npcFamilies)[0]}`)
expect(w.state.eventQueue).toContain('ev-legacypass') // 未丢失
})
@@ -98,7 +98,8 @@ describe('0.1.20 万世无谬', () => {
const f1 = stateFingerprint(w.state)
const ws = w.state.worldSim as WorldSimState
ws.era = ws.era === 'mofa' ? 'shengshi' : 'mofa' // 确保改成异值
ws.npcDyn['n-xuanying']!.stance = ws.npcDyn['n-xuanying']!.stance === 'expand' ? 'endure' : 'expand'
const dynKey = Object.keys(w.state.npcFamilies)[0]!
ws.npcDyn[dynKey]!.stance = ws.npcDyn[dynKey]!.stance === 'expand' ? 'endure' : 'expand'
const f2 = stateFingerprint(w.state)
expect(f1).not.toBe(f2)
})
+2 -2
View File
@@ -82,7 +82,7 @@ describe('审计回归:P0 修复固化', () => {
})
it('防御性修补后金钟罩不变(行为等价确认)', () => {
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('855600f5')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('2ecc7add')
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('d446167b')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('8db4594e')
})
})
+9 -6
View File
@@ -24,13 +24,14 @@ describe('0.1.3 audit regression', () => {
it('marries a npc family once only (allied lock)', () => {
const w = World.create({ seed: 'hy', surname: '卫', familyName: '卫家', motto: 'm', difficulty: 'normal' })
const npc = w.state.npcFamilies['n-danxin']
const npcId = Object.keys(w.state.npcFamilies)[0]!
const npc = w.state.npcFamilies[npcId]
npc.relation = 60
// 姑且让长子成年(18岁可婚)
w.state.members['x3'].bornYear = 1 - 18
expect(marryNpcFamily(w, 'n-danxin')).toBe(true)
expect(marryNpcFamily(w, npcId)).toBe(true)
expect(npc.allied).toBe(true)
expect(marryNpcFamily(w, 'n-danxin')).toBe(false)
expect(marryNpcFamily(w, npcId)).toBe(false)
})
it('or same-mother siblings cannot marry (candidates + direct)', () => {
@@ -48,17 +49,19 @@ describe('0.1.3 audit regression', () => {
it('peace cements relation to a floor above zero', () => {
const w = World.create({ seed: 'peace', surname: '华', familyName: '华家', motto: 'm', difficulty: 'normal' })
const npc = w.state.npcFamilies['n-nulei']
const npcId2 = Object.keys(w.state.npcFamilies)[0]!
const npc = w.state.npcFamilies[npcId2]
npc.relation = -80
w.state.family.stones = 900
expect(makePeace(w, 'n-nulei')).toBe(true)
expect(makePeace(w, npcId2)).toBe(true)
expect(npc.relation).toBeGreaterThanOrEqual(30)
})
it('event negative resource cannot sink stones below zero', () => {
const w = World.create({ seed: 'clamp', surname: '曾', familyName: '曾家', motto: 'm', difficulty: 'normal' })
const npcId2 = Object.keys(w.state.npcFamilies)[0]!
w.state.family.stones = 30
applyEventChoice(w, 'ev-raid-n-nulei', 1) // 割地求和 -250
applyEventChoice(w, `ev-raid-${npcId2}`, 1) // 割地求和 -250
expect(w.state.family.stones).toBe(0)
})
+8 -8
View File
@@ -7,20 +7,20 @@ import { World } from '../src/renderer/game/engine/runtime/World'
* 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。
* 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。
*/
// 0.1.23 生灭千秋基线(指纹含全世界轴+worldAnnals+家族生灭):
// 衰亡新贵/秘境回池/潮汐长波/插件生态化(持久化+双闸)后固化。
// 0.1.24 寰宇初构基线(指纹含全世界轴+worldgen 随机世界):
// 世界种子生成器(4~8 家/开局恩怨/era/市场偏移独立量)+时轮锚点后固化。
const GOLDEN: Record<string, Record<number, string>> = {
'bell-seed-1': { 560: '855600f5', 1200: 'e131b7d5', 2160: '7917ba3c' },
'bell-seed-2': { 560: '0184ccc8', 1200: 'ff36b9f7', 2160: '5fb6e896' },
'bell-seed-3': { 560: '2ecc7add', 1200: '62cfabdc', 2160: '4e0654ff' }
'bell-seed-1': { 560: 'd446167b', 1200: '549b868e', 2160: '6daf0d73' },
'bell-seed-2': { 560: '306c458c', 1200: '625f5d8e', 2160: '00b2ea0c' },
'bell-seed-3': { 560: '8db4594e', 1200: 'c3e1c746', 2160: 'e4f1029a' }
}
/** 第二金钟罩:自动 resolve 长跑("现实"世界——每 tick 处理待决事件;
* 锁事件闸/事件流全程,防"冻结世界"指纹漏锁)。 */
const GOLDEN_RESOLVED: Record<string, Record<number, string>> = {
'bell-seed-1': { 560: 'c17a9f82', 1200: 'cdcbbce9', 2160: '9abdd687' },
'bell-seed-2': { 560: '800674c1', 1200: 'c1ac7282', 2160: '4826c188' },
'bell-seed-3': { 560: '2c2ac417', 1200: 'ef9c3e4a', 2160: '2980499a' }
'bell-seed-1': { 560: '9571c9c4', 1200: 'a5a616e1', 2160: '6278847d' },
'bell-seed-2': { 560: '5dd40c96', 1200: '7b7c61a3', 2160: '8f49d288' },
'bell-seed-3': { 560: 'a1f441ce', 1200: 'ffb579d0', 2160: 'b5fc8019' }
}
const TIERS = [
+4 -2
View File
@@ -4,6 +4,7 @@ import { combatPowerOf, resolveEncounter, resolveRaid, rollWarbooty } from '../s
import { ENEMIES } from '../src/renderer/game/data/secrets'
import { sendMission, recallAll } from '../src/renderer/game/engine/runtime/Systems/missions'
import { missionById } from '../src/renderer/game/data/secrets'
import { anyNpc } from './world.helpers'
import { resetSaveBus, attachLogSink } from './world.helpers'
function baseWorld(seed: string): World {
@@ -85,12 +86,13 @@ describe('combat 战斗结算', () => {
it('raid 后关系变化方向正确', () => {
const w = baseWorld('cb-f')
const npc = w.state.npcFamilies['n-nulei']
const npc = w.state.npcFamilies[anyNpc(w, ['n-nulei'])]
npc.relation = -60
const npcId = anyNpc(w, ['n-nulei'])
const pre = npc.power
const team = [w.state.members['x4']]
team[0].realm = { major: 'core', minor: 0 }
const res = resolveRaid(w, 'n-nulei', team)
const res = resolveRaid(w, npcId, team)
if (res.win) {
expect(npc.relation).toBeGreaterThan(-60)
} else {
+1 -1
View File
@@ -108,7 +108,7 @@ describe('advance 死局与时钟守卫', () => {
describe('事件效果残留防御', () => {
it('关系 clamp 至 ±100', () => {
const w = baseWorld('rm-a')
applyEventChoice(w, 'ev-raid-n-nulei', 0) // 关系打向正 25? 取决于胜负
applyEventChoice(w, `ev-raid-${Object.keys(w.state.npcFamilies)[0]}`, 0) // 关系打向正 25? 取决于胜负
applyEventChoice(w, 'ev-zhusu', 2) // -20
for (const n of Object.values(w.state.npcFamilies)) {
expect(n.relation).toBeGreaterThanOrEqual(-100)
+7 -4
View File
@@ -35,12 +35,15 @@ describe('0.1.23 生灭千秋+插件生态', () => {
it('新贵补位:家族数 < 4 时年首可生(worldSim 工作)', () => {
const w = worldAt('dy-2', 12)
const ws = w.state.worldSim as WorldSimState
delete w.state.npcFamilies['n-nulei']
delete ws.npcDyn['n-nulei']
// 动态取当前世界任一家(wgen 世界名单随机)
const victim = Object.keys(w.state.npcFamilies)[0]!
delete w.state.npcFamilies[victim]
delete ws.npcDyn[victim]
const before = Object.keys(w.state.npcFamilies).length
ws.era = 'luanshi'
for (let i = 0; i < 240 && Object.keys(w.state.npcFamilies).length < 4; i++) w.advanceMonth()
expect(Object.keys(w.state.npcFamilies).length).toBeGreaterThan(before)
const target = Math.min(8, w.state.worldGen?.npcCount ?? 4)
for (let i = 0; i < 600 && Object.keys(w.state.npcFamilies).length < target; i++) w.advanceMonth()
expect(Object.keys(w.state.npcFamilies).length).toBe(target)
})
it('秘境产出回池:missions 完成后 lingcao 池有注入', () => {
+3 -3
View File
@@ -9,7 +9,7 @@ describe('echo 四邻回声', () => {
it('友好家族来使赠礼(关系>40 分支)', () => {
const w = baseWorld('echo-a')
const npcId = 'n-danxin'
const npcId = Object.keys(w.state.npcFamilies)[0]!
w.state.npcFamilies[npcId].relation = 60
w.state.year = 10
w.state.pendingEvent = undefined
@@ -34,7 +34,7 @@ describe('echo 四邻回声', () => {
it('敌视家族暗桩选项:花钱戒备扣灵石', () => {
const w = baseWorld('echo-b')
const npcId = 'n-nulei'
const npcId = Object.keys(w.state.npcFamilies)[0]!
w.state.npcFamilies[npcId].relation = -80
const stones0 = w.state.family.stones
applyEventChoice(w, `ev-echo-${npcId}-12`, 0)
@@ -43,7 +43,7 @@ describe('echo 四邻回声', () => {
it('中立传闻单选无副作用', () => {
const w = baseWorld('echo-c')
const npcId = 'n-sihai'
const npcId = Object.keys(w.state.npcFamilies)[0]!
w.state.npcFamilies[npcId].relation = 0
const rep0 = w.state.family.reputation
applyEventChoice(w, `ev-echo-${npcId}-14`, 0)
+8 -7
View File
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
import { World } from '../src/renderer/game/engine/runtime/World'
import { matchesCond, applyEventChoice, findEvent, eventRoll } from '../src/renderer/game/engine/runtime/Systems/events'
import { EVENTS } from '../src/renderer/game/data/events'
import { resetSaveBus, attachLogSink } from './world.helpers'
import { resetSaveBus, attachLogSink, anyNpc } from './world.helpers'
function baseWorld(seed: string): World {
const w = World.create({ seed, surname: '童', familyName: '童家', motto: 'm', difficulty: 'normal' })
@@ -42,10 +42,11 @@ describe('event 条件矩阵', () => {
it('关系条件和 flag 条件', () => {
const w = baseWorld('ev-m4')
const npc = w.state.npcFamilies['n-nulei']
const npcId = anyNpc(w, ['n-nulei'])
const npc = w.state.npcFamilies[npcId]
npc.relation = -60
expect(matchesCond(w, { relation: { npcId: 'n-nulei', lt: -50 } })).toBe(true)
expect(matchesCond(w, { relation: { npcId: 'n-nulei', gt: 0 } })).toBe(false)
expect(matchesCond(w, { relation: { npcId, lt: -50 } })).toBe(true)
expect(matchesCond(w, { relation: { npcId, gt: 0 } })).toBe(false)
w.state.family.flag['tenants'] = true
expect(matchesCond(w, { flag: { key: 'tenants', eq: true } })).toBe(true)
expect(matchesCond(w, { flag: { key: 'tenants', eq: false } })).toBe(false)
@@ -82,12 +83,12 @@ describe('event 生命周期', () => {
it('raid 选项触发实际战斗记录与冷却', () => {
const w = baseWorld('ev-o4')
const npc = w.state.npcFamilies['n-nulei']
const npc = w.state.npcFamilies[anyNpc(w, ['n-nulei'])]
npc.relation = -80
const b0 = w.state.battles.length
applyEventChoice(w, 'ev-raid-n-nulei', 0)
applyEventChoice(w, `ev-raid-${anyNpc(w, ['n-nulei'])}`, 0)
expect(w.state.battles.length).toBeGreaterThan(b0)
expect(w.state.family.flag['raidCD-n-nulei']).toBe(w.state.year)
expect(w.state.family.flag[`raidCD-${anyNpc(w, ['n-nulei'])}`]).toBe(w.state.year)
})
it('事件效果不会把成员数削减到非法', () => {
+3 -3
View File
@@ -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.22')
expect(info.version).toContain('0.1.24')
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('855600f5')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('2ecc7add')
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('d446167b')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('8db4594e')
})
})
+7 -5
View File
@@ -49,20 +49,22 @@ describe('赠礼档位', () => {
it.each(GIFT_TIERS.map((t) => [t] as const))('档位 %i 赠予后关系精确', (t) => {
const w = baseWorld(`gift-${t}`)
const npc = w.state.npcFamilies['n-xuanying']
const npcId = Object.keys(w.state.npcFamilies)[0]!
const npc = w.state.npcFamilies[npcId]
const before = npc.relation
w.state.family.stones = 1000
giftNpc(w, 'n-xuanying', t)
giftNpc(w, npcId, t)
expect(npc.relation).toBe(Math.min(100, before + calcGiftGain(t)))
})
it('赠礼不足预算拒绝,分文不动', () => {
const w = baseWorld('gift-poor')
const npcId = Object.keys(w.state.npcFamilies)[0]!
w.state.family.stones = 10
const before = w.state.npcFamilies['n-xuanying'].relation
const ok = giftNpc(w, 'n-xuanying', 40)
const before = w.state.npcFamilies[npcId].relation
const ok = giftNpc(w, npcId, 40)
expect(ok).toBe(false)
expect(w.state.npcFamilies['n-xuanying'].relation).toBe(before)
expect(w.state.npcFamilies[npcId].relation).toBe(before)
})
})
+17 -12
View File
@@ -35,15 +35,16 @@ describe('0.1.19 八方风雨', () => {
const w = World.create({ seed: 'st-2', surname: '邱', familyName: '邱家', motto: 'm', difficulty: 'normal' })
w.advanceMonth()
const dyns = w.state.worldSim as WorldSimState
dyns.npcDyn['n-nulei'] = { ...dyns.npcDyn['n-nulei']!, stance: 'expand', stanceSinceYear: 1 }
const npcKey = Object.keys(w.state.npcFamilies).find((k) => k === 'n-nulei') ?? Object.keys(w.state.npcFamilies)[0]!
dyns.npcDyn[npcKey] = { ...dyns.npcDyn[npcKey]!, stance: 'expand', stanceSinceYear: 1 }
// 关系拉到仇深 + 伪造 raidCD 前一年,跑 240 月计数
const npc = w.state.npcFamilies['n-nulei']
const npc = w.state.npcFamilies[npcKey]
npc.relation = -70
npc.allied = false
let evcount = 0
for (let i = 0; i < 600; i++) {
npc.relation = -70 // 钉死(diplomacy drift 会拉回)
dyns.npcDyn['n-nulei'] = { ...dyns.npcDyn['n-nulei'], stance: 'expand', stanceSinceYear: w.state.year }
dyns.npcDyn[npcKey] = { ...dyns.npcDyn[npcKey], stance: 'expand', stanceSinceYear: w.state.year }
w.advanceMonth()
// 任何 pending 顶住事件闸则先清场(否则后续 raid 无法 fire)
while (w.state.pendingEvent) {
@@ -71,22 +72,26 @@ describe('0.1.19 八方风雨', () => {
const w = World.create({ seed: 'st-4', surname: '方', familyName: '方家', motto: 'm', difficulty: 'normal' })
w.advanceMonth()
const dyns = w.state.worldSim as WorldSimState
dyns.npcDyn['n-xuanying']!.relationsWithOthers['n-nulei'] = -70
w.state.npcFamilies['n-xuanying'].relation = 60
const before = w.state.npcFamilies['n-nulei'].relation
expect(w.setAlliance('n-xuanying', true)).toBe(true)
expect(w.state.npcFamilies['n-nulei'].relation).toBeLessThan(before)
const allyKey = Object.keys(w.state.npcFamilies).find((k) => k === 'n-xuanying') ?? Object.keys(w.state.npcFamilies)[0]!
const foeKey = Object.keys(w.state.npcFamilies).find((k) => k === 'n-nulei') ?? Object.keys(w.state.npcFamilies)[1]!
dyns.npcDyn[allyKey]!.relationsWithOthers[foeKey] = -70
w.state.npcFamilies[allyKey].relation = 60
const before = w.state.npcFamilies[foeKey].relation
expect(w.setAlliance(allyKey, true)).toBe(true)
expect(w.state.npcFamilies[foeKey].relation).toBeLessThan(before)
})
it('调停:花50灵石令世仇回暖、声望+2', () => {
const w = World.create({ seed: 'st-5', surname: '凌', familyName: '凌家', motto: 'm', difficulty: 'normal' })
w.advanceMonth()
const dyns = w.state.worldSim as WorldSimState
dyns.npcDyn['n-danxin']!.relationsWithOthers['n-sihai'] = -60
const aKey = Object.keys(w.state.npcFamilies)[0]!
const bKey = Object.keys(w.state.npcFamilies)[1]!
dyns.npcDyn[aKey]!.relationsWithOthers[bKey] = -60
w.state.family.stones = 500
const r0 = dyns.npcDyn['n-danxin']!.relationsWithOthers['n-sihai']
expect(w.mediateNpcs('n-danxin', 'n-sihai')).toBe(true)
const r1 = dyns.npcDyn['n-danxin']!.relationsWithOthers['n-sihai']
const r0 = dyns.npcDyn[aKey]!.relationsWithOthers[bKey]
expect(w.mediateNpcs(aKey, bKey)).toBe(true)
const r1 = dyns.npcDyn[aKey]!.relationsWithOthers[bKey]
expect(r1).toBeGreaterThan(r0)
expect(w.state.family.reputation).toBeGreaterThanOrEqual(2)
})
+1 -1
View File
@@ -137,7 +137,7 @@ describe('Marriage & equipment & rites', () => {
it('taunt has 1-year cooldown', () => {
const w = World.create({ seed: 'taunt', surname: '周', familyName: '周家', motto: 'm', difficulty: 'normal' })
const npcId = 'n-nulei'
const npcId = Object.keys(w.state.npcFamilies)[0]!
const before = w.state.npcFamilies[npcId].relation
expect(w.tauntNpc(npcId)).toBe(true)
expect(w.state.npcFamilies[npcId].relation).toBe(before - 20)
+7 -5
View File
@@ -9,13 +9,15 @@ function opts(seed: string, difficulty: 'easy' | 'normal' | 'hard' = 'normal', s
describe('World 生命周期与初始态', () => {
it('三种难度初始资源与 NPC 强度梯度正确', () => {
const easy = createWorldState(opts('st-e', 'easy'))
const normal = createWorldState(opts('st-n', 'normal'))
const hard = createWorldState(opts('st-h', 'hard'))
// 同 seed 不同难度(worldgen 由 seed 决定——三世界名单一致,可比)
const easy = createWorldState(opts('st-diff', 'easy'))
const normal = createWorldState(opts('st-diff', 'normal'))
const hard = createWorldState(opts('st-diff', 'hard'))
expect(easy.family.stones).toBeGreaterThan(normal.family.stones)
expect(normal.family.stones).toBeGreaterThan(hard.family.stones)
expect(easy.npcFamilies['n-nulei'].power).toBeLessThan(normal.npcFamilies['n-nulei'].power)
expect(normal.npcFamilies['n-nulei'].power).toBeLessThan(hard.npcFamilies['n-nulei'].power)
const someId = Object.keys(normal.npcFamilies)[0]!
expect(easy.npcFamilies[someId].power).toBeLessThan(normal.npcFamilies[someId].power)
expect(normal.npcFamilies[someId].power).toBeLessThan(hard.npcFamilies[someId].power)
})
it('创建后基本不变量的存在性', () => {
+7
View File
@@ -19,3 +19,10 @@ export function attachLogSink(w: World): void {
sinks.add(bus)
w.out.push(bus)
}
/** 取当前世界 NPC id:prefer 命中则用之(多为老牌 id),缺失取任一家(wgen 世界名单随机) */
export function anyNpc(w: import('./../src/renderer/game/engine/runtime/World').World, prefer?: string[]): string {
const ids = Object.keys(w.state.npcFamilies)
const hit = prefer?.find((p) => ids.includes(p))
return hit ?? ids[0]!
}
+65
View File
@@ -0,0 +1,65 @@
import { describe, expect, it } from 'vitest'
import { World } from '../src/renderer/game/engine/runtime/World'
import { generateWorld } from '../src/renderer/game/engine/sim/worldgen'
describe('0.1.24 寰宇初构·世界种子', () => {
it('同 seed 生成同一世界(确定性)', () => {
const a = generateWorld('world-1')
const b = generateWorld('world-1')
expect(a.npcs.length).toBe(b.npcs.length)
expect(a.npcs[0]!.id).toBe(b.npcs[0]!.id)
expect(a.feuds).toEqual(b.feuds)
})
it('不同 seed 生成不同世界(势力随机 4~8 家)', () => {
const seen = new Set<string>()
for (const s of ['w1', 'w2', 'w3', 'w4', 'w5']) {
const g = generateWorld(s)
expect(g.npcs.length).toBeGreaterThanOrEqual(4)
expect(g.npcs.length).toBeLessThanOrEqual(8)
seen.add(g.npcs[0]!.id)
}
expect(seen.size).toBeGreaterThan(1)
})
it('势力名单无重名(去重保证)', () => {
for (const s of ['n1', 'n2', 'n3', 'n4']) {
const g = generateWorld(s)
const names = g.npcs.map((n) => n.name)
expect(new Set(names).size).toBe(names.length)
}
})
it('初始关系网:世仇/盟友对存在且对称', () => {
const g = generateWorld('wrel')
expect(g.feuds.length).toBeGreaterThanOrEqual(1)
expect(g.alliances.length).toBeGreaterThanOrEqual(1)
for (const [a, b] of [...g.feuds, ...g.alliances]) {
expect(g.relations[a]?.[b]).toBe(g.relations[b]?.[a])
expect(g.relations[a]?.[b]).toBeDefined()
}
})
it('开局时界(era 三态合法)与市场偏移存在', () => {
const g = generateWorld('wera')
expect(['shengshi', 'pingshi', 'luanshi']).toContain(g.eras)
expect(Object.keys(g.marketOffset).length).toBe(5)
})
it('世界创建即随机:同 seed 同势力、不同 seed 不同势力', () => {
const w1 = World.create({ seed: 'now-1', surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
const w2 = World.create({ seed: 'now-1', surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
const w3 = World.create({ seed: 'now-2', surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
expect(Object.keys(w1.state.npcFamilies).join()).toBe(Object.keys(w2.state.npcFamilies).join())
expect(Object.keys(w1.state.npcFamilies).join()).not.toBe(Object.keys(w3.state.npcFamilies).join())
})
it('开局即恩怨:关系网入档并生效(首月快讯/大样)', () => {
const w = World.create({ seed: 'now-3', surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
w.advanceMonth()
const ws = w.state.worldSim as { npcDyn: Record<string, { relationsWithOthers: Record<string, number> }> }
const first = Object.keys(w.state.npcFamilies)[0]!
const rels = ws.npcDyn[first]?.relationsWithOthers ?? {}
expect(Object.keys(rels).length).toBeGreaterThan(0)
})
})