California gets roads, traffic, and a car to follow
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { Plan } from "../interiors/plan.ts";
|
||||
import {
|
||||
createWalker,
|
||||
normalizeWalkerAction,
|
||||
type WalkerOptions,
|
||||
} from "../interiors/walker.ts";
|
||||
import type { Level, Office, Room, Wall } from "../interiors/types.ts";
|
||||
|
||||
const ROOM: Room = {
|
||||
id: "floor",
|
||||
name: "Floor",
|
||||
floor: "floor" as never,
|
||||
outline: [
|
||||
{ x: 0, z: 0 },
|
||||
{ x: 10, z: 0 },
|
||||
{ x: 10, z: 8 },
|
||||
{ x: 0, z: 8 },
|
||||
],
|
||||
};
|
||||
|
||||
function planWith(walls: Wall[] = []): Plan {
|
||||
const level: Level = {
|
||||
id: "ground",
|
||||
name: "Ground",
|
||||
elevation: 0,
|
||||
wallHeight: 3,
|
||||
wallThickness: 0.1,
|
||||
floorplan: { rooms: [ROOM], walls },
|
||||
};
|
||||
const office: Office = { id: "walk-test", name: "Walk Test", levels: [level], viewpoints: [] };
|
||||
return new Plan(office, { warn: false });
|
||||
}
|
||||
|
||||
function walker(plan: Plan, over: Partial<WalkerOptions> = {}) {
|
||||
return createWalker(plan, {
|
||||
levelId: "ground",
|
||||
position: { x: 2, z: 2 },
|
||||
speed: 1,
|
||||
fixedStep: 0.1,
|
||||
...over,
|
||||
});
|
||||
}
|
||||
|
||||
describe("walker input and clock", () => {
|
||||
it("normalizes planar actions without amplifying smaller input", () => {
|
||||
assert.deepEqual(normalizeWalkerAction({ x: 0.3, z: -0.4 }), { x: 0.3, z: -0.4 });
|
||||
const diagonal = normalizeWalkerAction({ x: 1, z: 1 });
|
||||
assert.ok(Math.abs(Math.hypot(diagonal.x, diagonal.z) - 1) < 1e-12);
|
||||
assert.deepEqual(normalizeWalkerAction({ x: Number.NaN, z: 1 }), { x: 0, z: 0 });
|
||||
});
|
||||
|
||||
it("moves only in fixed steps and is deterministic across frame chunking", () => {
|
||||
const plan = planWith();
|
||||
const one = walker(plan);
|
||||
const many = walker(plan);
|
||||
one.tick(0.05, { x: 1, z: 0 });
|
||||
assert.deepEqual(one.state().position, { x: 2, z: 2 });
|
||||
one.tick(0.35, { x: 1, z: 0 });
|
||||
for (let index = 0; index < 4; index += 1) many.tick(0.1, { x: 1, z: 0 });
|
||||
assert.deepEqual(one.state(), many.state());
|
||||
assert.ok(Math.abs(one.state().distance - 0.4) < 1e-12);
|
||||
});
|
||||
|
||||
it("returns defensive state snapshots", () => {
|
||||
const controller = walker(planWith());
|
||||
const leaked = controller.state();
|
||||
leaked.position.x = Number.NaN;
|
||||
assert.deepEqual(controller.state().position, { x: 2, z: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("walker collision", () => {
|
||||
it("sweeps its circular footprint and cannot tunnel through a wall", () => {
|
||||
const plan = planWith([{ id: "divider", from: { x: 5, z: 0 }, to: { x: 5, z: 8 } }]);
|
||||
const controller = walker(plan, { speed: 100, fixedStep: 0.1 });
|
||||
const state = controller.tick(0.1, { x: 1, z: 0 });
|
||||
assert.ok(state.position.x < 4.65 && state.position.x > 4.64, `${state.position.x}`);
|
||||
assert.equal(plan.blocked("ground", state.position, state.position, 0.3), false);
|
||||
});
|
||||
|
||||
it("slides the unblocked component of diagonal movement along a wall", () => {
|
||||
const plan = planWith([{ id: "divider", from: { x: 5, z: 0 }, to: { x: 5, z: 8 } }]);
|
||||
const controller = walker(plan, { position: { x: 4.6, z: 2 }, speed: 2 });
|
||||
for (let index = 0; index < 10; index += 1) controller.tick(0.1, { x: 1, z: 1 });
|
||||
const state = controller.state();
|
||||
assert.ok(state.position.x < 4.65, `${state.position.x}`);
|
||||
assert.ok(state.position.z > 3, `${state.position.z}`);
|
||||
assert.equal(plan.blocked("ground", state.position, state.position, 0.3), false);
|
||||
});
|
||||
|
||||
it("walks through a resolved door gap without a door-specific exception", () => {
|
||||
const plan = planWith([
|
||||
{
|
||||
id: "divider",
|
||||
from: { x: 5, z: 0 },
|
||||
to: { x: 5, z: 8 },
|
||||
openings: [{ kind: "door", start: 3.4, width: 1.2, sill: 0, head: 2.1 }],
|
||||
},
|
||||
]);
|
||||
const controller = walker(plan, { position: { x: 4, z: 4 }, speed: 2 });
|
||||
for (let index = 0; index < 10; index += 1) controller.tick(0.1, { x: 1, z: 0 });
|
||||
assert.ok(controller.state().position.x > 5.5, `${controller.state().position.x}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("walker guardrails", () => {
|
||||
it("stays within finite level bounds and ignores invalid time/input", () => {
|
||||
const controller = walker(planWith(), { position: { x: 9.6, z: 4 }, speed: 10 });
|
||||
controller.tick(Number.NaN, { x: 1, z: 0 });
|
||||
controller.tick(1, { x: Number.POSITIVE_INFINITY, z: 0 });
|
||||
assert.deepEqual(controller.state().position, { x: 9.6, z: 4 });
|
||||
controller.tick(1, { x: 1, z: 0 });
|
||||
const state = controller.state();
|
||||
assert.equal(state.position.x, 9.7);
|
||||
assert.ok(Number.isFinite(state.position.x) && Number.isFinite(state.distance));
|
||||
});
|
||||
|
||||
it("resets atomically and rejects invalid spawns/configuration", () => {
|
||||
const plan = planWith();
|
||||
const controller = walker(plan);
|
||||
controller.tick(0.2, { x: 1, z: 0 });
|
||||
assert.deepEqual(controller.reset({ levelId: "ground", position: { x: 7, z: 6 } }).position, { x: 7, z: 6 });
|
||||
assert.equal(controller.state().distance, 0);
|
||||
assert.throws(() => controller.reset({ levelId: "ground", position: { x: Number.NaN, z: 1 } }), RangeError);
|
||||
assert.deepEqual(controller.state().position, { x: 7, z: 6 });
|
||||
assert.throws(() => walker(plan, { radius: 0 }), RangeError);
|
||||
assert.throws(() => walker(plan, { position: { x: 11, z: 2 } }), RangeError);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user