Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
222 lines
8.8 KiB
TypeScript
222 lines
8.8 KiB
TypeScript
import { describe, expect, test } from "vitest";
|
|
import { OfficeSimulation, cameraFor, type MoveInput } from "../../src/renderer/src/pages/office/simulation";
|
|
import type { AgentActivity, OfficeAgentInput } from "../../src/renderer/src/pages/office/office-types";
|
|
|
|
const IDLE: MoveInput = { up: false, down: false, left: false, right: false };
|
|
|
|
function agentInput(id: string, roleLevel = "staff", activity: AgentActivity = "idle"): OfficeAgentInput {
|
|
return {
|
|
id,
|
|
name: `Agent ${id}`,
|
|
role: "Engineer",
|
|
title: "Engineer",
|
|
department: "Engineering",
|
|
roleLevel,
|
|
activity,
|
|
taskTitle: null,
|
|
};
|
|
}
|
|
|
|
function steps(sim: OfficeSimulation, ms: number, input: Partial<MoveInput>): void {
|
|
const move = { ...IDLE, ...input };
|
|
for (let i = 0; i < ms / 16; i++) sim.step(16, move);
|
|
}
|
|
|
|
describe("office simulation", () => {
|
|
test("the CEO takes the CEO office and staff fill open desks in order", () => {
|
|
const sim = new OfficeSimulation();
|
|
sim.sync([agentInput("a"), agentInput("ceo", "c_suite"), agentInput("b")]);
|
|
const ceo = sim.agents.find((agent) => agent.input.id === "ceo")!;
|
|
const staffA = sim.agents.find((agent) => agent.input.id === "a")!;
|
|
const staffB = sim.agents.find((agent) => agent.input.id === "b")!;
|
|
expect(ceo.isCeo).toBe(true);
|
|
expect(ceo.seat).toEqual(sim.plan.ceoSeat);
|
|
expect(staffA.deskIndex).toBe(0);
|
|
expect(staffB.deskIndex).toBe(1);
|
|
});
|
|
|
|
test("positions survive snapshot refreshes without teleporting", () => {
|
|
const sim = new OfficeSimulation();
|
|
sim.sync([agentInput("a")]);
|
|
sim.agents[0].x = 500;
|
|
sim.agents[0].y = 320;
|
|
sim.sync([agentInput("a")]);
|
|
expect(sim.agents[0].x).toBe(500);
|
|
expect(sim.agents[0].y).toBe(320);
|
|
});
|
|
|
|
test("the board member spawns at the entrance and keeps position on re-sync", () => {
|
|
const sim = new OfficeSimulation();
|
|
sim.syncPlayer("emp-1", "Dana Lee");
|
|
expect(sim.player!.x).toBe(sim.plan.entrance.x);
|
|
expect(sim.player!.y).toBe(sim.plan.entrance.y);
|
|
steps(sim, 600, { right: true });
|
|
const movedX = sim.player!.x;
|
|
sim.syncPlayer("emp-1", "Dana Lee");
|
|
expect(sim.player!.x).toBe(movedX);
|
|
});
|
|
|
|
test("WASD input moves and faces the board member", () => {
|
|
const sim = new OfficeSimulation();
|
|
sim.syncPlayer("emp-1", "Dana Lee");
|
|
const startX = sim.player!.x;
|
|
steps(sim, 320, { right: true });
|
|
expect(sim.player!.x).toBeGreaterThan(startX + 30);
|
|
expect(sim.player!.facing).toBe("east");
|
|
expect(sim.player!.walkPhase).toBeGreaterThan(0);
|
|
sim.step(16, IDLE);
|
|
expect(sim.player!.walkPhase).toBe(0);
|
|
});
|
|
|
|
test("walls stop the board member", () => {
|
|
const sim = new OfficeSimulation();
|
|
sim.syncPlayer("emp-1", "Dana Lee");
|
|
steps(sim, 4000, { left: true });
|
|
expect(sim.player!.x).toBeGreaterThanOrEqual(22);
|
|
const y = sim.player!.y;
|
|
steps(sim, 4000, { up: true });
|
|
expect(sim.player!.y).toBeGreaterThan(288);
|
|
expect(sim.player!.y).toBeLessThanOrEqual(y);
|
|
});
|
|
|
|
test("diagonal movement is not faster than straight movement", () => {
|
|
const sim = new OfficeSimulation();
|
|
sim.syncPlayer("emp-1", "Dana Lee");
|
|
const start = { x: sim.player!.x, y: sim.player!.y };
|
|
steps(sim, 160, { right: true, down: true });
|
|
const diagonal = Math.hypot(sim.player!.x - start.x, sim.player!.y - start.y);
|
|
const sim2 = new OfficeSimulation();
|
|
sim2.syncPlayer("emp-2", "Alex");
|
|
const start2 = sim2.player!.x;
|
|
steps(sim2, 160, { right: true });
|
|
const straight = sim2.player!.x - start2;
|
|
expect(diagonal).toBeLessThanOrEqual(straight + 0.5);
|
|
expect(diagonal).toBeGreaterThan(straight * 0.7);
|
|
});
|
|
|
|
test("idle agents take a coffee break and carry the cup back to their desk", () => {
|
|
const sim = new OfficeSimulation(() => 0);
|
|
sim.sync([agentInput("a")]);
|
|
const agent = sim.agents[0];
|
|
const coffee = sim.plan.amenities.find((spot) => spot.kind === "coffee")!;
|
|
|
|
let reachedCoffee = false;
|
|
let backSeated = false;
|
|
for (let i = 0; i < 6000; i++) {
|
|
sim.step(16, IDLE);
|
|
if (agent.behavior.kind === "dwelling") {
|
|
expect(agent.x).toBe(coffee.stand.x);
|
|
expect(agent.y).toBe(coffee.stand.y);
|
|
reachedCoffee = true;
|
|
}
|
|
if (reachedCoffee && agent.behavior.kind === "seated") {
|
|
backSeated = true;
|
|
break;
|
|
}
|
|
}
|
|
expect(reachedCoffee).toBe(true);
|
|
expect(backSeated).toBe(true);
|
|
expect(agent.x).toBe(agent.seat.x);
|
|
expect(agent.y).toBe(agent.seat.y);
|
|
expect(agent.carrying).toBe("coffee");
|
|
});
|
|
|
|
test("agents with a run, thinking or busy never leave their desk", () => {
|
|
for (const activity of ["active", "thinking", "busy"] as const) {
|
|
const sim = new OfficeSimulation(() => 0);
|
|
sim.sync([agentInput("ceo", "c_suite", activity), agentInput("a", "staff", activity)]);
|
|
for (let i = 0; i < 6000; i++) {
|
|
sim.step(16, IDLE);
|
|
for (const agent of sim.agents) expect(agent.behavior.kind).toBe("seated");
|
|
}
|
|
}
|
|
});
|
|
|
|
test("an idle refresh keeps a break going; a run starting sends the agent straight back to the desk", () => {
|
|
const sim = new OfficeSimulation(() => 0);
|
|
sim.sync([agentInput("a")]);
|
|
const agent = sim.agents[0];
|
|
for (let i = 0; i < 3000 && agent.behavior.kind === "seated"; i++) sim.step(16, IDLE);
|
|
for (let i = 0; i < 30; i++) sim.step(16, IDLE);
|
|
expect(agent.behavior.kind).toBe("walking");
|
|
expect(agent.behavior.kind === "walking" && agent.behavior.destination).toBe("amenity");
|
|
|
|
sim.sync([agentInput("a")]);
|
|
expect(sim.agents[0].behavior.kind === "walking" && sim.agents[0].behavior.destination).toBe("amenity");
|
|
|
|
sim.sync([agentInput("a", "staff", "active")]);
|
|
const working = sim.agents[0];
|
|
expect(working.behavior.kind === "walking" && working.behavior.destination).toBe("seat");
|
|
for (let i = 0; i < 6000 && working.behavior.kind !== "seated"; i++) {
|
|
sim.step(16, IDLE);
|
|
expect(working.behavior.kind).not.toBe("dwelling");
|
|
}
|
|
expect(working.x).toBe(working.seat.x);
|
|
expect(working.y).toBe(working.seat.y);
|
|
});
|
|
|
|
test("only idle agents gather around the table, and anyone with a run stays at their desk", () => {
|
|
const sim = new OfficeSimulation(() => 0);
|
|
sim.sync([agentInput("a"), agentInput("b"), agentInput("ceo", "c_suite", "active")]);
|
|
// Nobody has settled yet: a meeting cannot be convened.
|
|
expect(sim.conveneMeeting()).toBe(0);
|
|
for (const agent of sim.agents) agent.settledMs = 30_000;
|
|
|
|
const attendees = sim.conveneMeeting();
|
|
expect(attendees).toBeGreaterThanOrEqual(2);
|
|
expect(attendees).toBe(2);
|
|
const walkers = sim.agents.filter((agent) => agent.behavior.kind === "walking");
|
|
expect(walkers).toHaveLength(attendees);
|
|
expect(sim.agents.find((agent) => agent.input.id === "ceo")!.behavior.kind).toBe("seated");
|
|
const spots = new Set(
|
|
walkers.map((agent) => (agent.behavior.kind === "walking" ? `${agent.behavior.spot?.stand.x}` : "")),
|
|
);
|
|
expect(spots.size).toBe(attendees);
|
|
|
|
// Everyone reaches the meeting room, dwells together, and heads home.
|
|
let overlapped = false;
|
|
for (let i = 0; i < 12000 && !overlapped; i++) {
|
|
sim.step(16, IDLE);
|
|
const dwellingInMeeting = sim.agents.filter(
|
|
(agent) => agent.behavior.kind === "dwelling" && agent.behavior.spot.kind === "meeting",
|
|
);
|
|
if (dwellingInMeeting.length >= 2) overlapped = true;
|
|
}
|
|
expect(overlapped).toBe(true);
|
|
});
|
|
|
|
test("a solo office never convenes a meeting", () => {
|
|
const sim = new OfficeSimulation(() => 0);
|
|
sim.sync([agentInput("a")]);
|
|
sim.agents[0].settledMs = 30_000;
|
|
expect(sim.conveneMeeting()).toBe(0);
|
|
expect(sim.agents[0].behavior.kind).toBe("seated");
|
|
});
|
|
|
|
test("clicks hit the agent drawn at that spot, frontmost first", () => {
|
|
const sim = new OfficeSimulation();
|
|
sim.sync([agentInput("a"), agentInput("b")]);
|
|
const first = sim.agents[0];
|
|
expect(sim.hitTest({ x: first.x, y: first.y - 10 })!.input.id).toBe("a");
|
|
expect(sim.hitTest({ x: first.x - 60, y: first.y - 10 })).toBeNull();
|
|
const second = sim.agents[1];
|
|
second.x = first.x;
|
|
second.y = first.y + 6;
|
|
expect(sim.hitTest({ x: first.x, y: first.y })!.input.id).toBe("b");
|
|
});
|
|
|
|
test("camera follows the player and clamps at world edges", () => {
|
|
const sim = new OfficeSimulation();
|
|
sim.sync(Array.from({ length: 30 }, (_, i) => agentInput(`a${i}`)));
|
|
const world = sim.plan.world;
|
|
const centered = cameraFor(world, 600, 400, { x: 700, y: 700 });
|
|
expect(centered).toEqual({ x: 400, y: 500 });
|
|
const topLeft = cameraFor(world, 600, 400, { x: 0, y: 0 });
|
|
expect(topLeft).toEqual({ x: 0, y: 0 });
|
|
const bottomRight = cameraFor(world, 600, 400, { x: world.w, y: world.h });
|
|
expect(bottomRight).toEqual({ x: world.w - 600, y: world.h - 400 });
|
|
const wide = cameraFor(world, world.w + 200, 400, { x: 0, y: 0 });
|
|
expect(wide.x).toBe(-100);
|
|
});
|
|
});
|