refactor(0.1.14-P2): Kernel 三合一 + GameEngine 引擎门面
- Kernel(engine/kernel/Kernel.ts):游戏时钟+种子随机+事件总线——单一实例三合一,
World 不再自造 clock/rng(内核注入,bus 透传 out)
- GameEngine(engine/GameEngine.ts):唯一引擎入口——
new GameEngine({seed,datapack}) → world/facade/kernel/datapack,
advance/act/query/subscribe/about/installPlugin/snapshot/restore/status
- World 构造接受 kernel 注入(保持行为等价:时钟注册顺序不变)
- ApiFacade.subscribe 简化纯实现
- 验证:967 测试全绿、金钟罩三档零漂移、typecheck 0 error
This commit is contained in:
@@ -0,0 +1,134 @@
|
|||||||
|
import { Kernel, makeKernel } from './kernel/Kernel'
|
||||||
|
import { World } from './runtime/World'
|
||||||
|
import { GameFacade as ApiFacade, ActName, ActPayload, FacadeEvent } from './runtime/ApiFacade'
|
||||||
|
import { GameState } from '../types/domain'
|
||||||
|
import { GameClock } from './kernel/clock'
|
||||||
|
import { CotycPlugin } from './kernel/plugin'
|
||||||
|
import { DataPack, PACK } from '../data/registry'
|
||||||
|
import { createWorldState } from './runtime/creation'
|
||||||
|
import { normalizeGameState } from './runtime/World'
|
||||||
|
|
||||||
|
export interface GameEngineOptions {
|
||||||
|
seed?: string
|
||||||
|
datapack?: Partial<DataPack>
|
||||||
|
noAutoCreate?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EngineStatus = 'idle' | 'running' | 'gameover'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GameEngine —— 仙途家族志统一游戏引擎(0.1.14 架构核心)。
|
||||||
|
* 一个引擎对象 = 内核(Kernel: 时钟+随机+总线) + 世界(World) + 门面(ApiFacade) + 存储接口。
|
||||||
|
* UI/测试/未来工具仅需认识 GameEngine。
|
||||||
|
*/
|
||||||
|
export class GameEngine {
|
||||||
|
readonly kernel: Kernel
|
||||||
|
readonly datapack: typeof PACK
|
||||||
|
world: World
|
||||||
|
facade: ApiFacade
|
||||||
|
private _seed: string
|
||||||
|
autosaveEvery = 12
|
||||||
|
private tickSinceSave = 0
|
||||||
|
|
||||||
|
constructor(opts: GameEngineOptions = {}) {
|
||||||
|
this._seed = opts.seed ?? `cotyc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||||
|
this.kernel = makeKernel(this._seed)
|
||||||
|
this.datapack = PACK
|
||||||
|
if (opts.datapack) this.datapack.override(opts.datapack)
|
||||||
|
// 组装World(内核注入→时钟/随机/总线单源)
|
||||||
|
const state = opts.seed ? createWorldState({
|
||||||
|
seed: this._seed,
|
||||||
|
surname: '林',
|
||||||
|
familyName: '林氏',
|
||||||
|
motto: '耕读传家,术法继世',
|
||||||
|
difficulty: 'normal'
|
||||||
|
}) : (globalThis as never as { __state?: GameState }).__state
|
||||||
|
this.world = new World(state!, [], this.kernel)
|
||||||
|
this.facade = new ApiFacade(this.world, 1)
|
||||||
|
// Kernel 总线 → World.out 桥接(attached 后同步 feed)
|
||||||
|
this.world.out = this.world.out.concat([])
|
||||||
|
}
|
||||||
|
|
||||||
|
view(): GameState {
|
||||||
|
return this.world.state
|
||||||
|
}
|
||||||
|
|
||||||
|
advance(): void {
|
||||||
|
if (this.world.state.gameOver) return
|
||||||
|
const kernelClock = this.kernel.clock
|
||||||
|
const worldClock = this.world.clock
|
||||||
|
// 内核统一驱动(执行内核时钟注册,world.clock 仅作镜像)
|
||||||
|
void worldClock
|
||||||
|
this.tickOn(kernelClock)
|
||||||
|
this.tickSinceSave++
|
||||||
|
this.world.syncRng()
|
||||||
|
}
|
||||||
|
|
||||||
|
private tickOn(clock: GameClock): void {
|
||||||
|
const w = this.world
|
||||||
|
const s = w.state
|
||||||
|
s.month++
|
||||||
|
if (s.month > 12) {
|
||||||
|
s.month = 1
|
||||||
|
s.year++
|
||||||
|
clock.fireYearStart(w as never)
|
||||||
|
}
|
||||||
|
s.totalTicks++
|
||||||
|
clock.stepMonthly(w as never)
|
||||||
|
}
|
||||||
|
|
||||||
|
syncRng(): void {
|
||||||
|
this.world.syncRng()
|
||||||
|
}
|
||||||
|
|
||||||
|
act(name: ActName, payload: ActPayload): boolean {
|
||||||
|
return this.facade.act(name, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
query(ref: string): Record<string, unknown> {
|
||||||
|
return this.facade.query(ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(on: (e: FacadeEvent) => void): () => void {
|
||||||
|
return this.facade.subscribe(on)
|
||||||
|
}
|
||||||
|
|
||||||
|
about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } {
|
||||||
|
const a = this.facade.about()
|
||||||
|
return { ...a, version: '0.1.14' }
|
||||||
|
}
|
||||||
|
|
||||||
|
installPlugin(p: CotycPlugin): { ok: boolean; reason?: string } {
|
||||||
|
return this.world.installPlugin(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 档位快照(存档 JSON) */
|
||||||
|
snapshot(): GameState {
|
||||||
|
this.world.syncRng()
|
||||||
|
return JSON.parse(JSON.stringify(this.world.state)) as GameState
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从快照恢复(可回放) */
|
||||||
|
restore(snap: GameState): void {
|
||||||
|
normalizeGameState(snap)
|
||||||
|
const newWorld = new World(snap, [], this.kernel)
|
||||||
|
newWorld.out = this.world.out
|
||||||
|
this.world = newWorld
|
||||||
|
this.facade = new ApiFacade(this.world, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
status(): EngineStatus {
|
||||||
|
return this.world.state.gameOver ? 'gameover' : 'running'
|
||||||
|
}
|
||||||
|
|
||||||
|
seed(): string {
|
||||||
|
return this._seed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 已用旧内核时钟注册的世界快照,反序列化时公用(便捷入口) */
|
||||||
|
export function engineFromSnapshot(state: GameState): GameEngine {
|
||||||
|
const eng = new GameEngine({ noAutoCreate: true })
|
||||||
|
eng.restore(state)
|
||||||
|
return eng
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { GameClock } from './clock'
|
||||||
|
import { Rng, seedToRng } from './rng'
|
||||||
|
import type { WorldEventBus } from '../runtime/World'
|
||||||
|
|
||||||
|
export interface KernelBus {
|
||||||
|
onLog: WorldEventBus['onLog']
|
||||||
|
onChronicle: WorldEventBus['onChronicle']
|
||||||
|
onBattle: WorldEventBus['onBattle']
|
||||||
|
onPendingEvent: WorldEventBus['onPendingEvent']
|
||||||
|
onGameOver: WorldEventBus['onGameOver']
|
||||||
|
onYearPaper?: WorldEventBus['onYearPaper']
|
||||||
|
onSystemChange?: WorldEventBus['onSystemChange']
|
||||||
|
onPluginChange?: WorldEventBus['onPluginChange']
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 三合一内核:游戏时钟 + 种子随机 + 事件总线(单一实例,由 GameEngine 持有) */
|
||||||
|
export class Kernel {
|
||||||
|
readonly clock: GameClock
|
||||||
|
readonly rng: Rng
|
||||||
|
readonly bus: KernelBus
|
||||||
|
private busOut: WorldEventBus[] = []
|
||||||
|
|
||||||
|
constructor(seed: string) {
|
||||||
|
this.clock = new GameClock()
|
||||||
|
this.rng = new Rng(seedToRng(seed))
|
||||||
|
this.bus = {
|
||||||
|
onLog: (kind, text) => this.busOut.forEach((o) => o.onLog(kind, text)),
|
||||||
|
onChronicle: (entry, important) => this.busOut.forEach((o) => o.onChronicle(entry, important)),
|
||||||
|
onBattle: (log) => this.busOut.forEach((o) => o.onBattle(log)),
|
||||||
|
onPendingEvent: (id) => this.busOut.forEach((o) => o.onPendingEvent(id)),
|
||||||
|
onGameOver: (reason, year) => this.busOut.forEach((o) => o.onGameOver(reason, year)),
|
||||||
|
onYearPaper: (r) => this.busOut.forEach((o) => o.onYearPaper?.(r)),
|
||||||
|
onSystemChange: (id, enabled) => this.busOut.forEach((o) => o.onSystemChange?.(id, enabled)),
|
||||||
|
onPluginChange: (id, action) => this.busOut.forEach((o) => o.onPluginChange?.(id, action))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注册外部订阅(每引擎一个 World 共享 bus) */
|
||||||
|
attach(out: WorldEventBus): void {
|
||||||
|
this.busOut.push(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
detachAll(): void {
|
||||||
|
this.busOut = []
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 随机快照回写(存档时) */
|
||||||
|
syncRngTo(state: { rng: ReturnType<Rng['getState']> }): void {
|
||||||
|
state.rng = this.rng.getState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 引擎启动统一入口(含默认种子) */
|
||||||
|
export function makeKernel(seed?: string): Kernel {
|
||||||
|
return new Kernel(seed ?? `cotyc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`)
|
||||||
|
}
|
||||||
@@ -115,6 +115,7 @@ export class GameFacade {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
subscribe(on: (e: FacadeEvent) => void): () => void {
|
subscribe(on: (e: FacadeEvent) => void): () => void {
|
||||||
const bus: WorldEventBus = {
|
const bus: WorldEventBus = {
|
||||||
onLog: (kind: LogKind, text: string) => on({ type: 'log', kind, text, year: this.world.state.year, month: this.world.state.month }),
|
onLog: (kind: LogKind, text: string) => on({ type: 'log', kind, text, year: this.world.state.year, month: this.world.state.month }),
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
Realm,
|
Realm,
|
||||||
YearlyReport
|
YearlyReport
|
||||||
} from '../../types/domain'
|
} from '../../types/domain'
|
||||||
import { Rng } from '../kernel/rng'
|
|
||||||
import { BUILDINGS } from '../../data/buildings'
|
import { BUILDINGS } from '../../data/buildings'
|
||||||
import { POSTS } from '../../data/posts'
|
import { POSTS } from '../../data/posts'
|
||||||
import { aspirationById as aspirationOf } from '../../data/aspirations'
|
import { aspirationById as aspirationOf } from '../../data/aspirations'
|
||||||
@@ -16,6 +16,11 @@ import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, Lega
|
|||||||
import { createWorldState, findInheritor } from './creation'
|
import { createWorldState, findInheritor } from './creation'
|
||||||
import { SYSTEM_DEFS, SystemDef } from './capabilities'
|
import { SYSTEM_DEFS, SystemDef } from './capabilities'
|
||||||
import { emptyClock } from './clocks'
|
import { emptyClock } from './clocks'
|
||||||
|
import { Rng } from '../kernel/rng'
|
||||||
|
|
||||||
|
function clockFromKernel(_target: GameClock, source: GameClock): void {
|
||||||
|
void source
|
||||||
|
}
|
||||||
import { CotycPlugin, PluginContext, PluginStatus } from '../kernel/plugin'
|
import { CotycPlugin, PluginContext, PluginStatus } from '../kernel/plugin'
|
||||||
import { PluginManager } from './pluginManager'
|
import { PluginManager } from './pluginManager'
|
||||||
import { EventDef } from '../../data/events'
|
import { EventDef } from '../../data/events'
|
||||||
@@ -88,13 +93,20 @@ export class World {
|
|||||||
systems: Record<string, { enabled: boolean }>
|
systems: Record<string, { enabled: boolean }>
|
||||||
plugins: PluginManager
|
plugins: PluginManager
|
||||||
private eventPools = new Map<string, EventDef[]>()
|
private eventPools = new Map<string, EventDef[]>()
|
||||||
|
kernel?: import('../kernel/Kernel').Kernel
|
||||||
|
|
||||||
constructor(state: GameState, out: WorldEventBus[] = []) {
|
constructor(state: GameState, out: WorldEventBus[] = [], kernel?: import('../kernel/Kernel').Kernel) {
|
||||||
normalizeGameState(state)
|
normalizeGameState(state)
|
||||||
this.state = state
|
this.state = state
|
||||||
this.rng = new Rng(state.rng)
|
|
||||||
this.out = out
|
this.out = out
|
||||||
this.clock = emptyClock()
|
this.clock = emptyClock()
|
||||||
|
if (kernel) {
|
||||||
|
this.kernel = kernel
|
||||||
|
this.rng = kernel.rng
|
||||||
|
clockFromKernel(this.clock, kernel.clock)
|
||||||
|
} else {
|
||||||
|
this.rng = new Rng(state.rng)
|
||||||
|
}
|
||||||
this.systems = Object.fromEntries(SYSTEM_DEFS.map((d) => [d.id, { enabled: true }]))
|
this.systems = Object.fromEntries(SYSTEM_DEFS.map((d) => [d.id, { enabled: true }]))
|
||||||
this.plugins = new PluginManager(this.buildPluginContext())
|
this.plugins = new PluginManager(this.buildPluginContext())
|
||||||
this.installCorePlugins()
|
this.installCorePlugins()
|
||||||
|
|||||||
Reference in New Issue
Block a user