{"id":50932402,"url":"https://github.com/ianlintner/lexiconlang","last_synced_at":"2026-06-17T05:31:28.087Z","repository":{"id":356166534,"uuid":"1231301999","full_name":"ianlintner/lexiconlang","owner":"ianlintner","description":"Seedable, composable, multi-strategy game content generation for TypeScript — random names, NPCs, places, factions, items via grammars + Markov chains","archived":false,"fork":false,"pushed_at":"2026-05-11T03:53:30.000Z","size":283,"stargazers_count":0,"open_issues_count":4,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-11T05:28:37.929Z","etag":null,"topics":["conlang","gamedev","language-generator","lexicon","name-generator","phaser","seedable","typescript","web"],"latest_commit_sha":null,"homepage":"https://ianlintner.github.io/lexiconlang/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ianlintner.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-06T20:41:38.000Z","updated_at":"2026-05-11T03:50:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ianlintner/lexiconlang","commit_stats":null,"previous_names":["ianlintner/content-gen","ianlintner/lexiconlang"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/ianlintner/lexiconlang","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ianlintner%2Flexiconlang","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ianlintner%2Flexiconlang/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ianlintner%2Flexiconlang/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ianlintner%2Flexiconlang/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ianlintner","download_url":"https://codeload.github.com/ianlintner/lexiconlang/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ianlintner%2Flexiconlang/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34435978,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-17T02:00:05.408Z","response_time":127,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["conlang","gamedev","language-generator","lexicon","name-generator","phaser","seedable","typescript","web"],"created_at":"2026-06-17T05:31:24.863Z","updated_at":"2026-06-17T05:31:28.080Z","avatar_url":"https://github.com/ianlintner.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Lexicon · v0.3.0\n\n[![npm](https://img.shields.io/npm/v/lexiconlang)](https://www.npmjs.com/package/lexiconlang)\n[![CI](https://github.com/ianlintner/lexiconlang/actions/workflows/ci.yml/badge.svg)](https://github.com/ianlintner/lexiconlang/actions/workflows/ci.yml)\n\n**Procedural constructed-language generation: deterministic, seeded conlangs with phonotactics, lexicons, and culture-specific naming.**\n\nGenerate culture-specific names with meaningful translations. Names are morpheme-rich, phonotactically coherent, and derived from a culture's phonetic system and semantic meanings. Same seed → identical results across machines, runs, and library versions.\n\n```ts\nimport { fantasy } from \"@lexiconlang/fantasy\";\n\nconst game = fantasy.withSeed(\"campaign-1\");\n\nconst name = game.npc.name.full;\n// → { form: \"Drakaztum Ironforge\", translation: \"Strong-anvil Iron-forge\", language: \"fantasy.dwarvish\" }\n\nname.form; // \"Drakaztum Ironforge\" (conlang string)\nname.translation; // \"Strong-anvil Iron-forge\" (English morpheme meanings)\nname.toString(); // \"Drakaztum Ironforge\" (template-string compatible)\n```\n\n---\n\n## Why\n\nMost naming libraries treat names as opaque strings. **Lexicon generates names as _meaningful utterances_ — each morpheme carries semantic weight.**\n\nEach culture has:\n\n- A **glyph system** (phonotactics): which sounds cluster together, syllable patterns, phonotactic constraints\n- A **lexicon**: meaning ↔ conlang form mappings, derived deterministically from a culture's seed\n- **Templates**: morpheme recipes for personal names, place names, etc.\n\n**Result:** Names like `Drakaztum` (Strong-anvil), `Aelthelan` (Silver-stream), `Krazzivek` (Swarm-signal) — each name is both aesthetically coherent AND semantically meaningful.\n\n|                            | faker   | Tracery | rot.js  | **lexiconlang**        |\n| -------------------------- | ------- | ------- | ------- | ---------------------- |\n| Weighted lists             | ✓       | partial | partial | ✓                      |\n| Context-free grammars      | ✗       | ✓       | ✗       | ✓ (Tracery-compatible) |\n| Markov chains              | ✗       | ✗       | ✗       | ✓                      |\n| Hierarchical seeds         | ✗       | ✗       | partial | ✓                      |\n| Sibling-order independence | ✗       | ✗       | ✗       | ✓                      |\n| Strategies inter-operate   | n/a     | ✗       | ✗       | ✓                      |\n| Typed                      | partial | ✗       | partial | ✓                      |\n| Tree-shakeable genre packs | ✗       | n/a     | ✗       | ✓                      |\n\n## ✨ What's New in v0.3\n\n- **Visual glyph systems**: cultures can declare a `visualGlyphSystem` that renders names as glyphs — SVG runes, Unicode ideograms, or Canvas drawing instructions — derived from the same seed as the name itself.\n- **Three renderers**: SVG (compact inline vectors, ~2–5 ms/glyph), Unicode (instant character lookup), Canvas (replayable drawing-instruction sequences).\n- **Three mapping strategies**: phoneme (one glyph per phonetic unit), morpheme (one glyph per meaning component), holistic (one glyph for the whole name).\n- **Fantasy \u0026 sci-fi presets** ship with glyph systems: dwarvish runes (SVG), elvish ideograms (Unicode), humanoid geometry (Canvas), insectoid chitin (SVG).\n- **No breaking changes**: glyphs are opt-in. `TranslatedName.glyphs` and `Culture.visualGlyphSystems` are both optional.\n- **Minor template fix**: `NameTemplate` now supports an optional `transSep` so a culture can set `sep: \"\"` for the conlang form while keeping `-` as the translation separator (needed for morpheme-based glyph mapping).\n\n## ✨ What's New in v0.2\n\n- **Full language system**: Glyph systems, phonotactics, deterministic lexicon generation\n- **9 culture presets**: 5 fantasy (dwarvish, elvish, orcish, halfling, draconic) + 4 sci-fi (humanoid, insectoid, aquatic, synth) + extensible to custom cultures\n- **Morpheme-rich names**: Each name breaks down into semantic components with English translations\n- **Phonotactic archetypes**: Reusable templates for different language aesthetics (flowing, guttural, sibilant, clipped, resonant)\n- **Breaking change**: Name generators now return `TranslatedName` objects with `form`, `translation`, and `language` properties\n- **Determinism guarantees**: Seeded, order-independent, patch-stable lexicon generation\n\n---\n\n## Install\n\n```bash\npnpm add @lexiconlang/language @lexiconlang/fantasy\n# or sci-fi:\npnpm add @lexiconlang/scifi\n# add visual glyphs:\npnpm add @lexiconlang/glyphs\n# add generators for other content types:\npnpm add @lexiconlang/grammar @lexiconlang/markov @lexiconlang/core\n```\n\nRequires Node ≥ 20 or any modern browser. ESM-only. No native deps.\n\n---\n\n## Usage\n\n### Generate a dwarvish name\n\n```ts\nimport { dwarvish, buildLexicon } from \"@lexiconlang/fantasy/language\";\nimport { createContext } from \"@lexiconlang/core\";\n\nconst ctx = createContext({ seed: \"my-world\" });\nconst lexicon = buildLexicon(dwarvish, ctx);\n\n// Generate a given name:\nconst name = generateName(dwarvish, \"given\", ctx.child(\"hero:1\"));\nconsole.log(name.form); // \"Drakaztum\"\nconsole.log(name.translation); // \"Strong-anvil\"\nconsole.log(name.language); // \"fantasy.dwarvish\"\n```\n\n### Full NPC with fantasy integration\n\n```ts\nimport { fantasy } from \"@lexiconlang/fantasy\";\n\nconst game = fantasy.withSeed(\"campaign-7\");\nconst npc = game.npc;\n\nconsole.log(npc.name.full.form); // \"Aelyn Stormvale\"\nconsole.log(npc.name.full.translation); // \"Silver-stream Storm-vale\"\nconsole.log(npc.name.full.language); // \"fantasy.elvish\"\n```\n\n### Generate glyphs alongside the name\n\n```ts\nimport { createContext } from \"@lexiconlang/core\";\nimport { generateName } from \"@lexiconlang/language\";\nimport { glyphsFor } from \"@lexiconlang/glyphs\";\nimport { elvish } from \"@lexiconlang/fantasy\";\n\nconst ctx = createContext({ seed: \"campaign-1\" });\nconst name = generateName(elvish, \"given\", ctx.child(\"hero\"));\n// → { form: \"WaeYia\", translation: \"wild-vine\", language: \"fantasy.elvish\" }\n\nconst glyphs = glyphsFor(\n  name,\n  elvish.visualGlyphSystems!.conceptual!,\n  ctx.child(\"hero\"),\n);\n// → { conceptual: [{ id: \"g0\", meaning: \"wild\", unicode: \"🌿\" },\n//                  { id: \"g1\", meaning: \"vine\", unicode: \"🌿\" }] }\n\nglyphs.conceptual?.map((g) =\u003e g.unicode).join(\"\"); // \"🌿🌿\"\n```\n\nSame seed → byte-identical glyphs. Swap `elvish` for `dwarvish` to get SVG runes; for `humanoid` (sci-fi) to get Canvas drawing instructions. See [examples/09-glyphs.ts](examples/09-glyphs.ts).\n\n### Sci-fi alien names\n\n```ts\nimport { humanoidName, mycoidName, plantoidName } from \"@lexiconlang/scifi\";\nimport { createContext } from \"@lexiconlang/core\";\n\nconst ctx = createContext({ seed: \"crew-manifest\" });\n\nconsole.log(humanoidName.generate(ctx.child(\"humanoid:1\")).form);\nconsole.log(mycoidName.generate(ctx.child(\"mycoid:1\")).form);\nconsole.log(plantoidName.generate(ctx.child(\"plantoid:1\")).form);\n```\n\n### 4. Compose your own\n\n```ts\nimport { compose, oneOf, intRange } from \"@lexiconlang/core\";\nimport { fullName } from \"@lexiconlang/fantasy\";\n\nconst knight = compose\u003c{ name: string; rank: string; years: number }\u003e({\n  id: \"app.knight\",\n  parts: {\n    name: (ctx) =\u003e fullName.generate(ctx).full,\n    rank: oneOf(\"Squire\", \"Knight\", \"Knight-Captain\", \"Lord-Marshal\"),\n    years: intRange(1, 40),\n  },\n});\n\nconst gerald = knight.generate(world.child(\"knight:gerald\"));\n// → { name: \"Gerald Ironhold\", rank: \"Knight-Captain\", years: 23 }\n```\n\n### 5. Write your own grammar\n\n```ts\nimport { grammar, t } from \"@lexiconlang/grammar\";\n\nconst spell = grammar({\n  start: t`${\"prefix.cap\"} ${\"element.cap\"} ${\"form.cap\"}`,\n  prefix: [\"lesser\", \"greater\", \"true\", \"binding\"],\n  element: [\"fire\", \"frost\", \"shadow\", \"iron\"],\n  form: [\"bolt\", \"ward\", \"veil\", \"lash\"],\n});\nspell.generate(ctx); // \"Greater Frost Ward\"\n```\n\nJSON form is equivalent — both compile to the same AST. Modifiers (`cap`, `s`, `a/an`, `upper`, …) chain with dots; symbols can call other symbols, weighted lists, and **other generators** (e.g. `#markov:elven#` resolves through the registry).\n\n### 6. Train a Markov on your own corpus\n\n```ts\nimport { markov, train } from \"@lexiconlang/markov\";\n\nconst model = train([\"aberffraw\", \"betws\", \"caernarfon\" /* ... */], {\n  order: 3,\n  rejectSubstringsOfLength: 6, // refuse verbatim training entries\n});\nconst townName = markov(model);\ntownName.generate(ctx); // \"Llanrwst\" — never seen in training, but feels right\n```\n\nFor production: train offline via the CLI and ship the precomputed JSON model.\n\n```bash\nlexiconlang build-markov ./corpora/welsh-towns.json --out ./models/welsh.json --order 3\n```\n\n---\n\n## The seeding model\n\nDeterminism is the whole point. Three rules:\n\n1. **Every generator pulls its randomness from `ctx.rng`.** They never close over RNGs themselves.\n2. **`ctx.child(label)` derives a new context whose RNG is hashed from the parent's _origin seed_ and the label** — not from the parent's stream. This is the key trick: forking sibling A then sibling B gives you the same children regardless of how many times you fork, in what order, or whether you skip some.\n3. **`compose` uses field names as labels.** Reordering or adding fields to your generator type doesn't invalidate any existing field's seed.\n\n```ts\nconst root1 = createContext({ seed: \"world\" });\nconst root2 = createContext({ seed: \"world\" });\n\n// Walk many siblings before reaching the target.\nfor (let i = 0; i \u003c 100; i++) root1.child(`region:${i}`);\n\n// Both contexts produce the same NPC for the same path:\nconst a = npc.generate(\n  root1.child(\"region:5\").child(\"settlement:11\").child(\"npc:3\"),\n);\nconst b = npc.generate(\n  root2.child(\"region:5\").child(\"settlement:11\").child(\"npc:3\"),\n);\n// a equals b, byte for byte.\n```\n\nThe PRNG is **sfc32** (128-bit state, passes BigCrush, ~2 ns/call in V8). Forking uses **SplitMix64-on-strings** (FNV-1a → SplitMix64). State is serializable as 4 × u32.\n\n**Save = seed.** A whole world tree reconstructs from one string. To support player-driven rerolls without disturbing the rest of the world, encode \"version\" as part of the path:\n\n```ts\nconst ctx = root\n  .child(`region:0/settlement:5`)\n  .child(`v:${rerolls[path] ?? 0}`);\n```\n\nBumping `v:0` → `v:1` rerolls just that one settlement.\n\n---\n\n## Packages\n\n| Package                                    | Purpose                                                                                                                                                                                                   |\n| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [`@lexiconlang/core`](packages/core)       | sfc32 RNG with deterministic string-fork, `Context` tree, `Generator`, composition primitives (`compose`, `oneOf`, `pickOf`, `repeat`, `weightedList`, `map`, `chain`), alias-method sampling, `Registry` |\n| [`@lexiconlang/grammar`](packages/grammar) | Tracery-compatible JSON grammars + TS tagged-template DSL (`t\\`...\\``); 16 builtin modifiers; plugin-namespace symbol refs (e.g. `#markov:elven#`)                                                        |\n| [`@lexiconlang/markov`](packages/markov)   | Character-level Markov n-gram trainer + sampler; backoff smoothing; `rejectSubstringsOfLength` for verbatim-rejection; JSON model format                                                                  |\n| [`@lexiconlang/fantasy`](packages/fantasy) | Genre pack: 9 race-aware Markov name generators, NPCs, settlements, taverns, factions, cults, weapons, armor, dragons, quest hooks (~35 generators)                                                       |\n| [`@lexiconlang/scifi`](packages/scifi)     | Genre pack: alien species (humanoid/insectoid/aquatic/synth/human), star systems with planets, ships, megacorps, factions (~15 generators)                                                                |\n| [`@lexiconlang/glyphs`](packages/glyphs)   | Visual writing systems: deterministic SVG / Unicode / Canvas glyph rendering per culture, with phoneme / morpheme / holistic mapping strategies                                                           |\n| [`@lexiconlang/modern`](packages/modern)   | Genre pack: people with full email/phone/address, cities, streets, companies, bands, songs, books (~16 generators)                                                                                        |\n| [`@lexiconlang/cli`](packages/cli)         | `lexiconlang` command-line tool — `build-markov`, `scaffold-pack`                                                                                                                                         |\n\nAll packages ESM-only, `sideEffects: false`, no native deps.\n\n---\n\n## Examples\n\nSelf-contained, runnable demos in [examples/](examples/) covering common consumer tasks:\n\n|                                                        |                                                                     |\n| ------------------------------------------------------ | ------------------------------------------------------------------- |\n| [01-quickstart](examples/01-quickstart.ts)             | pin a seed, get content                                             |\n| [02-batch-patrons](examples/02-batch-patrons.ts)       | `repeat()` for batch generation                                     |\n| [03-world-tree](examples/03-world-tree.ts)             | hierarchical, lazily-generated worlds                               |\n| [04-custom-generator](examples/04-custom-generator.ts) | composing your own `Generator\u003cT\u003e`                                   |\n| [05-custom-grammar](examples/05-custom-grammar.ts)     | Tracery grammars in JSON or TS template + custom modifiers          |\n| [06-custom-markov](examples/06-custom-markov.ts)       | training a Markov on your own corpus                                |\n| [07-seed-and-reroll](examples/07-seed-and-reroll.ts)   | save/load by seed; partial rerolls                                  |\n| [08-cross-genre](examples/08-cross-genre.ts)           | mixing fantasy + sci-fi + modern packs                              |\n| [09-glyphs](examples/09-glyphs.ts)                     | visual writing systems: SVG runes, Unicode ideograms, Canvas glyphs |\n\n```bash\npnpm install\npnpm --filter examples quickstart\npnpm --filter examples all\n```\n\n---\n\n## CLI\n\nThe `@lexiconlang/cli` package installs a `lexiconlang` binary:\n\n```bash\n# Train a Markov model from a corpus and save the precomputed table.\nlexiconlang build-markov ./corpus.json --out ./model.json \\\n  --order 3 --min-length 4 --max-length 12 \\\n  --reject-substrings-of-length 5\n\n# Scaffold a new genre pack package.\nlexiconlang scaffold-pack noir --dir ./packages\n```\n\nThe corpus is either a JSON `string[]`, an array of `{ word, weight }`, or a newline-delimited text file (lines starting with `#` are ignored).\n\n---\n\n## Development\n\n```bash\npnpm install\npnpm typecheck         # tsc -b across all packages\npnpm build             # build every package\npnpm test              # 65+ tests across all packages, plus a determinism golden suite\npnpm samples           # regenerate tests/__artifacts__/samples.txt for human review\n```\n\nThe repo is a pnpm workspace. Each package is independent and publishable.\n\nCI runs typecheck + build + test on Node 20 and 22, plus a CLI smoke test, and uploads the `samples.txt` artifact on every run for visual review of generator output.\n\n---\n\n## Roadmap\n\n- **v0.1** — deterministic core, grammar, Markov, fantasy/scifi/modern packs, CLI.\n- **v0.2** — `@lexiconlang/language`: phoneme/syllable system, per-culture phonotactics, morpheme-rich names with English translations.\n- **v0.3** _(current)_ — `@lexiconlang/glyphs`: visual writing systems (SVG / Unicode / Canvas) with per-culture glyph registries and three mapping strategies.\n- **v0.4** — `@lexiconlang/llm`: bake-out CLI (recipe + Zod schema → validated weighted-list JSON) + live `AsyncGenerator` with content-addressed cache (`hash(prompt, scope, seed, model)`). Cache is shippable — play through your game once, commit the cache, ship a fully deterministic offline build.\n- **v0.5+** — browser playground for visualizing glyph systems, additional culture glyphs (orcish, halfling, draconic), CLI glyph rendering, additional packs (cyberpunk, post-apoc, historical).\n\n---\n\n## Prior art and credit\n\n- [Tracery](https://tracery.io/) (Kate Compton) — `lexiconlang` adopts its `#symbol#` grammar conventions and modifier model. JSON grammars from Tracery are mostly source-compatible.\n- [markov-namegen](https://github.com/Tw1ddle/markov-namegen-lib) (Tw1ddle) — Markov-process-based name generation; the verbatim-rejection idea is borrowed from this lineage.\n- [Dwarf Fortress](https://dwarffortresswiki.org/index.php/Language) (Bay 12) — the \"every culture has a language with words for things; names are compositions of meanings\" model that v0.2's phonology system is built around.\n- [Faker.js](https://fakerjs.dev/), [Chance.js](https://chancejs.com/), [rot.js](https://ondras.github.io/rot.js/), [Improv](https://github.com/sequitur/improv), [Bracery](https://github.com/ihh/bracery) — each tackles one slice of this problem; this library aims to absorb the best of each behind one composable interface.\n\n---\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fianlintner%2Flexiconlang","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fianlintner%2Flexiconlang","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fianlintner%2Flexiconlang/lists"}