@twardoch/namzy 1.0.19 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -1,35 +1,27 @@
1
1
  // this_file: src/index.ts
2
2
 
3
- import { activeRotationMask, joinClean, mangle, mulberry32 } from "./mangle.js";
4
- import { COMMON, GEO } from "./wordlist.js";
3
+ import { applyRotation, buildName, mulberry32 } from "./mangle.js";
4
+ import { ROTATIONS, STEMS } from "./wordlist.js";
5
5
 
6
6
  export interface NamzyOptions {
7
- seed?: number;
7
+ seed?: number;
8
8
  }
9
9
 
10
- function capitalize(s: string): string {
11
- return s.charAt(0).toUpperCase() + s.slice(1);
10
+ /** Generate one namzy name. Default seed is the current timestamp. */
11
+ export function generate(opts?: NamzyOptions): string {
12
+ const seed = opts?.seed ?? Date.now();
13
+ return buildName(mulberry32(seed));
12
14
  }
13
15
 
14
- function pick<T>(arr: T[], rng: () => number): T {
15
- return arr[Math.floor(rng() * arr.length)];
16
+ /** Generate `count` names. Distinct seeds derived from the base seed. */
17
+ export function generateMany(count: number, opts?: NamzyOptions): string[] {
18
+ const base = opts?.seed ?? Date.now();
19
+ const out: string[] = [];
20
+ for (let i = 0; i < count; i++) {
21
+ out.push(generate({ seed: base + i * 2654435761 }));
22
+ }
23
+ return out;
16
24
  }
17
25
 
18
- /**
19
- * Generate a single fused, mangled namzy name.
20
- * Two raw words → junction-cleaned fusion → consonant rotation → capitalize.
21
- */
22
- export async function generate(opts?: NamzyOptions): Promise<string> {
23
- const seed = opts?.seed ?? Date.now();
24
- const rng = mulberry32(seed);
25
-
26
- const w1 = pick(GEO, rng).toLowerCase();
27
- const w2 = pick(COMMON, rng).toLowerCase();
28
- const [first, second] = rng() < 0.5 ? [w1, w2] : [w2, w1];
29
-
30
- const fused = joinClean(first, second);
31
- return capitalize(mangle(fused, activeRotationMask(rng)));
32
- }
33
-
34
- export { COMMON, GEO } from "./wordlist.js";
35
- export { activeRotationMask, joinClean, mangle, mulberry32 };
26
+ export { STEMS, ROTATIONS } from "./wordlist.js";
27
+ export { applyRotation, buildName, mulberry32 };
package/src/mangle.ts CHANGED
@@ -1,92 +1,83 @@
1
1
  // this_file: src/mangle.ts
2
2
 
3
- /** Simple seeded mulberry32 RNG returns a function that yields [0,1) floats */
3
+ import { BAD_SEAMS, ROTATIONS, STEMS } from "./wordlist.js";
4
+
5
+ const MAX_LEN = 12;
6
+ const MAX_TRIES = 8;
7
+
4
8
  export function mulberry32(seed: number): () => number {
5
9
  let s = seed >>> 0;
6
10
  return () => {
7
- s += 0x6d2b79f5;
11
+ s = (s + 0x6d2b79f5) >>> 0;
8
12
  let t = Math.imul(s ^ (s >>> 15), 1 | s);
9
13
  t ^= t + Math.imul(t ^ (t >>> 7), 61 | t);
10
14
  return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
11
15
  };
12
16
  }
13
17
 
14
- type RotationRule = readonly [from: string, to: string];
15
-
16
- export const ROTATION_RULES: readonly RotationRule[] = [
17
- ["c", "q"],
18
- ["f", "v"],
19
- ["k", "c"],
20
- ["q", "k"],
21
- ["s", "z"],
22
- ["z", "s"],
23
- ["v", "f"],
24
- ["w", "u"],
25
- ["b", "p"],
26
- ["p", "b"],
27
- ];
28
-
29
- const ALL_ROTATIONS = (1 << ROTATION_RULES.length) - 1;
18
+ function pick<T>(arr: readonly T[], rng: () => number): T {
19
+ return arr[Math.floor(rng() * arr.length)];
20
+ }
30
21
 
31
- function rotationFor(lower: string, activeMask: number): string | undefined {
32
- for (let i = 0; i < ROTATION_RULES.length; i++) {
33
- if ((activeMask & (1 << i)) === 0) {
34
- continue;
35
- }
36
- const [from, to] = ROTATION_RULES[i];
37
- if (lower === from) {
38
- return to;
39
- }
22
+ function hasTripleLetter(s: string): boolean {
23
+ for (let i = 2; i < s.length; i++) {
24
+ if (s[i] === s[i - 1] && s[i] === s[i - 2]) return true;
40
25
  }
41
- return undefined;
26
+ return false;
42
27
  }
43
28
 
44
- /** Choose 1..10 active consonant-rotation rules from the seeded RNG. */
45
- export function activeRotationMask(rng: () => number): number {
46
- const order = ROTATION_RULES.map((_, i) => i);
47
- const activeCount = 1 + Math.floor(rng() * ROTATION_RULES.length);
48
- for (let i = 0; i < activeCount; i++) {
49
- const swap = i + Math.floor(rng() * (order.length - i));
50
- [order[i], order[swap]] = [order[swap], order[i]];
29
+ function junctionUgly(compound: string, junction: number): boolean {
30
+ // Inspect the 4-char window centered on the junction.
31
+ const start = Math.max(0, junction - 2);
32
+ const end = Math.min(compound.length, junction + 2);
33
+ const win = compound.slice(start, end);
34
+ for (const seam of BAD_SEAMS) {
35
+ if (win.includes(seam)) return true;
51
36
  }
52
- return order.slice(0, activeCount).reduce((mask, i) => mask | (1 << i), 0);
37
+ return false;
53
38
  }
54
39
 
55
- /** Consonant rotation. Case-preserving. Defaults to all rules for direct helper use. */
56
- export function mangle(s: string, activeMask = ALL_ROTATIONS): string {
57
- let out = "";
58
- for (const ch of s) {
59
- const lower = ch.toLowerCase();
60
- const repl = rotationFor(lower, activeMask);
61
- if (repl === undefined) {
62
- out += ch;
63
- } else {
64
- out += ch === lower ? repl : repl.toUpperCase();
40
+ /** Apply one rotation at a randomly chosen matching position. */
41
+ export function applyRotation(s: string, rng: () => number): string {
42
+ type Match = { i: number; src: string; dst: string };
43
+ const matches: Match[] = [];
44
+ for (let i = 0; i < s.length; i++) {
45
+ for (const [src, dst] of ROTATIONS) {
46
+ if (s.slice(i, i + src.length) === src) {
47
+ matches.push({ i, src, dst });
48
+ }
65
49
  }
66
50
  }
67
- return out;
51
+ if (matches.length === 0) return s;
52
+ const m = matches[Math.floor(rng() * matches.length)];
53
+ return s.slice(0, m.i) + m.dst + s.slice(m.i + m.src.length);
68
54
  }
69
55
 
70
- const VOWELS = new Set(["a", "e", "i", "o", "u", "y"]);
56
+ function capitalize(s: string): string {
57
+ return s.length === 0 ? s : s[0].toUpperCase() + s.slice(1);
58
+ }
71
59
 
72
- /**
73
- * Fuse two lowercase words at a clean junction.
74
- * Drops the first char of `b` while it duplicates the tail of `a`,
75
- * or while the seam is vowel-vowel. Max two trims.
76
- */
77
- export function joinClean(a: string, b: string): string {
78
- const head = a;
79
- let tail = b;
80
- for (let i = 0; i < 2 && head.length > 0 && tail.length > 0; i++) {
81
- const last = head[head.length - 1].toLowerCase();
82
- const first = tail[0].toLowerCase();
83
- if (last === first) {
84
- tail = tail.slice(1);
85
- } else if (VOWELS.has(last) && VOWELS.has(first)) {
86
- tail = tail.slice(1);
87
- } else {
88
- break;
60
+ /** Build one name with junction validation + one rotation. */
61
+ export function buildName(rng: () => number): string {
62
+ let best = "";
63
+ for (let attempt = 0; attempt < MAX_TRIES; attempt++) {
64
+ const a = pick(STEMS, rng);
65
+ const b = pick(STEMS, rng);
66
+ const compound = a + b;
67
+ if (compound.length > MAX_LEN) {
68
+ best = best || compound;
69
+ continue;
70
+ }
71
+ if (hasTripleLetter(compound)) {
72
+ best = best || compound;
73
+ continue;
74
+ }
75
+ if (junctionUgly(compound, a.length)) {
76
+ best = best || compound;
77
+ continue;
89
78
  }
79
+ return capitalize(applyRotation(compound, rng));
90
80
  }
91
- return head + tail;
81
+ // All attempts had some flaw; rotate and ship the first candidate anyway.
82
+ return capitalize(applyRotation(best, rng));
92
83
  }
package/src/web.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // this_file: src/web.ts
2
2
  // Browser entry point — exposes namzy.generate() on window.namzy.
3
3
 
4
- export { generate, mangle, joinClean, mulberry32, GEO, COMMON } from "./index.js";
4
+ export { generate, generateMany, applyRotation, buildName, mulberry32, STEMS, ROTATIONS } from "./index.js";
5
5
  export type { NamzyOptions } from "./index.js";