import { describe, expect, it } from "vitest"; import { boundsOf, DEFAULT_ROOM, getBlockedRects, isInsideRoom, overlaps } from "../geometry"; import { remaining } from "../inventory"; import { evaluateRules } from "../scoring"; import { initialState } from "../store"; import type { FurnitureItem, Room, Rule } from "../types"; import { anneal, type AnnealInput } from "./anneal"; import { autoFill } from "./autofill"; import { createRng } from "./rng"; const configs = initialState.configs; const weights = initialState.weights; const item = (id: string, type: string, x: number, y: number, extra: Partial = {}): FurnitureItem => { const c = configs[type]; return { id, type, x, y, width: c.defaultWidth, height: c.defaultHeight, rotation: 0, locked: false, ...extra }; }; const room: Room = { ...DEFAULT_ROOM, screens: [{ id: "s", wall: "top", offset: 3, width: 4 }], obstacles: [{ id: "o", x: 6, y: 3, width: 1, height: 1 }], }; const layout = (): FurnitureItem[] => [ item("t1", "table", 1, 2), item("t2", "table", 3, 5), item("t3", "table", 7, 5, { locked: true }), item("c1", "chair", 1.2, 3.1), item("c2", "chair", 8, 1), item("c3", "chair", 5, 2), ]; const run = (over: Partial = {}, iterations = 1500) => anneal({ room, items: layout(), configs, rules: [], weights, seed: 7, ...over }, { iterations }); const assertValid = (r: Room, items: FurnitureItem[]) => { const blocked = getBlockedRects(r); items.forEach((a, i) => { const b = boundsOf(a); expect(isInsideRoom(b, r)).toBe(true); blocked.forEach((z) => expect(overlaps(b, z)).toBe(false)); items.slice(i + 1).forEach((o) => expect(overlaps(b, boundsOf(o))).toBe(false)); }); }; describe("anneal", () => { it("never moves or rotates locked items", () => { const res = run(); const before = layout().find((i) => i.id === "t3")!; expect(res.items.find((i) => i.id === "t3")).toEqual(before); expect(res.skippedLocked).toBe(1); }); it("never returns an overlapping, out-of-room or blocked layout", () => { for (const seed of [1, 2, 3]) assertValid(room, run({ seed }).items); }); it("keeps Required rules unbroken when the start is valid", () => { const rules: Rule[] = [{ id: "r", kind: "againstWall", priority: "required", typeA: "chair" }]; const start = layout().map((i) => (i.type === "chair" ? { ...i, x: 0, y: i.y } : i)); // chairs on the left wall expect(evaluateRules(room, start, configs, rules).violations).toHaveLength(0); const res = run({ items: start, rules }); expect(evaluateRules(room, res.items, configs, rules).violations).toHaveLength(0); expect(res.remainingViolations).toBe(0); }); it("repairs a start that breaks a Required rule", () => { const rules: Rule[] = [{ id: "r", kind: "againstWall", priority: "required", typeA: "chair" }]; const startViolations = evaluateRules(room, layout(), configs, rules).violations.length; expect(startViolations).toBeGreaterThan(0); const res = run({ rules }, 4000); expect(res.remainingViolations).toBeLessThan(startViolations); }); it("gives the same result for the same seed", () => { expect(run({ seed: 42 })).toEqual(run({ seed: 42 })); expect(run({ seed: 42 }).items).not.toEqual(run({ seed: 43 }).items); }); it("never ends worse than it started", () => { for (const seed of [1, 2, 3, 4]) { const r = run({ seed }); expect(r.bestScore).toBeGreaterThanOrEqual(r.startScore - 1e-9); } }); it("only moves the allowed items", () => { const res = run({ movableIds: new Set(["c2"]) }); const start = layout(); res.items.filter((i) => i.id !== "c2").forEach((i) => expect(i).toEqual(start.find((s) => s.id === i.id))); }); it("runs thousands of iterations per second on a typical room", () => { // ~5,000/s on an idle machine; best of three and a 1,000/s floor so a busy machine doesn't fail the test let best = 0; for (let k = 0; k < 3; k++) { const t0 = performance.now(); const res = anneal({ room, items: layout(), configs, rules: [], weights, seed: 1 }, { iterations: 2000 }); best = Math.max(best, (res.iterations / (performance.now() - t0)) * 1000); } console.log(`iterations/s: ${Math.round(best)}`); expect(best).toBeGreaterThan(1000); }); }); describe("autoFill", () => { it("adds tables first, then chairs, from remaining inventory", () => { const f = autoFill(room, [], configs, initialState.typeOrder, [], 20, createRng(1)); expect(f.tablesAdded).toBeGreaterThan(0); expect(f.seats).toBeGreaterThanOrEqual(20); assertValid(room, f.items); expect(remaining(f.items, "table", configs.table.available)).toBeGreaterThanOrEqual(0); }); it("uses everything it can and reports a shortfall", () => { const f = autoFill(room, [], configs, initialState.typeOrder, [], 500, createRng(1)); expect(f.shortfall).toBeGreaterThan(0); expect(f.message).toMatch(/Only/); expect(remaining(f.items, "table", configs.table.available)).toBeGreaterThanOrEqual(0); expect(remaining(f.items, "chair", configs.chair.available)).toBeGreaterThanOrEqual(0); }); });