|
| 1 | +/** |
| 2 | + * Realistic username generation for autobot party hints. |
| 3 | + * |
| 4 | + * Party IDs look like `<hint>::<fingerprint>` on Canton; the hint is the |
| 5 | + * human-readable part a dashboard shows. Rather than `autobot-42`, we want |
| 6 | + * hints that look like real usernames — first names, first.last patterns, |
| 7 | + * handle + digits — so stakeholder views feel organic. |
| 8 | + * |
| 9 | + * All hints are lowercased `[a-z0-9_]+` (Canton-safe). No hyphens, so they |
| 10 | + * visually distinguish from any pre-existing `autobot-N` keys. |
| 11 | + */ |
| 12 | + |
| 13 | +const FIRST_NAMES = [ |
| 14 | + "alex", "sam", "jamie", "taylor", "jordan", "morgan", "riley", "casey", |
| 15 | + "quinn", "avery", "chris", "maya", "priya", "arjun", "raj", "ananya", |
| 16 | + "yuki", "ken", "haruto", "mei", "wei", "jun", "lin", "chen", |
| 17 | + "sofia", "diego", "luca", "emma", "liam", "noah", "olivia", "ava", |
| 18 | + "sarah", "mike", "dave", "jen", "rachel", "ben", "nate", "kate", |
| 19 | + "amir", "leila", "omar", "zara", "kwame", "adaeze", "tomas", "anna", |
| 20 | + "erik", "lars", "freya", "nora", "finn", "ivy", "eli", "ada", |
| 21 | + "hiro", "miko", "sana", "aiko", "ravi", "neha", "vikram", "asha", |
| 22 | +]; |
| 23 | + |
| 24 | +const LAST_NAMES = [ |
| 25 | + "smith", "jones", "brown", "taylor", "lee", "wang", "kim", "patel", |
| 26 | + "singh", "garcia", "martinez", "lopez", "kumar", "sharma", "tanaka", |
| 27 | + "suzuki", "nguyen", "tran", "cohen", "muller", "schmidt", "novak", |
| 28 | + "rossi", "bianchi", "silva", "oliveira", "kowalski", "andersson", |
| 29 | + "kapoor", "chen", "liu", "zhang", "yamada", "ito", "park", "choi", |
| 30 | +]; |
| 31 | + |
| 32 | +const HANDLE_WORDS = [ |
| 33 | + "fox", "wolf", "raven", "kite", "otter", "atlas", "nova", "echo", |
| 34 | + "river", "orbit", "pixel", "drift", "spark", "bloom", "loop", "forge", |
| 35 | + "neon", "sable", "lumen", "quartz", "flux", "cipher", "nebula", |
| 36 | +]; |
| 37 | + |
| 38 | +const HANDLE_ADJECTIVES = [ |
| 39 | + "cool", "silent", "neon", "fast", "lucky", "quiet", "brave", "sunny", |
| 40 | + "misty", "shady", "wild", "urban", "retro", "rapid", "clever", |
| 41 | +]; |
| 42 | + |
| 43 | +function pick(arr, rng) { |
| 44 | + return arr[Math.floor(rng() * arr.length)]; |
| 45 | +} |
| 46 | + |
| 47 | +function maybe(rng, p) { |
| 48 | + return rng() < p; |
| 49 | +} |
| 50 | + |
| 51 | +function twoDigit(rng) { |
| 52 | + // 10..99 — avoid the very common "1"/"2" suffix that looks incremental |
| 53 | + return String(10 + Math.floor(rng() * 90)); |
| 54 | +} |
| 55 | + |
| 56 | +/** |
| 57 | + * Generate one realistic username-style party hint. |
| 58 | + * Patterns are roughly distributed across common real-world shapes. |
| 59 | + */ |
| 60 | +export function generateHint(rng = Math.random) { |
| 61 | + const pattern = rng(); |
| 62 | + const first = pick(FIRST_NAMES, rng); |
| 63 | + const last = pick(LAST_NAMES, rng); |
| 64 | + const lastInitial = last[0]; |
| 65 | + |
| 66 | + if (pattern < 0.25) { |
| 67 | + // "alex" |
| 68 | + return first; |
| 69 | + } |
| 70 | + if (pattern < 0.5) { |
| 71 | + // "alex42" |
| 72 | + return `${first}${twoDigit(rng)}`; |
| 73 | + } |
| 74 | + if (pattern < 0.65) { |
| 75 | + // "alex_kim" |
| 76 | + return `${first}_${last}`; |
| 77 | + } |
| 78 | + if (pattern < 0.8) { |
| 79 | + // "alexk" or "alexk21" |
| 80 | + const base = `${first}${lastInitial}`; |
| 81 | + return maybe(rng, 0.5) ? `${base}${twoDigit(rng)}` : base; |
| 82 | + } |
| 83 | + // "coolfox7" / "neonkite42" |
| 84 | + const adj = pick(HANDLE_ADJECTIVES, rng); |
| 85 | + const word = pick(HANDLE_WORDS, rng); |
| 86 | + const digit = maybe(rng, 0.6) ? String(Math.floor(rng() * 1000)) : ""; |
| 87 | + return `${adj}${word}${digit}`; |
| 88 | +} |
| 89 | + |
| 90 | +/** |
| 91 | + * Generate a hint that doesn't collide with any existing one. Collision on |
| 92 | + * hint is not a Canton uniqueness violation (fingerprint disambiguates), but |
| 93 | + * local keyfiles live at `<hint>.json` so filename collision matters. |
| 94 | + * |
| 95 | + * @param {(hint: string) => boolean} exists — returns true if hint already used |
| 96 | + * @param {number} maxTries |
| 97 | + */ |
| 98 | +export function generateUniqueHint(exists, rng = Math.random, maxTries = 50) { |
| 99 | + for (let i = 0; i < maxTries; i++) { |
| 100 | + const h = generateHint(rng); |
| 101 | + if (!exists(h)) return h; |
| 102 | + } |
| 103 | + // Fallback: append random hex suffix |
| 104 | + return `${generateHint(rng)}${Math.floor(rng() * 1e6).toString(36)}`; |
| 105 | +} |
0 commit comments