not-tonight/tests/floor/layout-theRoyal.test.ts
m3ultra 4caa901df2 Four venues, four rooms: the ladder stops being one map with different numbers
The venue ladder has always listed four venues in data/venues.ts, but they
shared ONE floor map and differed only by difficulty knobs — so a promotion you
survived a whole week for looked exactly like the room you had just left.

- venueMap.ts grows a FloorLayout contract (map + props + lights + posts +
  probes + palette) and the newGrid() primitives every room is painted with.
  Voltage stays IN venueMap rather than moving to layouts/, so the new rooms can
  import primitives from it without closing an import cycle.
- Three new rooms in src/scenes/floor/layouts/: The Royal (horseshoe public bar,
  pool room, pokies corner, trough + two cubicles, a beer garden that is plainly
  the nicest room in the pub, and a DJ "corner" that is a folding table because
  this pub never built a booth), Elevate (open-air rooftop — most of the grid is
  sky, the DJ is on an unwalled plinth, you arrive by lift), ROOM (concrete
  warehouse, pillars you path around, central booth, loading-dock smoking area,
  twelve lights in the whole venue).
- Nothing about a room is a module singleton any more. FloorView, sweep and
  FloorDemoScene take the layout; the door picks its street plate by venue id.
- FloorDemoScene's six hardcoded station spots (TAPS_SPOT, DECKS_SPOT, ...) were
  Voltage's tile coordinates. At four venues a hardcoded (47,3) puts the bar
  shift inside The Royal's pool room, so each layout now names its own stations
  and the scene reads them from there.
- tests/floor/layoutInvariants.ts is an executable rulebook: perimeter holds,
  every walkable tile reachable from the entry, anchors on walkable ground,
  posts not sealed (a post on a sealed tile freezes the player for the night —
  that has shipped twice), probes reachable, props on-grid. Proved against
  Voltage BEFORE the new rooms were authored against it.

Dev route #floor:<venueId> boots the floor straight into one room, because
otherwise seeing ROOM means surviving three weeks of the ladder.

Gate: lint clean, build clean, 821 tests passing (was 784).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 19:51:54 +10:00

