dnd-name-generator/README.md

330 lines
13 KiB
Markdown
Raw Normal View History

# D&D Name and Language Generator
2023-11-24 05:48:03 -08:00
This package is a fantasy language generator. By defining a number of characteristics about your imagined language -- the graphemes, their relative frequency distributions, the construction of syllables, and so on -- you can generate random but internally consistent gibberish that feels distinct, evocative, and appropriate to your setting.
## Usage
The `fanlang` command-line utility supports three commands:
* **names**: generate names
* **text**: generate a paragraph of text
* **list**: list the supported language in the current language pack
### Examples:
2023-11-24 07:41:59 -08:00
```shell
% fanlang --language=dwarvish names --count 5
Tiny Châ Pothesadottyr
Khâkhu Zhûdothir
Quiet Ke Vêdothir
Cû Tozhon
Big Pâ Thadottyr
```
2023-11-24 07:41:59 -08:00
```shell
% fanlang --language=dwarvish text
Cû ne do tho khâ tasha, vê wûva lû, ku phu thâ thê, tûko kê, pevo kâ têtetv zha
pataso keks khate? Fâ zhû shû yf pho pa me. Dupha dê thê khâ! Shikm tu! Cê
sâdêto. Dê yo nâ topho, my sû pida phe, vi phûtw châcho, po sotê?
```
2023-11-24 07:41:59 -08:00
```shell
% fanlang list
Abyssal : Eshshishsee zozsuwhoshsh asaawheeziwh! A..whoszoshs ssaswhsu..o
Celestial : Ȯoiüöueîîoäuüeêiûȯ, ouịeüêoäaüaėu̇üoäuau âüiûeėûeuäuiüêuäaịii,
Common : Uemson noffom, eelom lesyoe exori emaah? Sytteel eudd liiom! Exemooaf
Draconic : Vysisa kseeseg ksotsaa' dseyhk yz'sax, 'sosuu'h lsitv dj'sih sir'seed
2023-12-17 15:05:57 -08:00
Druidic : Iagh tiama gih mi'allus? Laanog i heisoh lapt, uuch auh nu ta'ullug
Dwarvish : Nûzhewt co. Phûbê pâpp pûzhûde bî tho di dakf, kâ mû che pe nezhûphû
Elvish : Vyöua ycrem mluwre yrhyi syüstem? Uufyias eotdyian fleoe, eswoawyia
Gnomish : Di buae haui se miue taui nue, pue heo ruoe puoy roea, hauy diua mey
Halfling : Pelee fipo'we'nei, se'hovololi'y, rare'ri'ti'myo ja'to'lilae nybe'wao,
Infernal : Po p'daod p'kyid bou t'yagio k'pub x'opuj, p'nba jibi z'eux p'jaabu
Lizardfolk : Roast, malic orange earth. Grain musty coconut! Raspberry peanut meat
Orcish : Chuche cebru hiko tuma, zutup brumef fubrok cozb ciko, cokot fochi
Undercommon : Tniwus, aldupun sqiluth rivujel gijäxaa, aäuyäutäs vixälat vükäe
% fanlang list --names
Abyssal : Zwhoshwhshwhe
Celestial : Aȯîaûuêeäuêaaüuịäu̇
Common : Nana Zeborg
Draconic : Jsois
2023-12-17 15:05:22 -08:00
Druidic : Duullih
Dwarvish : Thi Vudottir
Elvish : Smoöbd am Dwesm
Gnomish : Tue Nio
Halfling : Fybobo'ga'jo'a Da'ro'y Byna'ba'e "Gallant"
Infernal : Edmius
Lizardfolk : Floral Maple Syrup Phrenolic
Orcish : Dih Buzh
```
## Language Packs
A *Language Pack* is a python package that defines one or more language modules. The default language pack includes a number of D&D languages with rules built according to the conventions established by my D&D group over several years of play in our homebrew setting.
The default language pack is [language.languages](language/languages/); each submodule contains a README that describes the basic characteristics of the language, along with examples.
### Using Your Own Language Pack
You can override `fanlang`'s default language pack by specifying the `FANLANG_LANGUAGE_PACK` environment variable:
2023-11-24 07:41:59 -08:00
```shell
# Create your ancient_elvish module in campaign/language_pack/ancient_elvish
% FANLANG_LANGUAGE_PACK=campaign.language_pack fanlang list
Ancient Elvish : ...
2023-11-24 07:02:49 -08:00
```
### Setting the Default Language
'common' is the default language module. You can override this by setting the `FANLANG_DEFAULT_LANGUAGE` environment variable:
2023-11-24 07:41:59 -08:00
```shell
% FANLANG_DEFAULT_LANGUAGE=gnomish fanlang names --count=1
Jey Lea
```
You can read about creating custom language packs below.
## Library Quick Start
You can load all supported languages in a language pack using `language.load_langauge_pack()`:
2023-11-24 05:48:03 -08:00
2023-11-24 07:41:59 -08:00
```python
>>> import language
>>> language_pack, supported_languages = language.load_language_pack()
2023-11-24 05:48:03 -08:00
>>> common = supported_languages['common']
>>> common.word(2)
['apsoo', 'nirtoet']
>>> common.text()
Proitsiiiy be itkif eesof detytaen. Ojaot tyskuaz apsoo nirtoet prenao.
>>> commoner = f"{common.Name}"
"Quiet" Gushi Murk Lirpusome
>>> common.Name.name()
{
name: ['Gushi', 'Murk'],
surname: ['Lirpusome'],
adjective: ["Quiet"],
}
```
You can also load individual languages directly:
2023-11-24 05:48:03 -08:00
2023-11-24 07:41:59 -08:00
```python
>>> from language.languages import common
>>> common.Language.word(2)
['apsoo', 'nirtoet']
>>> str(common.Name)
"Quiet" Gushi Murk Lirpusome
```
2023-11-24 05:48:03 -08:00
## Defining a New Language Pack
Language packs are python packages with the following structure:
2023-11-24 07:41:59 -08:00
```python
language_pack:
__init__.py
language_name:
__init__.py
2023-11-24 07:39:47 -08:00
README.md
base.py
names.py
rules.py
...
```
2023-11-24 05:48:03 -08:00
2023-11-24 07:39:47 -08:00
### Language Modules
2023-11-24 05:48:03 -08:00
A language consists of several submodules:
2023-11-24 07:39:47 -08:00
* `base.py`, which contains grapheme definitions and a `Language` subclass;
2023-11-24 05:48:03 -08:00
* `names.py`, which defines the `NameGenerator` subclasses; and
* `rules.py`, which is optional, and defines the rules all words in the language must follow.
2023-11-24 05:48:03 -08:00
2023-11-24 07:39:47 -08:00
Read on for a discussion of each of these components.
2023-11-24 05:48:03 -08:00
2023-11-24 07:39:47 -08:00
## Language Generators
2023-11-24 05:48:03 -08:00
Let's look at a simple example, the Gnomish language. Here's the `Language`
2023-11-24 07:39:47 -08:00
subclass defined in `base.py`:
2023-11-24 05:48:03 -08:00
2023-11-24 07:41:59 -08:00
```python
2023-11-24 05:48:03 -08:00
from language import defaults, types
from .rules import rules
consonants = types.WeightedSet(
("b", 0.5), ("d", 0.5), ("f", 0.3), ("g", 0.3),
("h", 0.5), ("j", 0.2), ("l", 1.0), ("m", 0.5),
("n", 1.0), ("p", 0.5), ("r", 1.0), ("s", 1.0),
("t", 1.0), ("v", 0.3), ("w", 0.2), ("z", 0.1),
)
suffixes = types.equal_weights(["a", "e", "i", "o", "y"], 1.0, blank=False)
Language = types.Language(
name="gnomish",
vowels=defaults.vowels,
consonants=consonants,
prefixes=None,
suffixes=suffixes,
syllables=types.SyllableSet(
(types.Syllable(template="consonant,vowel,vowel"), 1.0),
(types.Syllable(template="consonant,vowel"), 0.33),
),
rules=rules,
minimum_grapheme_count=2,
)
```
2023-11-24 07:39:47 -08:00
### Defining Graphemes
2023-11-24 05:48:03 -08:00
A Language definition includes *graphemes*, the basic building blocks of any
2023-11-24 08:04:36 -08:00
language. We start with **vowels** and **consonants**, which are required in
every language; Gnomish also includes **suffixes**, but no **prefixes**. Each
2023-11-24 05:48:03 -08:00
grapheme is a `WeightedSet`, which is like a regular set except its members
consist of a tuple of a string and a relative weight from 0.0 to 1.0. These
weights will be used when selecting a random grapheme.
Gnomish uses the default vowels, defined in [the
language.defaults](language/defaults.py) submodule, but define our consonants
with a subset of English consonants. By experimenting with different sets and
different weights, you can generate radically different feeling text output!
Gnomish also uses suffixes, which are graphemes added to the ends of words.
Here we use the helper function `types.equal_weights()`, which returns
a `WeightedSet` where each member is given the same weight. Normally this
function also inserts the grapheme `("", 1.0)` into the set, but we disable
this behaviour by specifying `blank=False`.
2023-11-24 07:39:47 -08:00
### Defining Syllables
2023-11-24 05:48:03 -08:00
A syllable is a collection of graphemes, including at least one vowel. When we
create words, we select a random syllable template from a `SyllableSet`, which
is just a `WeightedSet` whose members are `Syllable` instances. Each `Syllable`
declares a `template`, and like graphemes, has a weight associated with it that
will make it more or less likely to be chosen for a word.
A syllable's template consists of a comma-separated string of grapheme names.
In Gnomish, we have two possible syllable templates, `consonant,vowel,vowel`
and the shorter `consonant,vowel`, which will be selected one third as often.
2023-11-24 07:39:47 -08:00
Templates also support randomly-selected graphemes by joining two or more
2023-11-24 05:48:03 -08:00
grapheme types with a vertical bar, for example `vowel|consonant` would choose
one or the other; `vowel|consonant,vowel` would result in a vowel or
a consonant followed by a vowel.
### How Words Are Constructed:
2023-11-24 07:39:47 -08:00
The main interface for callers is `word()`, which returns a randomly-generated
2023-11-24 05:48:03 -08:00
word in the language according to the following algorithm:
1. Choose a random syllable from the syllable set
2023-11-24 08:04:36 -08:00
1. For each grapheme in the syllable:
* Choose a random grapheme template
* Choose a random sequence from the language for that grapheme
* Validate the word against the language rules
1. Repeat 1-2 until a valid word is generated
1. Add a prefix and suffix, if they are defined
2023-11-24 05:48:03 -08:00
When graphemes are chosen, the following rules are applied:
* Every syllable must have at least one vowel; and
* A syllable may never have three consecutive consonants.
2023-11-24 07:39:47 -08:00
### A More Complex Example
[The Common language](language/languages/common/base.py) is a more complex definition, with language-specific prefixes, suffixes, vowels, and consonants, and many possible syllables. This results in highly-varied text.
## Rules
Rules are a set of callables that accept a language instance and a word. The callable returns `True` if the word passes some test, and `False` otherwise. Every randomly-generated word must pass all defined rules for the language, or it is rejected.
2023-11-24 08:04:36 -08:00
[The language.rules module](language/rules.py) contains a number of useful rules that are applied by default to most languages, mostly used to aid readability and generate words that are pronouncable. Here's a simple example:
2023-11-24 07:39:47 -08:00
2023-11-24 07:41:59 -08:00
```python
2023-11-24 07:39:47 -08:00
def too_many_consonants(language: Language, word: str) -> bool:
found = re.compile(r"[bcdfghjklmnpqrstvwxz]{3}").findall(word)
if found == []:
return True
logger.debug(f"{word} has too many contiguous consonants: {found}")
return False
```
This rule ensures that a word does not contain more than 3 english consonants in a row.
### Defining Language-Specific Rules
Rules are passed as a set of callables to the `Language` constructor, so they can be anything you want, defined anywhere you want. By convention, language packs use a separate `rules` module when building custom rule sets.
## Name Generators
Name generators are similar to Language generators, but with a few key differences. Here is a simple example, also from the Gnomish language:
2023-11-24 07:41:59 -08:00
```python
2023-11-24 07:39:47 -08:00
from language import types
from language.languages.gnomish import Language
Name = types.NameGenerator(
language=Language,
templates=types.NameSet(
(types.NameTemplate("name,surname"), 1.0),
),
)
NobleName = Name
```
In Gnomish, names are straightforward, consisting of a name and a surname, and there is no distinction between regular names and the names of the nobility. By contrast, [Elvish names are complex](language/languages/elvish/names.py), consisting of multiple distinct parts, including place names, affixes, and separate rules for common and noble name construction.
### Defining Names
Name Generators are defined with a Language, and one or more `NameSet` templates. `NameSets` are equivalent to `SyllableSets`, but instead of creating templates for the construction of syllables from sequences of graphemes, they define sequences of parts of names -- **names**, **surnames**, **titles**, **nicknames**, and so on. They follow the same semantics as `Syllables`, allowing for a large variety of potential names.
By default, both **names** and **surnames** are generated automatically by calling `NameGenerator.language.word()`. Thus, the simplest name generator will simply follow all the rules of the language itself and generate one or more random words. You can override multiple aspects of a language's rules for word generation by passing `NameGenerator` additional arguments. For example, here is a generator for the names of locations in the Elvish language:
2023-11-24 07:41:59 -08:00
```python
2023-11-24 07:39:47 -08:00
from language import types, defaults
from language.languages.elvish import Language
PlaceName = types.NameGenerator(
language=Language,
syllables=types.SyllableSet(
(types.Syllable(template="vowel,vowel|consonant,vowel|consonant"), 1.0),
(types.Syllable(template="consonant,vowel|consonant,vowel|consonant"), 0.3),
),
templates=types.NameSet(
(types.NameTemplate("affix,name"), 1.0),
),
affixes=types.WeightedSet(("el", 1.0)),
adjectives=defaults.adjectives,
)
```
Note how we declare new `syllables`, `affixes`, and `adjectives`, which will replace the `Language`'s default behaviours.
#### Subclassing
Sometimes we need even more control than providing new syllable sets and weighted sets for graphemes. Subclassing `NameGenerator` gives you significant control over how names are constructed. For example, Elvish surnames are based on Elvish place names; to accomplish this, we subclass `NameGenerator` and override the method used to generate surnames:
2023-11-24 07:41:59 -08:00
```python
2023-11-24 07:39:47 -08:00
class ElvishNameGenerator(types.NameGenerator):
def __init__(self):
super().__init__(
language=Language,
syllables=Language.syllables,
templates=types.NameSet(
(types.NameTemplate("name,affix,surname"), 1.0),
),
affixes=types.equal_weights(["am", "an", "al", "um"], weight=1.0, blank=False),
adjectives=defaults.adjectives,
titles=defaults.titles,
suffixes=suffixes,
)
self.place_generator = PlaceName
def get_surname(self) -> str:
return self.place_generator.name()[0]["name"][0]
```