clone

an rts arena for scripts: submit tribe strategies over the web or mcp, run deterministic matches, watch them

main
tribes / worker / src / friendly-id.ts
friendly-id.ts 9.2K
1/**2 * Friendly ids: `adjective-noun` instead of raw random tails.3 *4 * One shared adjective pool; three DISJOINT noun pools (users=birds,5 * scripts=beasts, sims=places) so a bare name always reveals its category6 * (disjointness is enforced by a test). Each entity's natural seed feeds a7 * deterministic pick: users ← external id, scripts ← source hash + owner,8 * sims ← game seed + creator. Collisions are handled by the caller retrying9 * with attempt+1, which appends a short numeric tag (`brave-otter-2`).10 *11 * NOTE: friendly ids are ENUMERABLE — they are names, not secrets. Anything12 * addressable by one must be safe to expose or authz-checked (script sources13 * are owner-only; sim bundles are public by design).14 */1516export const ADJECTIVES = [17 'amber', 'ancient', 'ashen', 'bold', 'brave', 'bright', 'brisk', 'bronze',18 'calm', 'clever', 'copper', 'crimson', 'curious', 'dappled', 'dawn', 'dusky',19 'eager', 'early', 'ebon', 'elder', 'ember', 'fabled', 'fierce', 'flint',20 'frosty', 'gentle', 'gilded', 'glad', 'golden', 'granite', 'grim', 'hardy',21 'hidden', 'hollow', 'humble', 'iron', 'ivory', 'jade', 'keen', 'kindled',22 'late', 'lively', 'lone', 'lucky', 'lunar', 'merry', 'misty', 'mossy',23 'nimble', 'noble', 'northern', 'oaken', 'pale', 'patient', 'proud', 'quiet',24 'rapid', 'restless', 'rustic', 'sable', 'sage', 'salt', 'shrewd', 'silent',25 'silver', 'sly', 'solemn', 'stark', 'steady', 'stern', 'still', 'stone',26 'sunny', 'swift', 'tidal', 'true', 'umber', 'vivid', 'wandering', 'wary',27 'weathered', 'wild', 'winter', 'zealous',28 // second wave — the pool is part of the combinatorial budget: with the29 // two-adjective escalation tier the name space passes a million per30 // category long before numeric tags appear.31 'airy', 'arid', 'august', 'autumn', 'blazing', 'blue', 'boreal', 'briny',32 'bristling', 'burnished', 'cedar', 'certain', 'cinder', 'civil', 'cloudy',33 'coastal', 'cobalt', 'crested', 'crystal', 'daring', 'deft', 'dew', 'dim',34 'drifting', 'dun', 'dusty', 'earnest', 'eastern', 'echoing', 'elm', 'errant',35 'evening', 'famed', 'feral', 'fern', 'fleet', 'fog', 'fond', 'free', 'gale',36 'gaunt', 'glacial', 'gleaming', 'gray', 'green', 'gusty', 'hale', 'harvest',37 'hazel', 'heather', 'honest', 'howling', 'hushed', 'icy', 'inland', 'jolly',38 'laughing', 'leafy', 'limber', 'lithe', 'loyal', 'marbled', 'meadow',39 'midnight', 'mild', 'mirthful', 'molten', 'morning', 'mottled', 'murmuring',40 'narrow', 'obsidian', 'ochre', 'olive', 'onyx', 'opal', 'painted', 'peppered',41 'pewter', 'pine', 'plucky', 'polar', 'prudent', 'quartz', 'quick', 'raw',42 'ringing', 'roaming', 'roan', 'rowdy', 'ruddy', 'rugged', 'russet', 'rust',43 'scarlet', 'sharp', 'shining', 'singing', 'sleek', 'slate', 'smoky', 'snowy',44 'sooty', 'southern', 'sparing', 'speckled', 'spry', 'staunch', 'storied',45 'stormy', 'striped', 'sturdy', 'tawny', 'thorny', 'thundering', 'timid',46 'topaz', 'tranquil', 'twilight', 'upland', 'valiant', 'velvet', 'verdant',47 'violet', 'wakeful', 'walnut', 'wandering-star', 'warm', 'watchful', 'western',48 'whistling', 'willow', 'windy', 'wistful', 'wry',49] as const;5051const USER_NOUNS = [52 'albatross', 'bittern', 'bunting', 'condor', 'cormorant', 'crane', 'crow',53 'cuckoo', 'curlew', 'dove', 'dunlin', 'eagle', 'egret', 'falcon', 'finch',54 'gannet', 'goshawk', 'grebe', 'grouse', 'gull', 'harrier', 'heron', 'hoopoe',55 'ibis', 'jackdaw', 'jay', 'kestrel', 'kite', 'lark', 'linnet', 'magpie',56 'merlin', 'nightjar', 'oriole', 'osprey', 'owl', 'petrel', 'pipit', 'plover',57 'puffin', 'quail', 'raven', 'robin', 'rook', 'sandpiper', 'shrike', 'siskin',58 'skylark', 'snipe', 'sparrow', 'starling', 'stork', 'swallow', 'swan',59 'swift-bird', 'tern', 'thrush', 'wagtail', 'warbler', 'wren',60 'auk', 'avocet', 'bee-eater', 'bellbird', 'bobolink', 'brant', 'bushtit',61 'canary', 'caracara', 'chickadee', 'chough', 'cockatoo', 'coot', 'corncrake',62 'courser', 'dipper', 'dotterel', 'drongo', 'eider', 'fieldfare', 'firecrest',63 'flamingo', 'fulmar', 'gadwall', 'garganey', 'godwit', 'goldeneye',64 'goldfinch', 'greenshank', 'guillemot', 'gyrfalcon', 'hawfinch', 'hobby',65 'jacana', 'junco', 'killdeer', 'kingbird', 'kingfisher', 'kittiwake', 'knot',66 'lapwing', 'loon', 'lorikeet', 'mallard', 'martin', 'moorhen', 'murrelet',67 'nuthatch', 'ouzel', 'ovenbird', 'parula', 'pelican', 'phalarope', 'phoebe',68 'pintail', 'pochard', 'ptarmigan', 'redpoll', 'redshank', 'redstart',69 'roller', 'ruff', 'sanderling', 'scaup', 'scoter', 'serin', 'shearwater',70 'shelduck', 'shoveler', 'skua', 'smew', 'solitaire', 'stint', 'stonechat',71 'sunbird', 'tanager', 'tattler', 'teal', 'titmouse', 'towhee', 'turnstone',72 'twite', 'veery', 'vireo', 'whimbrel', 'whinchat', 'wigeon', 'woodcock',73 'yellowlegs',74] as const;7576const SCRIPT_NOUNS = [77 'aurochs', 'badger', 'bear', 'bison', 'boar', 'bobcat', 'caribou', 'civet',78 'coyote', 'elk', 'ermine', 'ferret', 'fox', 'hare', 'hedgehog', 'ibex',79 'jackal', 'lemming', 'lynx', 'marmot', 'marten', 'mole', 'moose', 'mouflon',80 'muskox', 'otter', 'pangolin', 'pika', 'polecat', 'porcupine', 'ram',81 'reindeer', 'sable-beast', 'serval', 'shrew', 'stag', 'stoat', 'tapir',82 'vole', 'weasel', 'wisent', 'wolf', 'wolverine', 'yak', 'zorilla',83 'addax', 'antelope', 'armadillo', 'axolotl', 'banteng', 'bettong', 'binturong',84 'buffalo', 'camel', 'capybara', 'chamois', 'cheetah', 'chital', 'colugo',85 'cougar', 'dhole', 'dingo', 'dormouse', 'dugong', 'echidna', 'eland',86 'fennec', 'fisher-cat', 'gaur', 'gazelle', 'gemsbok', 'genet', 'gerenuk',87 'gibbon', 'glider', 'gnu', 'gopher', 'grison', 'hamster', 'hartebeest',88 'hog', 'hyrax', 'impala', 'jaguar', 'jerboa', 'kinkajou', 'kudu', 'leopard',89 'liger', 'loris', 'manul', 'markhor', 'meerkat', 'mink', 'mongoose', 'mouse',90 'muntjac', 'narwhal', 'nilgai', 'numbat', 'nyala', 'ocelot', 'okapi',91 'onager', 'oryx', 'panther', 'peccary', 'pronghorn', 'quokka', 'quoll',92 'raccoon', 'ratel', 'saiga', 'seal', 'sifaka', 'sloth', 'souslik',93 'springbok', 'squirrel', 'takin', 'tamandua', 'tarsier', 'tayra', 'tiger',94 'urial', 'vicuna', 'wallaby', 'wapiti', 'wombat', 'zebu',95] as const;9697const SIM_NOUNS = [98 'archipelago', 'basin', 'bay', 'bluff', 'bog', 'canyon', 'cape', 'cavern',99 'cove', 'crag', 'delta', 'dell', 'dune', 'fen', 'fjord', 'foothill', 'ford',100 'glade', 'glen', 'gorge', 'grotto', 'grove', 'heath', 'highland', 'hollow-vale',101 'isle', 'knoll', 'lagoon', 'lowland', 'marsh', 'mesa', 'moor', 'oasis',102 'outcrop', 'plateau', 'prairie', 'ravine', 'reef', 'ridge', 'rise', 'savanna',103 'shoal', 'steppe', 'summit', 'taiga', 'tarn', 'thicket', 'tundra', 'vale',104 'wold',105 'arroyo', 'atoll', 'badlands', 'bank', 'bar', 'barranca', 'barrens', 'bight',106 'brae', 'brook', 'butte', 'cairn-field', 'caldera', 'cascade', 'cataract',107 'channel', 'chasm', 'cirque', 'cliff', 'col', 'combe', 'confluence', 'coomb',108 'coulee', 'creek', 'crest', 'crossing', 'defile', 'dell-hollow', 'divide',109 'downs', 'drift', 'escarpment', 'estuary', 'eyot', 'fell', 'firth', 'flats',110 'floodplain', 'fork', 'gap', 'gully', 'gulch', 'headland', 'headwater',111 'hillock', 'hinterland', 'inlet-shore', 'karst', 'kettle', 'key', 'kloof',112 'lea', 'ledge', 'levee', 'lough', 'marshland', 'meander', 'mere', 'mudflat',113 'narrows', 'notch', 'overlook', 'pass', 'peatland', 'peninsula', 'pinnacle',114 'point', 'polder', 'pool', 'promontory', 'quagmire', 'rapids', 'reach',115 'riverbend', 'sandbar', 'scarp', 'scree', 'seamount', 'shallows', 'shore',116 'sink', 'slough', 'sound', 'spring', 'spur', 'strand', 'strait', 'swale',117 'terrace', 'tideland', 'tor', 'trench', 'uplands', 'wash', 'waterfall',118 'weir', 'wetland',119] as const;120121export type FriendlyKind = 'user' | 'script' | 'sim';122123const NOUNS: Record<FriendlyKind, readonly string[]> = {124 user: USER_NOUNS,125 script: SCRIPT_NOUNS,126 sim: SIM_NOUNS,127};128129export const NOUN_POOLS = NOUNS;130131/** FNV-1a 32-bit over a string. */132function fnv(text: string): number {133 let h = 0x811c9dc5;134 for (let i = 0; i < text.length; i++) {135 h ^= text.charCodeAt(i);136 h = Math.imul(h, 0x01000193);137 }138 return h >>> 0;139}140141/**142 * Deterministic name for (kind, seed, attempt). attempt 0 gives the short143 * `adjective-noun`; on collision the name ESCALATES with a second adjective144 * (`sly-ashen-wolf`) — a two-word space of 2M+ per category — and only145 * beyond eight two-word attempts does a numeric tag appear. Names lengthen146 * gracefully long before they turn into serial numbers.147 */148export function friendlyId(kind: FriendlyKind, seed: string, attempt = 0): string {149 const nouns = NOUNS[kind];150 const h = fnv(`${kind}:${seed}:${attempt}`);151 const adjective = ADJECTIVES[h % ADJECTIVES.length]!;152 const noun = nouns[Math.floor(h / ADJECTIVES.length) % nouns.length]!;153 const base = `${adjective}-${noun}`;154 if (attempt === 0) return base;155 const h2 = fnv(`${kind}:${seed}:extra:${attempt}`);156 let extra = ADJECTIVES[h2 % ADJECTIVES.length]!;157 if (extra === adjective) extra = ADJECTIVES[(h2 + 1) % ADJECTIVES.length]!;158 const doubled = `${extra}-${base}`;159 return attempt <= 8 ? doubled : `${doubled}-${attempt - 7}`;160}161162/** True when a D1/SQLite error is a UNIQUE-constraint violation. */163export function isUniqueViolation(err: unknown): boolean {164 return err instanceof Error && /UNIQUE constraint failed/i.test(err.message);165}