engine.test.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. import { resolve } from "node:path";
  2. import { describe, expect, it } from "vitest";
  3. import { createEngine, HOOK_NAMES, seedRandom, type HookContext, type IntentSystem, type System, type TimeSystem } from "../src/index.js";
  4. const repoRoot = resolve(process.cwd(), "../..");
  5. const contentDir = resolve(repoRoot, "content");
  6. const modsDir = resolve(repoRoot, "mods");
  7. describe("Engine", () => {
  8. it("start loads content, seeds world, and advancing a day fires the example mod hook", async () => {
  9. const seen: { event: string; payload: unknown }[] = [];
  10. const spy: System = {
  11. id: "spy",
  12. init() {},
  13. on(event, payload) {
  14. seen.push({ event, payload });
  15. },
  16. };
  17. const engine = createEngine({ contentDir, modsDir, systems: [spy] });
  18. await engine.start();
  19. expect(engine.registry.ids("map")).toEqual(["farm"]);
  20. expect(engine.registry.ids("time")).toEqual(["default"]);
  21. const map = engine.world.get("farm");
  22. expect(map?.type).toBe("map");
  23. expect(map?.width).toBe(12);
  24. expect(map?.height).toBe(12);
  25. expect(Array.isArray(map?.layers)).toBe(true);
  26. const player = engine.world.get("player");
  27. expect(player?.type).toBe("player");
  28. expect(player?.x).toBe(2);
  29. expect(player?.y).toBe(2);
  30. expect(engine.hooks.bound("onDayStart")).toBe(true);
  31. const time = engine.systems.get<TimeSystem>("time");
  32. expect(time).toBeDefined();
  33. time!.advance(480);
  34. expect(seen.some((s) => s.event === "mod:example-day-start")).toBe(true);
  35. const modEmit = seen.find((s) => s.event === "mod:example-day-start")!.payload as {
  36. modId: string;
  37. day: number;
  38. season: string;
  39. };
  40. expect(modEmit.modId).toBe("example-mod");
  41. expect(modEmit.day).toBe(2);
  42. expect(modEmit.season).toBe("spring");
  43. await engine.stop();
  44. });
  45. it("facade exposes a seeded rng; same seed replays identical draws (D28)", async () => {
  46. const engine = createEngine({ contentDir, modsDir, seed: 42 });
  47. await engine.start();
  48. try {
  49. expect(engine.rng.state()).toEqual({ seed: 42, calls: 0 });
  50. const expected = seedRandom(42);
  51. expect(engine.rng.next()).toBe(expected.next());
  52. expect(engine.rng.nextInt(10)).toBe(expected.nextInt(10));
  53. expect(engine.rng.state().calls).toBe(2);
  54. } finally {
  55. await engine.stop();
  56. }
  57. });
  58. it("facade rng defaults to a random seed when no seed is provided (D28)", async () => {
  59. const engine = createEngine({ contentDir, modsDir });
  60. await engine.start();
  61. try {
  62. expect(typeof engine.rng.state().seed).toBe("number");
  63. expect(Number.isFinite(engine.rng.state().seed)).toBe(true);
  64. expect(engine.rng.next()).toBeGreaterThanOrEqual(0);
  65. expect(engine.rng.next()).toBeLessThan(1);
  66. } finally {
  67. await engine.stop();
  68. }
  69. });
  70. it("seeds player stamina/skills fields and fires onPlayerIntent with the intent payload (D29)", async () => {
  71. const engine = createEngine({ contentDir, modsDir });
  72. await engine.start();
  73. try {
  74. expect(engine.world.get("player")).toMatchObject({ stamina: 100, skills: {} });
  75. expect(HOOK_NAMES).toHaveLength(16);
  76. expect(HOOK_NAMES).toContain("onPlayerIntent");
  77. const seen: unknown[] = [];
  78. const unsubscribe = engine.hooks.on("onPlayerIntent", (ctx: HookContext) => seen.push(ctx.payload));
  79. try {
  80. const intent = engine.systems.get<IntentSystem>("intent");
  81. if (!intent) throw new Error("intent system not booted");
  82. intent.handle("move", { tile: { x: 3, y: 4 } });
  83. intent.handle("interact", { tile: { x: 9, y: 9 } });
  84. } finally {
  85. unsubscribe();
  86. }
  87. expect(seen).toHaveLength(2);
  88. expect(seen[0]).toMatchObject({ action: "move", tile: { x: 3, y: 4 }, ok: true });
  89. expect(seen[1]).toMatchObject({ action: "interact", tile: { x: 9, y: 9 }, ok: true });
  90. } finally {
  91. await engine.stop();
  92. }
  93. });
  94. });