77 lines
2.5 KiB
JavaScript
77 lines
2.5 KiB
JavaScript
import { test } from "node:test";
|
|||
|
|
import assert from "node:assert/strict";
|
||
|
|
import { readFileSync } from "node:fs";
|
||
|
|
import { Engine } from "../src/core/engine.js";
|
||
|
|
import { SAVE_VERSION } from "../src/core/save.js";
|
||
|
|
|
||
|
|
const example = JSON.parse(readFileSync(new URL("../example/game.json", import.meta.url), "utf8"));
|
||
|
|
|
||
|
|
const fresh = () => {
|
||
|
|
let now = 0;
|
||
|
|
const e = new Engine(structuredClone(example), { clock: () => now });
|
||
|
|
e.newGame();
|
||
|
|
return { e, advance: (ms) => { now += ms; e.tick(ms); } };
|
||
|
|
};
|
||
|
|
|
||
|
|
test("save/load round-trip restores the whole world", () => {
|
||
|
|
const { e, advance } = fresh();
|
||
|
|
e.selectVerb("use");
|
||
|
|
e.click({ kind: "object", id: "mug", tags: ["container"] });
|
||
|
|
e.goTo("tavern");
|
||
|
|
e.world.setFlag("didSomething", true);
|
||
|
|
e.world.addVar("score", 42);
|
||
|
|
const save = e.save();
|
||
|
|
|
||
|
|
const e2 = new Engine(structuredClone(example), { clock: () => 0 });
|
||
|
|
e2.load(save);
|
||
|
|
assert.equal(e2.world.room, "tavern");
|
||
|
|
assert.deepEqual(e2.world.inventory, ["mug"]);
|
||
|
|
assert.equal(e2.world.flags.didSomething, true);
|
||
|
|
assert.equal(e2.world.vars.score, 42);
|
||
|
|
assert.equal(e2.phase, "playing");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("save format is versioned and game-tagged", () => {
|
||
|
|
const { e } = fresh();
|
||
|
|
const data = JSON.parse(e.save());
|
||
|
|
assert.equal(data.version, SAVE_VERSION);
|
||
|
|
assert.equal(data.game, "skeleton-demo");
|
||
|
|
assert.ok(data.savedAt);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("rejects saves from another game", () => {
|
||
|
|
const { e } = fresh();
|
||
|
|
const save = e.save();
|
||
|
|
const other = structuredClone(example);
|
||
|
|
other.meta.name = "different-game";
|
||
|
|
const e2 = new Engine(other, { clock: () => 0 });
|
||
|
|
assert.throws(() => e2.load(save), /save is for game/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("rejects future save versions", () => {
|
||
|
|
const { e } = fresh();
|
||
|
|
const data = JSON.parse(e.save());
|
||
|
|
data.version = 999;
|
||
|
|
assert.throws(() => e.load(JSON.stringify(data)), /newer than engine/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("rejects garbage saves", () => {
|
||
|
|
const { e } = fresh();
|
||
|
|
assert.throws(() => e.load("not json"), /not valid JSON/);
|
||
|
|
assert.throws(() => e.load("{}"), /no version/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("mid-dialogue save resumes at the saved node", () => {
|
||
|
|
const { e } = fresh();
|
||
|
|
e.goTo("tavern");
|
||
|
|
e.selectVerb("talk");
|
||
|
|
e.click({ kind: "character", id: "barkeep", tags: ["talker"] });
|
||
|
|
assert.equal(e.dialogue.current().node, "greet");
|
||
|
|
const save = e.save();
|
||
|
|
|
||
|
|
const e2 = new Engine(structuredClone(example), { clock: () => 0 });
|
||
|
|
e2.load(save);
|
||
|
|
assert.ok(e2.dialogue.active);
|
||
|
|
assert.equal(e2.dialogue.current().node, "greet");
|
||
|
|
assert.equal(e2.dialogueChoices().length, 2);
|
||
|
|
});
|