drive-between-the-lines/src/sim/sim.test.ts
dejvino 73e8ac68f1 Phase 2: bases, quest board, and known-vs-new targets
Four allied bases, placed by greedy farthest-point so no choke point can strand
the player, with the first at the spawn junction so the loop is available at
once. Missions hand in at any base, not the issuing one.

The board offers known and new targets side by side and describes each route in
words. Crucially it never reads heat directly — it reads sim/intel.ts, a record
of what the player has actually observed and when. Notes age while roads keep
escalating unwatched, so a known route is knowable, stale, and never a promise.
Recon missions survey everything within 130m, which is how a new target becomes
a known one without driving every road there.

New targets pay 1.8x. That multiplier is the phase's tuning dial: too low and
nobody takes the unknown, too high and nobody takes the known.

Payment is parts, which repair the car. To keep "decline, not reset" intact,
each subsystem gains a ceiling that falls permanently with damage — repairs
restore up to what the car is still capable of, never to what it was. Without an
economy the board's rewards had no stakes; with one, the risky job is what keeps
the car running a while longer.

Adds Dijkstra over the road graph, taking a cost function so a "safest route"
preview is a small change later.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:06:32 +02:00

111 lines
3.8 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { generateWorld } from './world';
import { applyWear, deriveHandling, freshCondition, repair, SUBSYSTEMS, type CarCondition } from './car';
describe('world generation', () => {
it('is reproducible from a seed', () => {
expect(generateWorld(42)).toEqual(generateWorld(42));
});
it('differs between seeds', () => {
expect(generateWorld(42)).not.toEqual(generateWorld(43));
});
it('keeps the spawn point clear', () => {
const world = generateWorld(7);
for (const o of world.obstacles) {
expect(Math.hypot(o.x - world.spawn.x, o.z - world.spawn.z)).toBeGreaterThanOrEqual(12);
}
});
});
describe('car condition', () => {
it('never recovers on its own', () => {
let c = freshCondition();
for (let i = 0; i < 600; i++) {
const next = applyWear(c, { dt: 1 / 60, distance: 0.4, throttle: 1, impactForce: 0 });
expect(next.level.engine).toBeLessThanOrEqual(c.level.engine);
expect(next.level.tires).toBeLessThanOrEqual(c.level.tires);
c = next;
}
});
it('bottoms out rather than going negative', () => {
let c = freshCondition();
for (let i = 0; i < 200; i++) {
c = applyWear(c, { dt: 1 / 60, distance: 5, throttle: 1, impactForce: 5e5 });
}
expect(c.level.chassis).toBe(0);
expect(c.level.tires).toBe(0);
});
it('makes a worn car measurably worse to drive', () => {
const fresh = deriveHandling(freshCondition());
const worn = deriveHandling({
level: { engine: 0.3, tires: 0.3, chassis: 0.3 },
ceiling: { engine: 1, tires: 1, chassis: 1 },
});
expect(worn.engineForce).toBeLessThan(fresh.engineForce);
expect(worn.frictionSlip).toBeLessThan(fresh.frictionSlip);
expect(worn.steeringPull).toBeGreaterThan(fresh.steeringPull);
});
});
describe('repair', () => {
const damaged = () => {
let c = freshCondition();
for (let i = 0; i < 40; i++) {
c = applyWear(c, { dt: 1 / 60, distance: 3, throttle: 1, impactForce: 2e5 });
}
return c;
};
it('lowers the ceiling permanently, so a car never comes back whole', () => {
const c = damaged();
expect(c.ceiling.chassis).toBeLessThan(1);
// No amount of parts can undo it.
const restored = repair(c, 99).condition;
expect(restored.level.chassis).toBeLessThan(1);
expect(restored.level.chassis).toBeCloseTo(c.ceiling.chassis, 6);
});
it('never lifts a subsystem above its ceiling', () => {
const c = damaged();
const restored = repair(c, 99).condition;
for (const part of SUBSYSTEMS) {
expect(restored.level[part]).toBeLessThanOrEqual(restored.ceiling[part] + 1e-9);
}
});
it('hands back parts it could not use', () => {
const { unused } = repair(freshCondition(), 5);
expect(unused).toBeCloseTo(5, 6);
});
it('spends a scarce part on the worst subsystem', () => {
const c: CarCondition = {
level: { engine: 0.9, tires: 0.2, chassis: 0.8 },
ceiling: { engine: 1, tires: 1, chassis: 1 },
};
const restored = repair(c, 0.1).condition;
expect(restored.level.tires).toBeCloseTo(0.3, 6);
expect(restored.level.engine).toBe(0.9);
});
it('ratchets down over repeated damage-and-repair cycles', () => {
let c = freshCondition();
let previousCeiling = 1;
for (let cycle = 0; cycle < 5; cycle++) {
for (let i = 0; i < 30; i++) {
c = applyWear(c, { dt: 1 / 60, distance: 3, throttle: 1, impactForce: 2e4 });
}
c = repair(c, 99).condition;
// Fully repaired every cycle, and still worse off every cycle.
expect(c.level.chassis).toBeCloseTo(c.ceiling.chassis, 6);
expect(c.ceiling.chassis).toBeLessThan(previousCeiling);
previousCeiling = c.ceiling.chassis;
}
expect(previousCeiling).toBeGreaterThan(0);
});
});