158 lines
7.1 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { THE_ROYAL } from '../../src/scenes/floor/layouts/theRoyal';
import { isWalkable, tileAt, worldToTile } from '../../src/scenes/floor/venueMap';
import { assertLayout } from './layoutInvariants';
const map = THE_ROYAL.map;
const countTiles = (predicate: (kind: ReturnType<typeof tileAt>) => boolean): number => {
let n = 0;
for (let ty = 0; ty < map.height; ty++) {
for (let tx = 0; tx < map.width; tx++) if (predicate(tileAt(map, tx, ty))) n++;
}
return n;
};
const propsOf = (kind: string) => THE_ROYAL.props.filter((p) => p.kind === kind);
describe('THE_ROYAL layout', () => {
it('satisfies the shared venue invariants', () => {
assertLayout(THE_ROYAL, { bar: 6, dance: 4, booth: 2, toilet: 2, smoke: 2, entry: 1, exit: 1 });
});
it('matches the venue id the ladder looks it up by', () => {
// data/venues.ts finds a night's room by VenueDef.id — a typo here is a
// venue that silently falls back to Voltage.
expect(THE_ROYAL.id).toBe('theRoyal');
});
// ---- the horseshoe ---------------------------------------------------------
it('wraps the public bar in counter on three sides with a workable pit inside', () => {
expect(tileAt(map, 22, 17)).toBe('bar'); // north run
expect(tileAt(map, 22, 25)).toBe('bar'); // south run
expect(tileAt(map, 28, 21)).toBe('bar'); // the bend
// The open west end is the whole point: the taps post lives in the pit, so
// walling this in freezes the bar shift for the night.
expect(isWalkable(map, 15, 21)).toBe(true);
const taps = THE_ROYAL.posts.taps;
expect(taps).toBeDefined();
expect(tileAt(map, taps!.tx, taps!.ty)).toBe('floor');
});
// ---- the DJ corner ---------------------------------------------------------
it('gives the DJ a folding table on the carpet, not a booth', () => {
expect(countTiles((k) => k === 'djbooth' || k === 'djfloor')).toBe(0);
const decks = THE_ROYAL.posts.decks;
expect(decks).toBeDefined();
expect(isWalkable(map, decks!.tx, decks!.ty)).toBe(true);
for (const p of [...propsOf('deck'), ...propsOf('mixer'), ...propsOf('highTable')]) {
if (p.tx < 50 || p.ty < 33) continue; // the pool-room leaner is not DJ gear
expect(isWalkable(map, p.tx, p.ty), `'${p.id}' is not on plain floor`).toBe(true);
}
});
// ---- the dunnies -----------------------------------------------------------
it('has two cubicles, one of them plainly out of order', () => {
expect(map.stalls).toHaveLength(2);
expect(map.anchors.toilet).toHaveLength(2);
for (const stall of map.stalls) {
const inside = worldToTile(stall.inside.x, stall.inside.y);
expect(tileAt(map, inside.tx, inside.ty)).toBe('stall');
}
// The story prop: a wet floor sign and a mop bucket parked at cubicle two.
const wet = propsOf('wetFloor');
expect(wet).toHaveLength(1);
expect(propsOf('mopBucket')).toHaveLength(1);
const secondDoor = worldToTile(map.stalls[1]!.door.x, map.stalls[1]!.door.y);
expect(Math.abs(wet[0]!.tx - secondDoor.tx)).toBeLessThanOrEqual(2);
});
it('plumbs the dunnies with a trough and a single basin', () => {
expect(propsOf('trough')).toHaveLength(1);
expect(propsOf('sinkRow')).toHaveLength(1);
// Both stand on 'sink' tiles, which are furniture: you cannot walk the trough.
for (const p of [...propsOf('trough'), ...propsOf('sinkRow')]) {
expect(tileAt(map, p.tx, p.ty), `'${p.id}' is not plumbed in`).toBe('sink');
}
});
// ---- the beer garden -------------------------------------------------------
it('opens the beer garden to the sky inside a fence, with one gate', () => {
const yard = countTiles((k) => k === 'yard');
expect(yard).toBeGreaterThan(200);
// Every yard tile must sit inside the fence rectangle, or the rain falls in
// the pub (the renderer keys the weather off 'yard', not off a room id).
for (let ty = 0; ty < map.height; ty++) {
for (let tx = 0; tx < map.width; tx++) {
if (tileAt(map, tx, ty) !== 'yard') continue;
expect(tx).toBeGreaterThan(12);
expect(tx).toBeLessThan(45);
expect(ty).toBeGreaterThan(33);
expect(ty).toBeLessThan(43);
}
}
expect(countTiles((k) => k === 'smokeDoor')).toBe(1);
expect(tileAt(map, 29, 33)).toBe('smokeDoor');
expect(tileAt(map, 29, 32)).toBe('floor');
expect(tileAt(map, 29, 34)).toBe('yard');
// It is the nicest room in the pub, so it is furnished like one.
expect(propsOf('picnicTable').length).toBeGreaterThanOrEqual(4);
expect(propsOf('beerUmbrella').length).toBeGreaterThanOrEqual(2);
expect(propsOf('patioHeater')).toHaveLength(1);
expect(propsOf('buttBin')).toHaveLength(1);
});
// ---- the rooms that are not the dance floor ---------------------------------
it('keeps the dance floor smaller than the pool room it plays second fiddle to', () => {
const dance = countTiles((k) => k === 'dance');
expect(dance).toBeGreaterThan(0);
// Voltage's floor is 375 tiles. This one is an afterthought bolted on in 2004.
expect(dance).toBeLessThan(150);
expect(propsOf('poolTable')).toHaveLength(2);
expect(propsOf('cueRack')).toHaveLength(1);
expect(propsOf('dartboard')).toHaveLength(1);
expect(propsOf('pokie')).toHaveLength(6);
expect(propsOf('tabBoard').length).toBeGreaterThanOrEqual(2);
expect(propsOf('bistroTable').length).toBeGreaterThanOrEqual(4);
expect(propsOf('bainMarie')).toHaveLength(1);
expect(propsOf('raffleDrum')).toHaveLength(1);
expect(propsOf('jukebox')).toHaveLength(1);
expect(propsOf('discoball')).toHaveLength(1);
});
// ---- light signature -------------------------------------------------------
it('lights the pub amber and spends its one signature colour on the pokies', () => {
const AMBER = 0xd8a020;
const POKIE_BLUE = 0x4aa0c0;
const allowed = new Set([
AMBER, POKIE_BLUE,
0x30a050, // pool-table cone
0x50c060, // dunnies fluoro
0xd03470, // dance + entry pink
0x3060c0, // DJ
0x6080a0, // beer garden
]);
for (const l of THE_ROYAL.lights) {
expect(allowed.has(l.colour), `light '${l.id}' invents a colour`).toBe(true);
}
expect(THE_ROYAL.lights.filter((l) => l.colour === AMBER).length).toBeGreaterThanOrEqual(6);
expect(THE_ROYAL.lights.filter((l) => l.colour === POKIE_BLUE).length).toBeGreaterThanOrEqual(2);
expect(THE_ROYAL.lights.filter((l) => l.colour === 0x30a050)).toHaveLength(2);
// No pink anywhere but the dance corner and the way out (VENUES.md §2).
for (const l of THE_ROYAL.lights) {
if (l.colour !== 0xd03470) continue;
const onDance = tileAt(map, l.tx, l.ty) === 'dance';
expect(onDance || l.id === 'entry', `pink light '${l.id}' is loose in the pub`).toBe(true);
if (onDance) expect(l.pulse).toBe('beat');
}
});
it('lays pub carpet rather than club floor', () => {
expect(THE_ROYAL.palette?.floor).toBeDefined();
// Warm: more red than blue, which is the whole difference from Voltage.
const floor = THE_ROYAL.palette!.floor!;
expect((floor >> 16) & 0xff).toBeGreaterThan(floor & 0xff);
});
});