import { describe, expect, it } from "vitest"; import { activeHypothesis, beginHypothesis, createGameplayHistory, createSavepoint, deleteSavepoint, describeGameplayChange, finishHypothesis, gameplayMoment, recordGameplayMoment, restoreSavepoint, sessionFromGameplayState, } from "../../src/state/playHistory"; import { createAidMemoire, enterAidMemoireCell, } from "../../src/state/aidMemoire"; import { createSession } from "../../src/state/session"; describe("gameplay history", () => { it("records immutable states with useful move labels", () => { const initial = createSession([0, 0, 0, 0]); const changed = { ...initial, values: [0, 3, 0, 0] }; const label = describeGameplayChange(initial, changed, 2); const history = recordGameplayMoment( createGameplayHistory(initial), changed, label, ); expect(label).toBe("Set r1c2 to 3"); expect(history.moments.map((moment) => moment.label)).toEqual([ "Puzzle opened", "Set r1c2 to 3", ]); changed.values[1] = 4; expect(history.moments[1]!.state.values[1]).toBe(3); }); it("creates, restores and deletes full named savepoints", () => { const session = createSession([0, 0, 0, 0]); session.values[1] = 2; session.centerMarks[2] = 5; session.colors[3] = 4; session.elapsedSeconds = 75; let history = createSavepoint( createGameplayHistory(session), session, "Before chain", ); session.values[1] = 4; session.centerMarks[2] = 0; const transition = restoreSavepoint(history, history.savepoints[0]!.id); const restored = sessionFromGameplayState(transition.state); expect(restored.values[1]).toBe(2); expect(restored.centerMarks[2]).toBe(5); expect(restored.colors[3]).toBe(4); expect(restored.elapsedSeconds).toBe(75); expect(transition.history.moments.at(-1)?.label).toBe( "Restored “Before chain”", ); history = deleteSavepoint( transition.history, transition.history.savepoints[0]!.id, ); expect(history.savepoints).toHaveLength(0); }); it("captures aid-mémoire state in moments and savepoints", () => { const session = createSession([0, 0, 0, 0]); let aidMemoire = createAidMemoire(2, { enabled: true, cellCount: 2, }); aidMemoire = enterAidMemoireCell(aidMemoire, 0, "value", 1, 2); const history = createSavepoint( createGameplayHistory(session, aidMemoire), session, "Scratch one", aidMemoire, ); aidMemoire = enterAidMemoireCell(aidMemoire, 0, "value", 2, 2); const restored = restoreSavepoint(history, history.savepoints[0]!.id); expect(restored.state.aidMemoire?.cells[0]?.value).toBe(1); expect(history.moments[0]?.state.aidMemoire?.cells[0]?.value).toBe(1); expect(aidMemoire.cells[0]?.value).toBe(2); }); it("keeps discarded hypotheses available while restoring their exact base", () => { const session = createSession([0, 0, 0, 0]); session.values[0] = 1; let transition = beginHypothesis( createGameplayHistory(session), session, "Try a 2", ); expect(activeHypothesis(transition.history)?.name).toBe("Try a 2"); const branchSession = sessionFromGameplayState(transition.state); branchSession.values[1] = 2; transition = { history: recordGameplayMoment( transition.history, branchSession, "Set r1c2 to 2", ), state: transition.state, }; const result = finishHypothesis( transition.history, branchSession, "discard", ); expect(result.state.values).toEqual([1, 0, 0, 0]); expect(result.history.branches[0]?.status).toBe("discarded"); expect( result.history.moments.some( (moment) => moment.branchId === result.history.branches[0]!.id && moment.state.values[1] === 2, ), ).toBe(true); expect(result.history.moments.at(-1)?.label).toBe( "Discarded hypothesis “Try a 2”", ); }); it("can branch from a replayed moment and merge accepted changes", () => { const initial = createSession([0, 0, 0, 0]); const first = { ...initial, values: [1, 0, 0, 0] }; const second = { ...initial, values: [1, 2, 0, 0] }; let history = recordGameplayMoment( createGameplayHistory(initial), first, "First move", ); const replayId = history.currentMomentId; history = recordGameplayMoment(history, second, "Second move"); let transition = beginHypothesis(history, second, "Alternative", replayId); expect(transition.state.values).toEqual([1, 0, 0, 0]); const alternative = sessionFromGameplayState(transition.state); alternative.values[2] = 3; transition = { history: recordGameplayMoment( transition.history, alternative, "Alternative move", ), state: transition.state, }; const kept = finishHypothesis(transition.history, alternative, "keep"); expect(kept.state.values).toEqual([1, 0, 3, 0]); expect(kept.history.branches.at(-1)?.status).toBe("kept"); expect( gameplayMoment(kept.history, kept.history.currentMomentId)?.label, ).toBe("Kept hypothesis “Alternative”"); }); it("rejects blank checkpoint and hypothesis names", () => { const session = createSession([0, 0, 0, 0]); const history = createGameplayHistory(session); expect(() => createSavepoint(history, session, " ")).toThrow( "Savepoint name cannot be empty", ); expect(() => beginHypothesis(history, session, " ")).toThrow( "Hypothesis name cannot be empty", ); }); });