PROCITY/web/js/citygen/names.js
m3ultra 6fba79a1cb Lane A round 13: v3.0-beta the district (?gigs=1) — 2–4 venues, week schedule, town-wide posters, verge fix
Publishes the multi-venue plan.gigs interface C/D/B/F hang off (half-day handshake, A first).

- pickVenues: seeded 2–4 shops converted in place → pub (spine end) / band_room (warehouse
  fringe) / rsl (off-spine); 4th = 2nd pub at far end. Registry gains weight-0 band_room+rsl
  kinds + venueKind/genreKey/placement. Never the openLate landmark; osm-safe fallbacks.
- gigKey contract (debt #1): gigKeyFor(genreKey) => 'gig-'+genreKey, the single exported
  genre→bed mapping. No table anywhere; F deletes the GIG_BED bridge.
- plan.gigs goes district: 7 nights × every venue, each plays a seeded 4–6 (dark reads true);
  cover skewed by kind; no band plays two venues on one night; custom names spread night-major.
- plan.posters goes town-wide: tonight's playing venues each get frontage + seeded spine run.
- Verge fix (debt #5): new corridor law roadWidth/vergeBand/poleOffset(edge) in registry.js
  (road-vs-verge split of edge.width, published as functions ⇒ base goldens frozen). Posters
  seat on the verge, never bitumen; selfcheck asserts no poster on a carriageway.
- registry pub.interior 'band_room'→'pub' (unread field; avoids collision with band_room type).

Gig golden re-pinned 0xa6ae5a5e→0x1f636349 (multi-venue changed output; in-commit per amendment
law). selfcheck ALL GREEN 3273/3273; scaffold_check + consistency GREEN; flags-off byte-identical.
CITY_SPEC §v3.0-beta + LANE_A_NOTES §Round 13 updated in this commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 22:35:05 +10:00

147 lines
7.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// PROCITY shop & town name generator — seeded, deterministic, 90s-Australian flavour.
// Given a shop's stable seed, produces the same {name, sign} forever. `sign` is the short form
// that fits a signboard (the game renders it big; `name` is the full directory/label string).
//
// Pure: only imports prng (mulberry32) + wordlists. No THREE, no network, no Math.random.
import { mulberry32 } from '../core/prng.js';
import * as W from './wordlists.js';
const pick = (r, arr) => arr[(r() * arr.length) | 0];
const cap = s => s.charAt(0).toUpperCase() + s.slice(1);
const initials = (r) => pick(r, ['A', 'B', 'C', 'D', 'E', 'G', 'J', 'K', 'M', 'P', 'R', 'S', 'T', 'V'])
+ ' & ' + pick(r, ['A', 'B', 'C', 'D', 'E', 'G', 'J', 'K', 'M', 'P', 'R', 'S', 'T', 'V']);
// Token → wordlist resolver. Each token draws from the bank with the given stream.
function tok(r, t) {
switch (t) {
case 'Word': return pick(r, W.WORDS);
case 'Adj': return pick(r, W.ADJ);
case 'Noun': return pick(r, W.NOUN);
case 'Suburb': return pick(r, W.SUBURB);
case 'Family': return pick(r, W.FAMILY);
case 'Saint': return pick(r, W.SAINT);
case 'Charity': return pick(r, W.CHARITY);
case 'First': return pick(r, W.FIRSTNAME);
case 'Animal': return pick(r, W.ANIMAL);
case 'Init': return initials(r);
case 'Pub': return pick(r, W.PUB_NAME); // v3: full pub name ("The Royal")
case 'BandAdj': return pick(r, W.BAND_ADJ); // v3: band-name banks
case 'BandNoun':return pick(r, W.BAND_NOUN);
case 'BandOne': return pick(r, W.BAND_ONEWORD);
default: return t;
}
}
// Fill "{Adj} {Noun}" style token patterns. Possessives are written with a literal s in the
// pattern itself (e.g. "{First}s Records", "{Saint}s Charity Barn") — one mechanism, no magic.
function fill(r, pattern) {
return pattern.replace(/\{(\w+)\}/g, (_, t) => tok(r, t));
}
// ── name patterns per shop type (≥40 across the board) ─────────────────────────────
const PATTERNS = {
record: [
'{Word} Records', '{Suburb} Sound Exchange', '{Adj} Groove', '{Word} & Wax',
'Rotation', '{First}s Records', 'The {Animal} Sound Co.', '{Suburb} Vinyl',
],
opshop: [
'St {Saint}s Opportunity Shop', '{Charity} Op Shop', 'The {Adj} Op Shop',
'{Suburb} Recycled', '{Saint}s Charity Barn', 'Second Time {Suburb}', 'The Op Shop',
],
toy: [
'{First}s Toys', 'The {Adj} Toy Box', '{Word} Toys & Games', '{Animal} Playthings',
'{Suburb} Toyworld-ish', 'Marbles & Kites', 'The Toy Nook',
],
book: [
'The {Adj} Book Barn', '{Suburb} Books', '{Word} & Sons Booksellers', 'The {Animal} Bookshop',
'Dusty Pages', '{Init} Books', 'Second-Hand Reads', 'The Book Cellar',
],
video: [
'{Suburb} Video', '{Adj} Video', 'Video {Word}', '{First}s Video',
'The Picture Show', 'Late Nite Video', 'Video Barn',
],
pawn: [
'{Suburb} Cash Converters-ish', '{First}s Pawn & Loan', '{Adj} Cash', 'The Cash Corner',
'{Init} Trading Co.', 'Quick Cash Pawn', 'Second Chance Loans',
],
milkbar: [
'{Family} Milk Bar', '{Suburb} Milk Bar', 'The {Adj} Corner Store', '{First}s Milk Bar',
'{Family}s Deli & Milk Bar', 'Corner Store', 'The Milk Bar',
],
dept: [
'{Suburb} Emporium', '{Family} & Co.', 'The Grand {Word}', '{Adj} Department Store',
'{Suburb} Trading Co.', 'Palais {Word}',
],
stall: [
'{First}s Stall', '{Word} Stall', '{Adj} Bits', 'Trash & Treasure', '{Animal} Wares',
'Bric-a-Brac', 'Sunday Stall',
],
pub: [ // v3 venue (?gigs=1): the town pub/hotel
'{Pub}', 'The {Suburb} Hotel', 'The {Family} Hotel', 'The {Adj} Arms', '{Pub}',
],
band_room: [ // v3 venue (ROUND13): the tin shed on the warehouse fringe
'The {Adj} Warehouse', '{Suburb} Band Room', 'The {Animal} Room', 'The {Word} Shed',
'The Tote-ish', 'The {Adj} Room', '{Word} Rehearsal Rooms',
],
rsl: [ // v3 venue (ROUND13): the returned & services league club — carpet, bistro, meat raffle
'{Suburb} RSL Club', '{Suburb} RSL', '{Suburb} Services Club', 'The {Suburb} Memorial Club',
'{Suburb} Workers Club', '{Suburb} Returned Servicemens Club',
],
};
// ── band-name patterns (v3 gigs, 90s Aussie pub-rock) ──────────────────────────────
const BAND_PATTERNS = [
'The {BandAdj} {BandNoun}', '{BandAdj} {BandNoun}', 'The {BandNoun}', '{BandOne}',
'{First} & The {BandNoun}', 'The {Animal}s', '{BandAdj} {Animal}', 'The {BandOne}s',
];
// ── short signboard forms (what actually fits above the door) ──────────────────────
const SIGN_PATTERNS = {
record: ['{Word}', 'RECORDS', 'VINYL', '{Suburb} SOUND'],
opshop: ['OP SHOP', 'ST {Saint}S', '{Charity}', 'RECYCLED'],
toy: ['TOYS', '{Adj} TOYS', 'TOY BOX', 'GAMES'],
book: ['BOOKS', 'BOOK BARN', '{Suburb} BOOKS', 'READS'],
video: ['VIDEO', '{Suburb} VIDEO', 'MOVIES', 'RENTALS'],
pawn: ['CASH', 'PAWN', 'LOANS', '{Init}'],
milkbar: ['MILK BAR', '{Family}', 'DELI', 'CORNER STORE'],
dept: ['{Suburb}', 'EMPORIUM', '{Family} & CO'],
stall: ['STALL', 'BITS', 'BRIC-A-BRAC', 'WARES'],
pub: ['HOTEL', 'THE ROYAL', '{Suburb} HOTEL', 'PUB'],
band_room: ['BAND ROOM', 'LIVE MUSIC', '{Word} ROOM', 'THE SHED'],
rsl: ['RSL', '{Suburb} RSL', 'SERVICES CLUB', 'MEMORIAL CLUB'],
};
// Public: name a shop from its stable seed + type. Returns { name, sign }.
export function shopName(seed, type) {
const r = mulberry32((seed ^ 0x9e3779b9) >>> 0); // decorrelate from other seed uses
const pats = PATTERNS[type] || PATTERNS.stall;
const signs = SIGN_PATTERNS[type] || SIGN_PATTERNS.stall;
const name = fill(r, pick(r, pats)).replace(/\s+/g, ' ').trim();
let sign = fill(r, pick(r, signs)).replace(/\s+/g, ' ').trim();
// Signboard can't be too long — fall back to a punchy word from the full name.
if (sign.length > 16) {
const word = name.split(' ').sort((a, b) => b.length - a.length)[0] || sign;
sign = word.slice(0, 16).toUpperCase();
}
return { name, sign };
}
// Public (v3 gigs): generate a band name from a stable seed. Pure — the OPTIONAL custom-name
// drop-in is mixed in by gigs.js (which calls this only when it needs a fresh generated name), so
// this stays node-testable and fetch-free. Deterministic: same seed ⇒ same name, forever.
export function bandName(seed) {
const r = mulberry32((seed ^ 0x1b56c4e9) >>> 0); // decorrelate from shop/town seed uses
return fill(r, pick(r, BAND_PATTERNS)).replace(/\s+/g, ' ').trim();
}
// Public: name the town from the city seed.
export function townName(citySeed) {
const r = mulberry32((citySeed ^ 0x2545f491) >>> 0);
const root = pick(r, W.TOWN_ROOT);
const suffix = pick(r, W.TOWN_SUFFIX);
const usePrefix = r() < 0.35;
const prefix = usePrefix ? pick(r, W.TOWN_PREFIX) + ' ' : '';
return `${prefix}${cap(root)}${suffix ? ' ' + suffix : ''}`.trim();
}