import { describe, expect, it } from 'vitest'; import { MAP_H, MAP_W, TILE, VENUE, isWalkable, isWalkableWorld, tileAt, tileColour, tileToWorld, worldToTile, } from '../../src/scenes/floor/venueMap'; import type { AnchorKind, TileKind, WorldPoint } from '../../src/scenes/floor/venueMap'; const ANCHOR_KINDS: readonly AnchorKind[] = ['bar', 'dance', 'booth', 'toilet', 'smoke', 'entry', 'exit']; const allAnchors = (): WorldPoint[] => ANCHOR_KINDS.flatMap((k) => [...VENUE.anchors[k]]); const key = (tx: number, ty: number): number => ty * MAP_W + tx; /** Every walkable tile 4-connected to the seed. */ function flood(seed: WorldPoint): Set { const start = worldToTile(seed.x, seed.y); const seen = new Set(); if (!isWalkable(VENUE, start.tx, start.ty)) return seen; const stack = [start]; seen.add(key(start.tx, start.ty)); while (stack.length > 0) { const cur = stack.pop()!; for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]] as const) { const tx = cur.tx + dx; const ty = cur.ty + dy; const k = key(tx, ty); if (seen.has(k) || !isWalkable(VENUE, tx, ty)) continue; seen.add(k); stack.push({ tx, ty }); } } return seen; } describe('venueMap dimensions', () => { it('matches the shared geometry constants', () => { expect(TILE).toBe(16); expect(VENUE.width).toBe(MAP_W); expect(VENUE.height).toBe(MAP_H); expect(MAP_W * TILE).toBe(1280); expect(MAP_H * TILE).toBe(720); }); it('has one tile per cell, all of known kinds', () => { expect(VENUE.tiles).toHaveLength(MAP_W * MAP_H); const known = new Set([ 'void', 'floor', 'wall', 'bar', 'stool', 'dance', 'booth', 'stall', 'stallDoor', 'exit', 'smokeDoor', 'neon', ]); for (const t of VENUE.tiles) expect(known.has(t)).toBe(true); }); it('is ringed by non-walkable edges apart from doors', () => { for (let tx = 0; tx < MAP_W; tx++) { for (const ty of [0, MAP_H - 1]) { const kind = tileAt(VENUE, tx, ty); if (kind === 'smokeDoor') continue; expect(isWalkable(VENUE, tx, ty)).toBe(false); } } }); it('contains every authored tile kind', () => { const seen = new Set(VENUE.tiles); for (const k of ['floor', 'wall', 'bar', 'stool', 'dance', 'booth', 'stall', 'stallDoor', 'exit', 'smokeDoor', 'neon'] as const) { expect(seen.has(k)).toBe(true); } }); }); describe('tileAt / walkability', () => { it('reports void out of bounds', () => { expect(tileAt(VENUE, -1, 0)).toBe('void'); expect(tileAt(VENUE, 0, -1)).toBe('void'); expect(tileAt(VENUE, MAP_W, 0)).toBe('void'); expect(tileAt(VENUE, 0, MAP_H)).toBe('void'); expect(isWalkable(VENUE, -5, -5)).toBe(false); }); it('walks floor/dance/exit/smokeDoor/stall and nothing else', () => { const walkable = new Set(['floor', 'dance', 'exit', 'smokeDoor', 'stall']); for (let ty = 0; ty < MAP_H; ty++) { for (let tx = 0; tx < MAP_W; tx++) { expect(isWalkable(VENUE, tx, ty)).toBe(walkable.has(tileAt(VENUE, tx, ty))); } } }); it('agrees between tile and world walkability queries', () => { for (const a of allAnchors()) expect(isWalkableWorld(VENUE, a.x, a.y)).toBe(true); expect(isWalkableWorld(VENUE, -8, -8)).toBe(false); }); }); describe('coordinate conversion', () => { it('round-trips tile -> world -> tile', () => { for (let ty = 0; ty < MAP_H; ty += 3) { for (let tx = 0; tx < MAP_W; tx += 3) { const w = tileToWorld(tx, ty); expect(worldToTile(w.x, w.y)).toEqual({ tx, ty }); } } }); it('returns tile centres, and floors world coords', () => { expect(tileToWorld(0, 0)).toEqual({ x: 8, y: 8 }); expect(tileToWorld(3, 5)).toEqual({ x: 56, y: 88 }); expect(worldToTile(0, 0)).toEqual({ tx: 0, ty: 0 }); expect(worldToTile(15.9, 31.9)).toEqual({ tx: 0, ty: 1 }); }); }); describe('anchors', () => { it('meets the minimum counts per kind', () => { const min: Record = { bar: 6, dance: 8, booth: 4, toilet: 4, smoke: 1, entry: 1, exit: 1, }; for (const k of ANCHOR_KINDS) { expect(VENUE.anchors[k].length).toBeGreaterThanOrEqual(min[k]); } }); it('places every anchor on a walkable tile centre', () => { for (const k of ANCHOR_KINDS) { for (const a of VENUE.anchors[k]) { expect(a.x % TILE).toBe(TILE / 2); expect(a.y % TILE).toBe(TILE / 2); expect(isWalkableWorld(VENUE, a.x, a.y)).toBe(true); } } }); it('CRITICAL: every anchor is 4-connected to every other anchor', () => { const anchors = allAnchors(); const first = anchors[0]!; const reachable = flood(first); for (const a of anchors) { const t = worldToTile(a.x, a.y); expect( reachable.has(key(t.tx, t.ty)), `anchor at tile ${t.tx},${t.ty} is cut off from ${JSON.stringify(worldToTile(first.x, first.y))}`, ).toBe(true); } }); it('leaves no orphaned open floor — every non-stall walkable tile is reachable', () => { const reachable = flood(VENUE.anchors.entry[0]!); const orphans: string[] = []; for (let ty = 0; ty < MAP_H; ty++) { for (let tx = 0; tx < MAP_W; tx++) { if (!isWalkable(VENUE, tx, ty) || reachable.has(key(tx, ty))) continue; // Cubicle interiors are sealed behind their doors by design. if (tileAt(VENUE, tx, ty) === 'stall') continue; orphans.push(`${tx},${ty}`); } } expect(orphans, `unreachable open tiles: ${orphans.join(' ')}`).toEqual([]); }); it('anchors of different kinds sit on distinct tiles within their kind', () => { for (const k of ANCHOR_KINDS) { const keys = VENUE.anchors[k].map((a) => { const t = worldToTile(a.x, a.y); return key(t.tx, t.ty); }); expect(new Set(keys).size).toBe(keys.length); } }); }); describe('stalls', () => { it('has four cubicles with unique ids', () => { expect(VENUE.stalls).toHaveLength(4); expect(new Set(VENUE.stalls.map((s) => s.id)).size).toBe(4); for (const s of VENUE.stalls) expect(s.id).toMatch(/^s[1-4]$/); }); it('door tiles are walkable and sit directly south of a stallDoor tile', () => { for (const s of VENUE.stalls) { const d = worldToTile(s.door.x, s.door.y); expect(isWalkable(VENUE, d.tx, d.ty)).toBe(true); expect(tileAt(VENUE, d.tx, d.ty - 1)).toBe('stallDoor'); } }); it('inside points land on stall tiles', () => { for (const s of VENUE.stalls) { // The 2x2 interior centre is a tile corner; probe just inside it. expect(isWalkableWorld(VENUE, s.inside.x - 1, s.inside.y - 1)).toBe(true); const t = worldToTile(s.inside.x - 1, s.inside.y - 1); expect(tileAt(VENUE, t.tx, t.ty)).toBe('stall'); } }); it('seals each cubicle behind its door — the only way in is StallDef.inside', () => { for (const s of VENUE.stalls) { const d = worldToTile(s.door.x, s.door.y); const reachable = flood(s.door); const interior = worldToTile(s.inside.x - 1, s.inside.y - 1); expect(reachable.has(key(interior.tx, interior.ty))).toBe(false); // ...and the door tile still connects back to the rest of the venue. expect(reachable.has(key(worldToTile(VENUE.anchors.entry[0]!.x, VENUE.anchors.entry[0]!.y).tx, worldToTile(VENUE.anchors.entry[0]!.x, VENUE.anchors.entry[0]!.y).ty))).toBe(true); expect(isWalkable(VENUE, d.tx, d.ty)).toBe(true); } }); it('exposes one toilet anchor per stall door', () => { const doors = VENUE.stalls.map((s) => `${s.door.x},${s.door.y}`).sort(); const anchors = VENUE.anchors.toilet.map((a) => `${a.x},${a.y}`).sort(); expect(anchors).toEqual(doors); }); }); describe('tileColour', () => { it('gives every kind a distinct 24-bit colour', () => { const kinds: readonly TileKind[] = [ 'void', 'floor', 'wall', 'bar', 'stool', 'dance', 'booth', 'stall', 'stallDoor', 'exit', 'smokeDoor', 'neon', ]; const colours = kinds.map(tileColour); for (const c of colours) { expect(Number.isInteger(c)).toBe(true); expect(c).toBeGreaterThanOrEqual(0); expect(c).toBeLessThanOrEqual(0xffffff); } expect(new Set(colours).size).toBe(kinds.length); }); });