import { describe, expect, it } from 'vitest'; import { CARS, buy, carById, createGarage, current, nextUnlock, owns, take, virtues, } from './garage'; import { applyWear, deriveHandling, freshCondition } from './car'; describe('the catalogue', () => { it('gives you something to drive for nothing', () => { // There is always a car. The loop's dead end was a player with no money and // nothing that ran, and the garage exists to make that state unreachable. const garage = createGarage(); expect(garage.owned.length).toBeGreaterThan(0); expect(CARS[0]!.cost).toBe(0); expect(current(garage)).toBeTruthy(); }); it('has no car that beats another at everything', () => { // The invariant the whole design rests on. Suspicion is not tied to // capability here — a lorry is huge and tough and nobody looks twice — // so the thing keeping the catalogue honest is not a ladder but the // absence of a dominant option. If any vehicle were at least as good on // speed, protection, discretion *and* handling, picking the car would // stop being a decision and the loop would lose its only one. const axes = ['speed', 'protection', 'discretion', 'handling'] as const; for (const a of CARS) { for (const b of CARS) { if (a === b) continue; const va = virtues(a); const vb = virtues(b); const dominates = axes.every((k) => va[k] >= vb[k]); expect(dominates, `${a.name} dominates ${b.name}`).toBe(false); } } }); it('keeps the civilian half quiet however capable it gets', () => { // An ordinary thing doing an ordinary thing is invisible whatever its // specification. This is what stops "buy protection" and "stay unnoticed" // being the same axis. const lorry = carById('lorry'); const sports = carById('sports'); expect(lorry.presence).toBeLessThan(sports.presence / 2); // ...even though it is far better protected than the flashy one. expect(lorry.fragility).toBeLessThan(sports.fragility); }); it('charges attention only for the things that do not belong here', () => { const conspicuous = CARS.filter((c) => c.presence > 1.5).map((c) => c.id); expect(conspicuous.sort()).toEqual(['apc', 'sports']); }); it('does not put discretion up for sale', () => { // Being unremarkable must stay available for nothing, or the early game is // simply the worst version of the late one. The quietest vehicle in the // catalogue is one of the two cheapest. const byQuiet = [...CARS].sort((a, b) => a.presence - b.presence); const byPrice = [...CARS].sort((a, b) => a.cost - b.cost); expect(byPrice.slice(0, 2).map((c) => c.id)).toContain(byQuiet[0]!.id); }); it('starts you in the small car and nothing else', () => { const garage = createGarage(); expect(garage.owned).toEqual(['runabout']); expect(current(garage).cost).toBe(0); }); }); describe('buying and taking cars', () => { it('will not sell you what you cannot afford', () => { const garage = createGarage(); const dear = CARS[CARS.length - 1]!; const { bought, funds } = buy(garage, dear.id, dear.cost - 1); expect(bought).toBe(false); expect(funds).toBe(dear.cost - 1); expect(owns(garage, dear.id)).toBe(false); }); it('takes the money once and only once', () => { const garage = createGarage(); const car = CARS[1]!; const first = buy(garage, car.id, car.cost + 50); expect(first.bought).toBe(true); expect(first.funds).toBe(50); // Buying it again is not a way to lose fifty more. const second = buy(garage, car.id, first.funds); expect(second.bought).toBe(false); expect(second.funds).toBe(50); }); it('only lets you drive what is in the garage', () => { const garage = createGarage(); expect(take(garage, CARS[2]!.id)).toBe(false); expect(current(garage).id).toBe(CARS[0]!.id); buy(garage, CARS[2]!.id, 99999); expect(take(garage, CARS[2]!.id)).toBe(true); expect(current(garage).id).toBe(CARS[2]!.id); }); it('points at the next thing worth saving for, until there is none', () => { const garage = createGarage(); expect(nextUnlock(garage)!.id).toBe(CARS[1]!.id); for (const car of CARS) buy(garage, car.id, 99999); expect(nextUnlock(garage)).toBeNull(); }); it('hands back a real car for an id it does not know', () => { // Saves outlive catalogues; a renamed car must not leave the player on foot. expect(carById('no-such-car').id).toBe(CARS[0]!.id); }); }); describe('what the car you picked actually changes', () => { const fresh = freshCondition(); const runabout = carById('runabout'); const tractor = carById('tractor'); const armoured = carById('apc'); it('drives like the car in the catalogue, not like one car with a skin', () => { const slow = deriveHandling(fresh, tractor); const quick = deriveHandling(fresh, armoured); expect(slow.engineForce).toBeCloseTo(tractor.engineForce, 6); expect(quick.engineForce).toBeCloseTo(armoured.engineForce, 6); // A tractor turns tighter than an armoured car and grips less. expect(slow.maxSteer).toBeGreaterThan(quick.maxSteer); expect(slow.frictionSlip).toBeLessThan(quick.frictionSlip); }); it('still leaves a ruined car driveable, whichever car it is', () => { // The garage exists so that being in a bad way is a situation rather than // an ending. A car that stops entirely at 0% is a fail state wearing a dial. const ruined = { level: { engine: 0, tires: 0, chassis: 0 }, ceiling: { engine: 1, tires: 1, chassis: 1 }, }; for (const spec of CARS) { const h = deriveHandling(ruined, spec); expect(h.engineForce).toBeGreaterThan(0); expect(h.maxSteer).toBeGreaterThan(0); expect(h.brakeForce).toBeGreaterThan(0); } }); it('makes armour worth the attention it costs', () => { // The same crash, in the thing with no protection and the thing built for it. const crash = { dt: 1 / 60, distance: 0.4, throttle: 0, impactForce: 2.47e6 }; const inTin = applyWear(fresh, crash, runabout.fragility); const inArmour = applyWear(fresh, crash, armoured.fragility); // Tin: one head-on and it is all but finished. The per-step cap is what // stops it reading as exactly zero. expect(inTin.level.chassis).toBeLessThan(0.25); // Steel: the same crash is a bad afternoon. expect(inArmour.level.chassis).toBeGreaterThan(0.5); // And the permanent scar scales with it too. expect(inArmour.ceiling.chassis).toBeGreaterThan(inTin.ceiling.chassis); }); it('leaves the free car genuinely fragile, so the trade has two sides', () => { // If the starting car merely went slower it would be a punishment rather // than a choice. It has to actually be made of tin. const knock = { dt: 1 / 60, distance: 0.4, throttle: 0, impactForce: 6e5 }; const tin = applyWear(fresh, knock, runabout.fragility); const steel = applyWear(fresh, knock, armoured.fragility); expect(1 - tin.level.chassis).toBeGreaterThan((1 - steel.level.chassis) * 3); }); it('gives the tractor real protection, since it is a lump of iron', () => { // "Very slow, some protection": it is not the fragile one, the runabout is. expect(tractor.fragility).toBeLessThan(runabout.fragility); expect(virtues(tractor).speed).toBeLessThan(virtues(runabout).speed); }); });