import { resolve } from "node:path"; import { describe, expect, it } from "vitest"; import { createEngine, HOOK_NAMES, seedRandom, type HookContext, type IntentSystem, type System, type TimeSystem } from "../src/index.js"; const repoRoot = resolve(process.cwd(), "../.."); const contentDir = resolve(repoRoot, "content"); const modsDir = resolve(repoRoot, "mods"); describe("Engine", () => { it("start loads content, seeds world, and advancing a day fires the example mod hook", async () => { const seen: { event: string; payload: unknown }[] = []; const spy: System = { id: "spy", init() {}, on(event, payload) { seen.push({ event, payload }); }, }; const engine = createEngine({ contentDir, modsDir, systems: [spy] }); await engine.start(); expect(engine.registry.ids("map")).toEqual(["farm"]); expect(engine.registry.ids("time")).toEqual(["default"]); const map = engine.world.get("farm"); expect(map?.type).toBe("map"); expect(map?.width).toBe(12); expect(map?.height).toBe(12); expect(Array.isArray(map?.layers)).toBe(true); const player = engine.world.get("player"); expect(player?.type).toBe("player"); expect(player?.x).toBe(2); expect(player?.y).toBe(2); expect(engine.hooks.bound("onDayStart")).toBe(true); const time = engine.systems.get("time"); expect(time).toBeDefined(); time!.advance(480); expect(seen.some((s) => s.event === "mod:example-day-start")).toBe(true); const modEmit = seen.find((s) => s.event === "mod:example-day-start")!.payload as { modId: string; day: number; season: string; }; expect(modEmit.modId).toBe("example-mod"); expect(modEmit.day).toBe(2); expect(modEmit.season).toBe("spring"); await engine.stop(); }); it("facade exposes a seeded rng; same seed replays identical draws (D28)", async () => { const engine = createEngine({ contentDir, modsDir, seed: 42 }); await engine.start(); try { expect(engine.rng.state()).toEqual({ seed: 42, calls: 0 }); const expected = seedRandom(42); expect(engine.rng.next()).toBe(expected.next()); expect(engine.rng.nextInt(10)).toBe(expected.nextInt(10)); expect(engine.rng.state().calls).toBe(2); } finally { await engine.stop(); } }); it("facade rng defaults to a random seed when no seed is provided (D28)", async () => { const engine = createEngine({ contentDir, modsDir }); await engine.start(); try { expect(typeof engine.rng.state().seed).toBe("number"); expect(Number.isFinite(engine.rng.state().seed)).toBe(true); expect(engine.rng.next()).toBeGreaterThanOrEqual(0); expect(engine.rng.next()).toBeLessThan(1); } finally { await engine.stop(); } }); it("seeds player stamina/skills fields and fires onPlayerIntent with the intent payload (D29)", async () => { const engine = createEngine({ contentDir, modsDir }); await engine.start(); try { expect(engine.world.get("player")).toMatchObject({ stamina: 100, skills: {} }); expect(HOOK_NAMES).toHaveLength(16); expect(HOOK_NAMES).toContain("onPlayerIntent"); const seen: unknown[] = []; const unsubscribe = engine.hooks.on("onPlayerIntent", (ctx: HookContext) => seen.push(ctx.payload)); try { const intent = engine.systems.get("intent"); if (!intent) throw new Error("intent system not booted"); intent.handle("move", { tile: { x: 3, y: 4 } }); intent.handle("interact", { tile: { x: 9, y: 9 } }); } finally { unsubscribe(); } expect(seen).toHaveLength(2); expect(seen[0]).toMatchObject({ action: "move", tile: { x: 3, y: 4 }, ok: true }); expect(seen[1]).toMatchObject({ action: "interact", tile: { x: 9, y: 9 }, ok: true }); } finally { await engine.stop(); } }); });