initial import

This commit is contained in:
evilchili 2023-11-24 08:48:03 -05:00
commit 45f4d6e401
66 changed files with 3601 additions and 0 deletions

24
LICENSE Normal file
View File

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

129
README.md Normal file
View File

@ -0,0 +1,129 @@
# D&D Language Generator
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.
## Quick Start
```
>>> from language imported supported_languages
>>> 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"],
}
```
## Supported Languages
A number of D&D languages are defined, with rules built according to the
conventions established by my D&D group over several years of play in our
homebrew setting. You can find all supported languages [in the languages
submodule](language/languages/); each submodule contains a README that
describes the basic characteristics of the language, along with examples.
## Defining a Language
### Layout
A language consists of several submodules:
* `base.py`, which contains grapheme definitions and the `Language` subclasses;
* `names.py`, which defines the `NameGenerator` subclasses; and
* `rules.py`, which is optional but defines the rules all words in the language must follow.
### Language Construction
Let's look at a simple example, the Gnomish language. Here's the `Language`
subclass that would be defined in `base.py`:
```
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,
)
```
#### Defining Graphemes
A Language definition includes *graphemes*, the basic building blocks of any
language. We start with **vowels**, **consonants**, which are required in every
language; Gnomish also includes **suffixes**, but no **prefixes**. Each
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`.
#### Defining Syllables
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.
Templates can also support randomly-selected graphemes by joining two or more
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:
The main interface for callers is word(), which returns a randomly-generated
word in the language according to the following algorithm:
1. Choose a random syllable from the syllable set
2. For each grapheme in the syllable
3. Choose a random grapheme template
4. Choose a random sequence from the language for that grapheme
5. Validate the word against the language rules
6. Repeat 1-5 until a valid word is generated
7. Add a prefix and suffix, if they are defined
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.

1
language/__init__.py Normal file
View File

@ -0,0 +1 @@
from .languages import supported_languages

72
language/cli.py Normal file
View File

@ -0,0 +1,72 @@
import logging
import os
from enum import Enum
from types import ModuleType
import typer
from rich.logging import RichHandler
from rich.console import Console
from rich.markdown import Markdown
from language import supported_languages
app = typer.Typer()
app_state = {}
Supported = Enum("Supported", ((k, k) for k in supported_languages.keys()))
def print_sample(lang: str, module: ModuleType) -> None:
summary = module.__doc__.replace("\n", " \n")
print(f"{summary}")
print(f" Text: {module.Language.text(10)}")
print(f" Names: {module.Name}")
print(f" {module.Name}")
print(f" {module.Name}")
if module.NobleName != module.Name:
print(f" Noble Names: {module.NobleName}")
print(f" {module.NobleName}")
print(f" {module.NobleName}")
print("")
@app.callback()
def main(
language: Supported = typer.Option("common", help="The language to use."),
):
app_state["language"] = supported_languages[language.name]
debug = os.getenv("DEBUG", None)
logging.basicConfig(
format="%(message)s",
level=logging.DEBUG if debug else logging.INFO,
handlers=[RichHandler(rich_tracebacks=True, tracebacks_suppress=[typer])],
)
@app.command()
def words(count: int = typer.Option(50, help="The number of words to generate.")):
print(" ".join(list(app_state["language"].Language.word(count))))
@app.command()
def names(
count: int = typer.Option(50, help="The number of names to generate."),
noble: bool = typer.Option(False, help="Generate noble names."),
):
generator = app_state["language"].Name if not noble else app_state["language"].NobleName
for name in generator.name(count):
print(name["fullname"])
@app.command()
def list():
console = Console(width=80)
for lang, module in supported_languages.items():
console.print(Markdown(module.__doc__))
if __name__ == "__main__":
app()

864
language/defaults.py Normal file
View File

@ -0,0 +1,864 @@
from language import types
vowels = types.equal_weights(["a", "e", "i", "o", "u"], 1.0)
consonants = types.equal_weights(
["b", "c", "d", "f", "g", "h", "j", "k", "l", "m", "n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z"], 1.0
)
adjectives = types.equal_weights(
["big", "tiny", "wild", "crazy", "quiet", "red", "black", "blue", "sick", "dancing", "jumping"], 0.05
)
titles = types.equal_weights(["sir", "dame", "capt.", "sgt.", "miss", "mrs.", "mr.", "dr."], 0.01) + types.WeightedSet(
("lord", 0.05), ("lady", 0.05)
)
counts = types.equal_weights(["II", "III", "IV", "V", "VI", "VII", "VIII", "IX"], 0.01)
# source: http://ideonomy.mit.edu/essays/traits.html
personality = types.equal_weights(
[
"Accessible",
"Active",
"Adaptable",
"Admirable",
"Adventurous",
"Agreeable",
"Alert",
"Allocentric",
"Amiable",
"Anticipative",
"Appreciative",
"Articulate",
"Aspiring",
"Athletic",
"Attractive",
"Balanced",
"Benevolent",
"Brilliant",
"Calm",
"Capable",
"Captivating",
"Caring",
"Challenging",
"Charismatic",
"Charming",
"Cheerful",
"Clean",
"Clear-headed",
"Clever",
"Colorful",
"Companionly",
"Compassionate",
"Conciliatory",
"Confident",
"Conscientious",
"Considerate",
"Constant",
"Contemplative",
"Cooperative",
"Courageous",
"Courteous",
"Creative",
"Cultured",
"Curious",
"Daring",
"Debonair",
"Decent",
"Decisive",
"Dedicated",
"Deep",
"Dignified",
"Directed",
"Disciplined",
"Discreet",
"Dramatic",
"Dutiful",
"Dynamic",
"Earnest",
"Ebullient",
"Educated",
"Efficient",
"Elegant",
"Eloquent",
"Empathetic",
"Energetic",
"Enthusiastic",
"Esthetic",
"Exciting",
"Extraordinary",
"Fair",
"Faithful",
"Farsighted",
"Felicific",
"Firm",
"Flexible",
"Focused",
"Forecful",
"Forgiving",
"Forthright",
"Freethinking",
"Friendly",
"Fun-loving",
"Gallant",
"Generous",
"Gentle",
"Genuine",
"Good-natured",
"Gracious",
"Hardworking",
"Healthy",
"Hearty",
"Helpful",
"Herioc",
"High-minded",
"Honest",
"Honorable",
"Humble",
"Humorous",
"Idealistic",
"Imaginative",
"Impressive",
"Incisive",
"Incorruptible",
"Independent",
"Individualistic",
"Innovative",
"Inoffensive",
"Insightful",
"Insouciant",
"Intelligent",
"Intuitive",
"Invulnerable",
"Kind",
"Knowledge",
"Leaderly",
"Leisurely",
"Liberal",
"Logical",
"Lovable",
"Loyal",
"Lyrical",
"Magnanimous",
"Many-sided",
"Masculine",
"Manly",
"Mature",
"Methodical",
"Maticulous",
"Moderate",
"Modest",
"Multi-leveled",
"Neat",
"Nonauthoritarian",
"Objective",
"Observant",
"Open",
"Optimistic",
"Orderly",
"Organized",
"Original",
"Painstaking",
"Passionate",
"Patient",
"Patriotic",
"Peaceful",
"Perceptive",
"Perfectionist",
"Personable",
"Persuasive",
"Planful",
"Playful",
"Polished",
"Popular",
"Practical",
"Precise",
"Principled",
"Profound",
"Protean",
"Protective",
"Providential",
"Prudent",
"Punctual",
"Pruposeful",
"Rational",
"Realistic",
"Reflective",
"Relaxed",
"Reliable",
"Resourceful",
"Respectful",
"Responsible",
"Responsive",
"Reverential",
"Romantic",
"Rustic",
"Sage",
"Sane",
"Scholarly",
"Scrupulous",
"Secure",
"Selfless",
"Self-critical",
"Self-defacing",
"Self-denying",
"Self-reliant",
"Self-sufficent",
"Sensitive",
"Sentimental",
"Seraphic",
"Serious",
"Sexy",
"Sharing",
"Shrewd",
"Simple",
"Skillful",
"Sober",
"Sociable",
"Solid",
"Sophisticated",
"Spontaneous",
"Sporting",
"Stable",
"Steadfast",
"Steady",
"Stoic",
"Strong",
"Studious",
"Suave",
"Subtle",
"Sweet",
"Sympathetic",
"Systematic",
"Tasteful",
"Teacherly",
"Thorough",
"Tidy",
"Tolerant",
"Tractable",
"Trusting",
"Uncomplaining",
"Understanding",
"Undogmatic",
"Unfoolable",
"Upright",
"Urbane",
"Venturesome",
"Vivacious",
"Warm",
"Well-bred",
"Well-read",
"Well-rounded",
"Winning",
"Wise",
"Witty",
"Youthful",
"Absentminded",
"Aggressive",
"Ambitious",
"Amusing",
"Artful",
"Ascetic",
"Authoritarian",
"Big-thinking",
"Boyish",
"Breezy",
"Businesslike",
"Busy",
"Casual",
"Crebral",
"Chummy",
"Circumspect",
"Competitive",
"Complex",
"Confidential",
"Conservative",
"Contradictory",
"Crisp",
"Cute",
"Deceptive",
"Determined",
"Dominating",
"Dreamy",
"Driving",
"Droll",
"Dry",
"Earthy",
"Effeminate",
"Emotional",
"Enigmatic",
"Experimental",
"Familial",
"Folksy",
"Formal",
"Freewheeling",
"Frugal",
"Glamorous",
"Guileless",
"High-spirited",
"Huried",
"Hypnotic",
"Iconoclastic",
"Idiosyncratic",
"Impassive",
"Impersonal",
"Impressionable",
"Intense",
"Invisible",
"Irreligious",
"Irreverent",
"Maternal",
"Mellow",
"Modern",
"Moralistic",
"Mystical",
"Neutral",
"Noncommittal",
"Noncompetitive",
"Obedient",
"Old-fashined",
"Ordinary",
"Outspoken",
"Paternalistic",
"Physical",
"Placid",
"Political",
"Predictable",
"Preoccupied",
"Private",
"Progressive",
"Proud",
"Pure",
"Questioning",
"Quiet",
"Religious",
"Reserved",
"Restrained",
"Retiring",
"Sarcastic",
"Self-conscious",
"Sensual",
"Skeptical",
"Smooth",
"Soft",
"Solemn",
"Solitary",
"Stern",
"Stoiid",
"Strict",
"Stubborn",
"Stylish",
"Subjective",
"Surprising",
"Soft",
"Tough",
"Unaggressive",
"Unambitious",
"Unceremonious",
"Unchanging",
"Undemanding",
"Unfathomable",
"Unhurried",
"Uninhibited",
"Unpatriotic",
"Unpredicatable",
"Unreligious",
"Unsentimental",
"Whimsical",
"Abrasive",
"Abrupt",
"Agonizing",
"Aimless",
"Airy",
"Aloof",
"Amoral",
"Angry",
"Anxious",
"Apathetic",
"Arbitrary",
"Argumentative",
"Arrogantt",
"Artificial",
"Asocial",
"Assertive",
"Astigmatic",
"Barbaric",
"Bewildered",
"Bizarre",
"Bland",
"Blunt",
"Biosterous",
"Brittle",
"Brutal",
"Calculating",
"Callous",
"Cantakerous",
"Careless",
"Cautious",
"Charmless",
"Childish",
"Clumsy",
"Coarse",
"Cold",
"Colorless",
"Complacent",
"Complaintive",
"Compulsive",
"Conceited",
"Condemnatory",
"Conformist",
"Confused",
"Contemptible",
"Conventional",
"Cowardly",
"Crafty",
"Crass",
"Crazy",
"Criminal",
"Critical",
"Crude",
"Cruel",
"Cynical",
"Decadent",
"Deceitful",
"Delicate",
"Demanding",
"Dependent",
"Desperate",
"Destructive",
"Devious",
"Difficult",
"Dirty",
"Disconcerting",
"Discontented",
"Discouraging",
"Discourteous",
"Dishonest",
"Disloyal",
"Disobedient",
"Disorderly",
"Disorganized",
"Disputatious",
"Disrespectful",
"Disruptive",
"Dissolute",
"Dissonant",
"Distractible",
"Disturbing",
"Dogmatic",
"Domineering",
"Dull",
"Easily Discouraged",
"Egocentric",
"Enervated",
"Envious",
"Erratic",
"Escapist",
"Excitable",
"Expedient",
"Extravagant",
"Extreme",
"Faithless",
"False",
"Fanatical",
"Fanciful",
"Fatalistic",
"Fawning",
"Fearful",
"Fickle",
"Fiery",
"Fixed",
"Flamboyant",
"Foolish",
"Forgetful",
"Fraudulent",
"Frightening",
"Frivolous",
"Gloomy",
"Graceless",
"Grand",
"Greedy",
"Grim",
"Gullible",
"Hateful",
"Haughty",
"Hedonistic",
"Hesitant",
"Hidebound",
"High-handed",
"Hostile",
"Ignorant",
"Imitative",
"Impatient",
"Impractical",
"Imprudent",
"Impulsive",
"Inconsiderate",
"Incurious",
"Indecisive",
"Indulgent",
"Inert",
"Inhibited",
"Insecure",
"Insensitive",
"Insincere",
"Insulting",
"Intolerant",
"Irascible",
"Irrational",
"Irresponsible",
"Irritable",
"Lazy",
"Libidinous",
"Loquacious",
"Malicious",
"Mannered",
"Mannerless",
"Mawkish",
"Mealymouthed",
"Mechanical",
"Meddlesome",
"Melancholic",
"Meretricious",
"Messy",
"Miserable",
"Miserly",
"Misguided",
"Mistaken",
"Money-minded",
"Monstrous",
"Moody",
"Morbid",
"Muddle-headed",
"Naive",
"Narcissistic",
"Narrow",
"Narrow-minded",
"Natty",
"Negativistic",
"Neglectful",
"Neurotic",
"Nihilistic",
"Obnoxious",
"Obsessive",
"Obvious",
"Odd",
"Offhand",
"One-dimensional",
"One-sided",
"Opinionated",
"Opportunistic",
"Oppressed",
"Outrageous",
"Overimaginative",
"Paranoid",
"Passive",
"Pedantic",
"Perverse",
"Petty",
"Pharissical",
"Phlegmatic",
"Plodding",
"Pompous",
"Possessive",
"Power-hungry",
"Predatory",
"Prejudiced",
"Presumptuous",
"Pretentious",
"Prim",
"Procrastinating",
"Profligate",
"Provocative",
"Pugnacious",
"Puritanical",
"Quirky",
"Reactionary",
"Reactive",
"Regimental",
"Regretful",
"Repentant",
"Repressed",
"Resentful",
"Ridiculous",
"Rigid",
"Ritualistic",
"Rowdy",
"Ruined",
"Sadistic",
"Sanctimonious",
"Scheming",
"Scornful",
"Secretive",
"Sedentary",
"Selfish",
"Self-indulgent",
"Shallow",
"Shortsighted",
"Shy",
"Silly",
"Single-minded",
"Sloppy",
"Slow",
"Sly",
"Small-thinking",
"Softheaded",
"Sordid",
"Steely",
"Stiff",
"Strong-willed",
"Stupid",
"Submissive",
"Superficial",
"Superstitious",
"Suspicious",
"Tactless",
"Tasteless",
"Tense",
"Thievish",
"Thoughtless",
"Timid",
"Transparent",
"Treacherous",
"Trendy",
"Troublesome",
"Unappreciative",
"Uncaring",
"Uncharitable",
"Unconvincing",
"Uncooperative",
"Uncreative",
"Uncritical",
"Unctuous",
"Undisciplined",
"Unfriendly",
"Ungrateful",
"Unhealthy",
"Unimaginative",
"Unimpressive",
"Unlovable",
"Unpolished",
"Unprincipled",
"Unrealistic",
"Unreflective",
"Unreliable",
"Unrestrained",
"Unself-critical",
"Unstable",
"Vacuous",
"Vague",
"Venal",
"Venomous",
"Vindictive",
"Vulnerable",
"Weak",
"Weak-willed",
"Well-meaning",
"Willful",
"Wishful",
"Zany",
],
1.0,
)
positive_adjectives = types.equal_weights(
[
"able",
"clean",
"enthusiastic",
"heartening",
"meek",
"reasonable",
"talented",
"accommodating",
"clever",
"ethical",
"helpful",
"meritorious",
"refined",
"temperate",
"accomplished",
"commendable",
"excellent",
"moral",
"reliable",
"terrific",
"adept",
"compassionate",
"exceptional",
"honest",
"neat",
"remarkable",
"tidy",
"admirable",
"composed",
"exemplary",
"honorable",
"noble",
"resilient",
"quality",
"agreeable",
"considerate",
"exquisite",
"hopeful",
"obliging",
"respectable",
"tremendous",
"amazing",
"consummate",
"extraordinary",
"humble",
"observant",
"respectful",
"trustworthy",
"appealing",
"cooperative",
"fabulous",
"important",
"optimistic",
"resplendent",
"trusty",
"astute",
"correct",
"faithful",
"impressive",
"organized",
"responsible",
"truthful",
"attractive",
"courageous",
"fantastic",
"incisive",
"outstanding",
"robust",
"unbeatable",
"awesome",
"courteous",
"fascinating",
"incredible",
"peaceful",
"selfless",
"understanding",
"beautiful",
"dazzling",
"fine",
"innocent",
"perceptive",
"sensational",
"unequaled",
"benevolent",
"decent",
"classy",
"insightful",
"perfect",
"sensible",
"unparalleled",
"brave",
"delightful",
"fortitudinous",
"inspiring",
"pleasant",
"serene",
"upbeat",
"breathtaking",
"dependable",
"gallant",
"intelligent",
"pleasing",
"sharp",
"valiant",
"bright",
"devoted",
"generous",
"joyful",
"polite",
"shining",
"valuable",
"brilliant",
"diplomatic",
"gentle",
"judicious",
"positive",
"shrewd",
"vigilant",
"bubbly",
"discerning",
"gifted",
"just",
"praiseworthy",
"smart",
"vigorous",
"buoyant",
"disciplined",
"giving",
"kindly",
"precious",
"sparkling",
"virtuous",
"calm",
"elegant",
"gleaming",
"laudable",
"priceless",
"spectacular",
"well mannered",
"capable",
"elevating",
"glowing",
"likable",
"principled",
"splendid",
"wholesome",
"charitable",
"enchanting",
"good",
"lovable",
"prompt",
"steadfast",
"wise",
"charming",
"encouraging",
"gorgeous",
"lovely",
"prudent",
"stunning",
"witty",
"chaste",
"endearing",
"graceful",
"loyal",
"punctual",
"super",
"wonderful",
"cheerful",
"energetic",
"gracious",
"luminous",
"pure",
"superb",
"worthy",
"chivalrous",
"engaging",
"great",
"magnanimous",
"quick",
"superior",
"zesty",
"gallant",
"enhanced",
"happy",
"magnificent",
"radiant",
"supportive",
"civil",
"enjoyable",
"hardy",
"marvelous",
"rational",
"supreme",
],
1.0,
)

View File

@ -0,0 +1,14 @@
import importlib
import pkgutil
import sys
def import_submodules(module):
pkgs = pkgutil.iter_modules(module.__path__)
for loader, module_name, is_pkg in pkgs:
yield importlib.import_module(f"{module.__name__}.{module_name}")
supported_languages = dict(
(module.__name__.split(".")[-1], module) for module in list(import_submodules(sys.modules[__name__]))
)

View File

@ -0,0 +1,16 @@
### Abyssal
Low-ranking demons speak in a guttural, broken dialect that is louder and more
consonant, like dogs barking. The written language and naming conventions
reflect the more refined language of demons, a quiet, sibilant susurrus that
sounds at times like waves, at others like buzzing, keening insects.
*Whuewhezoawh ..uesewhaza whs..uwhiezowhisoi aizi..wheuz, whoshesowhewh
she..iwhzzos..ozi iuzu..ueshes izsha..ua..uashezshuesas, ..awho....uza
whuashoowhsz!*
**Abyssal Names:**
* Aa..Whaazwhis
* Az..Zazzou
* Uzeushsoshe..Osezawhwh

View File

@ -0,0 +1,7 @@
"""
Abyssal
"""
from .base import Language
from .names import Name, NobleName
__all__ = [Language, Name, NobleName]

View File

@ -0,0 +1,18 @@
from language import defaults, types
vowels = types.equal_weights(["a", "e", "i"], 1.0, blank=False)
consonants = types.equal_weights(["z", "sh", "s", "wh", ".."], 1.0, blank=False)
Language = types.Language(
name="celstial",
vowels=defaults.vowels,
consonants=consonants,
prefixes=None,
suffixes=None,
rules=[],
syllables=types.SyllableSet(
(types.Syllable(template="vowel|consonant") * 10, 1.0),
(types.Syllable(template="vowel|consonant") * 15, 1.0),
(types.Syllable(template="vowel|consonant") * 20, 1.0),
),
)

View File

@ -0,0 +1,11 @@
from language import types
from language.languages.abyssal import Language
Name = types.NameGenerator(
language=Language,
templates=types.NameSet(
(types.NameTemplate("name"), 1.0),
),
)
NobleName = Name

View File

@ -0,0 +1,15 @@
### Celestial
Celestial is sung, not spoken, and sounds to mortal ears like stacked harmonies
of an impossible choir. Written Celestial is akin to sheet music; it records
the grammar and vocabulary, but cannot convey meaning.
*U̇ôeȧäuueüiäûoîịėeöäueaȯịo äuäuiaîiüoûoėeäoîeȧü öoaịäu̇ôuüöeiääoȧėu
ôaîaêaịeiîäoöiôoėöi, ueûuêûiüȧuuȧaüeeêeuȯ oääuêaüâiîȧiûeu̇ėaâaȧ
uîoėu̇au̇oîäeiäüuȧoêaiêu̇u uaäeîeoäuiaịôoėȯîiäiȧė îûȧäuaûėuoäôiûäuaȧ!
Âȧuȯüiêịiäaiėöaöoi.*
**Celestial Names:**
* Ịaêäuuȧoeėuäuoaịooȯîoäu̇Au̇Aû
* Ûịịeịuȧoȯuȯaäâeeu̇Ėuiüaị
* Au̇Üaoịėuüöaâaėoöȧeäėu

View File

@ -0,0 +1,7 @@
"""
Celestial
"""
from .base import Language
from .names import Name, NobleName
__all__ = [Language, Name, NobleName]

View File

@ -0,0 +1,21 @@
from language import defaults, types
vowels = types.equal_weights(["a", "e", "i", "o", "u"], 1.0, blank=False)
consonants = types.equal_weights(
["î", "ê", "â", "û", "ô", "ä", "ö", "ü", "äu", "ȧ", "ė", "", "ȯ", ""], 1.0, blank=False
)
Language = types.Language(
name="celstial",
vowels=defaults.vowels,
consonants=consonants,
prefixes=None,
suffixes=None,
rules=[],
syllables=types.SyllableSet(
(types.Syllable(template="vowel|consonant") * 20, 1.0),
(types.Syllable(template="vowel|consonant") * 25, 1.0),
(types.Syllable(template="vowel|consonant") * 30, 1.0),
(types.Syllable(template="vowel|consonant") * 35, 1.0),
),
)

View File

@ -0,0 +1,11 @@
from language import types
from language.languages.celestial import Language
Name = types.NameGenerator(
language=Language,
templates=types.NameSet(
(types.NameTemplate("name"), 1.0),
),
)
NobleName = Name

View File

@ -0,0 +1,16 @@
### Common
Common is a complicated pidgin of influences with multiple regional dialects. Written Common is the language of
traders, and can be relied upon to be understood by most peoples to a greater or lesser degree.
*Proitsiiiy be itkif eesof detytaen. Ojaot tyskuaz apsoo nirtoet prenao.*
**Common Names:**
* Rubi Ca Momaman
* "Quiet" Gushi Murk Lirpusome
* Fewse Kerloborg
**Noble Common Names:**
* Lord Pasti Quusi Maghiheim
* Lady Gotki Lane Lopileigh III
* Dame Cu Lehaberry IX

View File

@ -0,0 +1,7 @@
"""
Common
"""
from .base import Language
from .names import Name, NobleName
__all__ = [Language, Name, NobleName]

View File

@ -0,0 +1,158 @@
from language import types
from .rules import rules
vowels = types.WeightedSet(
("a", 1.0),
("e", 1.0),
("i", 1.0),
("o", 0.8),
("u", 0.7),
("y", 0.01),
)
consonants = types.WeightedSet(
("b", 0.5),
("c", 0.5),
("d", 0.5),
("f", 0.3),
("g", 0.3),
("h", 0.5),
("j", 0.2),
("k", 0.3),
("l", 1.0),
("m", 0.5),
("n", 1.0),
("p", 0.5),
("q", 0.05),
("r", 1.0),
("s", 1.0),
("t", 1.0),
("v", 0.3),
("w", 0.2),
("x", 0.2),
("y", 0.01),
("z", 0.1),
("bs", 0.3),
("ct", 0.4),
("ch", 0.4),
("ck", 0.4),
("dd", 0.2),
("ff", 0.2),
("gh", 0.3),
("gs", 0.2),
("ms", 0.4),
("ns", 0.4),
("ps", 0.3),
("qu", 0.2),
("rb", 0.1),
("rd", 0.2),
("rf", 0.1),
("rk", 0.2),
("rl", 0.2),
("rm", 0.2),
("rn", 0.2),
("rp", 0.1),
("rs", 0.75),
("rt", 0.75),
("ry", 0.5),
("sh", 1.0),
("sk", 0.5),
("ss", 0.75),
("st", 1.0),
("sy", 0.5),
("th", 1.0),
("tk", 0.5),
("ts", 1.0),
("tt", 1.0),
("ty", 1.0),
("ws", 0.5),
)
prefixes = types.equal_weights(["ex", "re", "pre", "de", "pro", "anti"], 0.05)
suffixes = types.equal_weights(
[
"ad",
"ed",
"id",
"od",
"ud",
"af",
"ef",
"if",
"of",
"uf",
"ah",
"eh",
"ih",
"oh",
"uh",
"al",
"el",
"il",
"ol",
"ul",
"am",
"em",
"im",
"om",
"um",
"an",
"en",
"in",
"on",
"un",
"ar",
"er",
"ir",
"or",
"ur",
"as",
"es",
"is",
"os",
"us",
"at",
"et",
"it",
"ot",
"ut",
"ax",
"ex",
"ix",
"ox",
"ux",
"ay",
"ey",
"iy",
"oy",
"uy",
"az",
"ez",
"iz",
"oz",
"uz",
"ing",
],
0.05,
)
Language = types.Language(
name="common",
vowels=vowels,
consonants=consonants,
prefixes=prefixes,
suffixes=suffixes,
syllables=types.SyllableSet(
(types.Syllable(template="vowel|consonant"), 0.01),
(types.Syllable(template="vowel|consonant") * 2, 0.2),
(types.Syllable(template="vowel|consonant") * 3, 0.4),
(types.Syllable(template="vowel|consonant") * 3, 0.5),
(types.Syllable(template="vowel|consonant") * 4, 1.0),
(types.Syllable(template="vowel|consonant") * 5, 0.3),
(types.Syllable(template="vowel|consonant") * 6, 0.2),
(types.Syllable(template="vowel|consonant") * 7, 0.05),
),
rules=rules,
minimum_grapheme_count=1,
)

View File

@ -0,0 +1,82 @@
from language import defaults, types
from language.languages.common import Language
suffixes = types.equal_weights(
[
"berg",
"borg",
"borough",
"bury",
"berry",
"by",
"ford",
"gard",
"grave",
"grove",
"gren",
"hardt",
"hart",
"heim",
"holm",
"land",
"leigh",
"ley",
"ly",
"lof",
"love",
"lund",
"man",
"mark",
"ness",
"olf",
"olph",
"quist",
"rop",
"rup",
"stad",
"stead",
"stein",
"strom",
"thal",
"thorpe",
"ton",
"vall",
"wich",
"win",
"some",
"smith",
"bridge",
"cope",
"town",
"er",
"don",
"den",
"dell",
"son",
]
)
Name = types.NameGenerator(
language=Language,
syllables=types.SyllableSet(
(types.Syllable(template="vowel|consonant"), 0.01),
(types.Syllable(template="consonant,vowel"), 0.2),
(types.Syllable(template="consonant,vowel") * 2, 1.0),
),
templates=types.NameSet(
(types.NameTemplate("adjective,title,name,surname,count"), 1.0),
(types.NameTemplate("title,name,name,surname,count"), 1.0),
(types.NameTemplate("title,name,name,surname,surname,count"), 1.0),
),
names=None,
surnames=None,
nicknames=None,
adjectives=defaults.adjectives,
titles=defaults.titles,
counts=defaults.counts,
suffixes=suffixes,
)
Name.language.prefixes = None
Name.language.suffixes = None
NobleName = Name

View File

@ -0,0 +1,115 @@
import logging
import re
from language.rules import default_rules
from language.types import Language
logger = logging.getLogger()
permitted_starting_clusters = [
"bh",
"bl",
"br",
"bw",
"by",
"ch",
"cl",
"cr",
"cw",
"cy",
"dh",
"dj",
"dr",
"dw",
"dy",
"fl",
"fn",
"fr",
"fw",
"fy",
"gh",
"gl",
"gn",
"gr",
"gw",
"gy",
"hy",
"jh",
"jy",
"kh",
"kl",
"kr",
"kw",
"ky",
"ll",
"ly",
"mw",
"my",
"ny",
"ph",
"pl",
"pn",
"pr",
"pw",
"py",
"rh",
"ry",
"sb",
"sc",
"sd",
"sf",
"sg",
"sh",
"sj",
"sk",
"sl",
"sm",
"sn",
"sp",
"sr",
"st",
"sv",
"sw",
"sy",
"th",
"tr",
"tw",
"ty",
"vy",
"wh",
"wr",
"wy",
"xh",
"xy",
"yh",
"zb",
"zc",
"zd",
"zh",
"zl",
"zm",
"zn",
"zr",
"zw",
"zy",
]
def cannot_start_with_two_consonants(language: Language, word: str) -> bool:
found = re.compile(r"(^[bcdfghjklmnpqrstvwxz]{2})").search(word)
if not found:
return True
first, second = found.group(1)
if first == second:
logger.debug(f"{word} starts with a repeated consonant.")
return False
return found in permitted_starting_clusters
rules = default_rules.union(
{
cannot_start_with_two_consonants,
}
)

View File

@ -0,0 +1,23 @@
### Draconic
The tongue of dragons and the Dragonborn. It is punctuated by low, gutteral
grunts denoted by an apostrophe ('), the particular inflections of which can
change the meaning of an utterance dramatically. When shouting, a speaker of
Draconic should accentuate these grunts with spurts of flame, acid, ice, and so
on.
The high form is reserved for the names of nobility -- that is, dragons -- who
typically choose an appelation for themselves in the common tongue, the better
to spread the terror of their name.
*Sey'so'jsii ysuut 'sei'gk gf'seiv 'se'su'rx 'se'yv hsi', sahsei' k'si'saa' f'si''see?*
**Draconic Names:**
* V'Sei'Sey'a
* Sir''Seytza
* 'Soh'Suutus
**Noble (Dragon) Names:**
* Seytthix the Willful
* Saseezex the Enervated
* Sahkzis the Miserly

View File

@ -0,0 +1,7 @@
"""
Draconic
"""
from .base import Language
from .names import Name, NobleName
__all__ = [Language, Name, NobleName]

View File

@ -0,0 +1,47 @@
from language import types
from .rules import rules
vowels = types.equal_weights(
["sa", "saa", "sah", "se", "see", "sei", "sey", "si", "sii", "sir", "so", "su", "suu"], 1.0, blank=False
)
consonants = types.WeightedSet(
("d", 1.0),
("f", 0.5),
("g", 0.5),
("h", 1.0),
("j", 1.0),
("k", 1.0),
("l", 0.3),
("n", 0.3),
("r", 0.2),
("t", 1.0),
("v", 1.0),
("x", 1.0),
("y", 1.0),
("z", 1.0),
)
class DraconicLanguage(types.Language):
stops = types.equal_weights(["'"], 1.0)
def get_grapheme_vowel(self) -> str:
return self.stops.random() + self.vowels.random() + self.stops.random()
Language = DraconicLanguage(
name="draconic",
vowels=vowels,
consonants=consonants,
prefixes=None,
suffixes=None,
syllables=types.SyllableSet(
(types.Syllable(template="consonant|vowel") * 2, 0.2),
(types.Syllable(template="consonant|vowel") * 3, 1.0),
(types.Syllable(template="consonant|vowel") * 4, 1.0),
),
rules=rules,
minimum_grapheme_count=2,
)

View File

@ -0,0 +1,88 @@
from language import defaults, types
from language.languages.draconic import Language
# dragon_titles = types.equal_weights([
# ], 1.0)
class DraconicNameGenerator(types.NameGenerator):
def __init__(self):
super().__init__(
language=Language,
templates=types.NameSet(
(types.NameTemplate("name"), 1.0),
(types.NameTemplate("adjective,name"), 1.0),
),
adjectives=defaults.adjectives,
)
self.language.minimum_grapheme_count = 2
self.suffixes = types.equal_weights(["us", "ius", "eus", "a", "an", "is"], 1.0, blank=False)
def get_name(self) -> str:
return super().get_name() + self.suffixes.random()
class NobleDraconicNameGenerator(types.NameGenerator):
def __init__(self):
super().__init__(
language=Language,
templates=types.NameSet(
(types.NameTemplate("surname,the,title"), 1.0),
),
# titles=dragon_titles,
syllables=types.SyllableSet(
(types.Syllable(template="consonant|vowel") * 2, 1.0),
),
)
self.language.minimum_grapheme_count = 2
self.suffixes = types.equal_weights(
[
"thus",
"thux",
"thas",
"thax",
"this",
"thix",
"thes",
"thex",
"xus",
"xux",
"xas",
"xax",
"xis",
"xix",
"xes",
"xex",
"ssus",
"ssux",
"ssas",
"ssax",
"ssis",
"ssix",
"sses",
"ssex",
"zus",
"zux",
"zas",
"zax",
"zis",
"zix",
"zes",
"zex",
],
1.0,
blank=False,
)
def get_title(self) -> str:
p = ""
while not p:
p = defaults.personality.random()
return p
def get_surname(self) -> str:
return super().get_name().replace("'", "").title() + self.suffixes.random()
Name = DraconicNameGenerator()
NobleName = NobleDraconicNameGenerator()

View File

@ -0,0 +1,7 @@
import logging
from language.rules import default_rules
logger = logging.getLogger("draconic-rules")
rules = default_rules

View File

@ -0,0 +1,14 @@
### Dwarvish
Dwarvish words are short, sharp, and to the point. Much like the Dwarves
themselves. This is "Low Dwarvish"; an ancient form of High Dwarvish still
exists in some regions, notably the Dewa Q'Asos area of southern Vosh, but it
is considered "dead" by most scholars and is reserved for arcane legal
contracts (and those signifying a high social status).
*Dowêchâ khâ, natu phe tu, futê dêdachî pê she wu?*
**Dwarvish Names:**
* "Black" Yzh Se
* Ka Shidothir
* Zhâ Syzhon

View File

@ -0,0 +1,7 @@
"""
Dwarvish
"""
from .base import Language
from .names import Name, NobleName
__all__ = [Language, Name, NobleName]

View File

@ -0,0 +1,54 @@
from language import types
from .rules import rules
vowels = types.WeightedSet(
("a", 1.0),
("e", 1.0),
("i", 0.3),
("o", 0.8),
("u", 0.7),
("y", 0.3),
("j", 0.05),
("î", 0.3),
("ê", 1.0),
("â", 1.0),
("û", 1.0),
)
consonants = types.WeightedSet(
("b", 0.3),
("c", 0.5),
("d", 1.0),
("f", 0.5),
("k", 1.0),
("l", 0.3),
("m", 0.3),
("n", 0.3),
("p", 1.0),
("s", 1.0),
("t", 1.0),
("v", 0.5),
("w", 0.5),
("y", 0.3),
("ph", 1.0),
("th", 1.0),
("ch", 1.0),
("kh", 1.0),
("zh", 1.0),
("sh", 1.0),
)
Language = types.Language(
name="dwarvish",
vowels=vowels,
consonants=consonants,
prefixes=None,
suffixes=None,
syllables=types.SyllableSet(
(types.Syllable(template="consonant,vowel|consonant") * 1, 1.0),
(types.Syllable(template="consonant,vowel|consonant") * 2, 0.5),
(types.Syllable(template="consonant,vowel|consonant") * 3, 0.2),
),
rules=rules,
minimum_grapheme_count=1,
)

View File

@ -0,0 +1,25 @@
from language import defaults, types
from language.languages.dwarvish import Language
class DwarvishNameGenerator(types.NameGenerator):
def __init__(self):
super().__init__(
language=Language,
templates=types.NameSet(
# (types.NameTemplate("adjective,name,nickname,surname"), 1.0),
(types.NameTemplate("adjective,name,surname"), 1.0),
),
affixes=None,
adjectives=defaults.adjectives,
titles=defaults.titles,
)
self.language.minimum_grapheme_count = 2
self.suffixes = types.equal_weights(["son", "sson", "zhon", "dottir", "dothir", "dottyr"], 1.0)
def get_surname(self) -> str:
return super().get_surname() + self.suffixes.random()
Name = DwarvishNameGenerator()
NobleName = Name

View File

@ -0,0 +1,24 @@
import logging
import re
from language.rules import default_rules
from language.types import Language
logger = logging.getLogger("dwarvish-rules")
def cannot_start_with_repeated_consonants(language: Language, word: str) -> bool:
found = re.compile(r"(^[bcdfghklmnpqrstvwxz]{2})").search(word)
if not found:
return True
first, second = found.group(1)
if first == second:
logger.debug(f"{word} starts with a repeated consonant.")
return False
return True
rules = default_rules
rules.add(cannot_start_with_repeated_consonants)

View File

@ -0,0 +1,20 @@
### Elvish
> The Elvish tongue flows like a river and pools upon the lips like sweetest wine. -- Anwin am Vakaralithien
Elvish is an ornate, multisyllabic language featuring a complex and nuanced set
of vowel sounds that can take non-native speakers decades to master. Surnames
are typically derived from place names; the more noble the name, the more
baroque its construction.
*Aaclysmior uewhön fyaeiath nöagefniath iedyglion ryavies, eamwien acyöe, uëstr udrocrem!*
**Elvish Names:**
* Afydyioth um Envyien
* Wjillias am Aofll
* Anyir um Edyoieth
**Noble Elvish Names:**
* Uebs Sviiies um Udjsmierios
* Nytyuia Oudryhn um Ioltier
* Emyea Whonoel an Ofyediar

View File

@ -0,0 +1,7 @@
"""
Elvish
"""
from .base import Language
from .names import Name, NobleName
__all__ = [Language, Name, NobleName]

View File

@ -0,0 +1,129 @@
from language import types
from .rules import rules
vowels = types.WeightedSet(
("a", 1.0),
("e", 1.0),
("i", 0.3),
("o", 0.8),
("u", 0.7),
("y", 0.3),
("j", 0.05),
("ä", 0.1),
("ë", 0.1),
("ö", 0.1),
("ü", 0.1),
)
consonants = types.WeightedSet(
("b", 0.5),
("c", 0.3),
("d", 0.3),
("f", 0.4),
("g", 0.3),
("h", 0.5),
("k", 0.05),
("l", 1.0),
("m", 1.0),
("n", 1.0),
("p", 0.5),
("r", 1.0),
("s", 1.0),
("t", 0.7),
("v", 0.3),
("w", 0.2),
("y", 0.3),
) + types.equal_weights(
[
"ch",
"cl",
"cr",
"cw",
"cy",
"dh",
"dj",
"dr",
"dw",
"dy",
"fl",
"fn",
"fr",
"fw",
"fy",
"gl",
"ll",
"ly",
"ml",
"mw",
"my",
"ny",
"rh",
"ry",
"sh",
"sl",
"sm",
"sn",
"st",
"sv",
"sw",
"sy",
"th",
"tr",
"tw",
"ty",
"vy",
"wh",
"wr",
"wy",
"yh",
]
)
suffixes = types.equal_weights(
[
"a",
"i",
"e",
"t",
"s",
"m",
"n",
"l",
"r",
"d",
"a",
"th",
"ss",
"ieth",
"ies",
"ier",
"ien",
"iath",
"ias",
"iar",
"ian",
"ioth",
"ios",
"ior",
"ion",
],
weight=1.0,
)
Language = types.Language(
name="elvish",
vowels=vowels,
consonants=consonants,
prefixes=None,
suffixes=suffixes,
syllables=types.SyllableSet(
(types.Syllable(template="vowel|consonant") * 3, 0.4),
(types.Syllable(template="vowel|consonant") * 3, 0.5),
(types.Syllable(template="vowel|consonant") * 4, 1.0),
(types.Syllable(template="vowel|consonant") * 5, 0.5),
(types.Syllable(template="vowel|consonant") * 6, 0.1),
(types.Syllable(template="vowel|consonant") * 7, 0.1),
),
rules=rules,
minimum_grapheme_count=3,
)

View File

@ -0,0 +1,77 @@
from language import defaults, types
from language.languages.elvish import Language
from language.languages.elvish.base import suffixes
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,
suffixes=suffixes,
)
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.language.minimum_grapheme_count = 2
self.place_generator = PlaceName
def get_surname(self) -> str:
return self.place_generator.name()[0]["name"][0]
class NobleElvishNameGenerator(types.NameGenerator):
def __init__(self):
super().__init__(
language=Language,
syllables=Language.syllables,
templates=types.NameSet(
(types.NameTemplate("name,name,affix,surname"), 1.0),
),
affixes=types.equal_weights(["am", "an", "al", "um"], weight=1.0, blank=False),
adjectives=defaults.adjectives,
titles=defaults.titles,
)
self.language.minimum_grapheme_count = 2
self.place_generator = PlaceName
self.suffixes = types.equal_weights(
[
"ieth",
"ies",
"ier",
"ien",
"iath",
"ias",
"iar",
"ian",
"ioth",
"ios",
"ior",
"ion",
],
1.0,
)
def get_surname(self) -> str:
return self.place_generator.name()[0]["name"][0] + self.suffixes.random()
Name = ElvishNameGenerator()
NobleName = NobleElvishNameGenerator()

View File

@ -0,0 +1,109 @@
import logging
import re
from language.types import Language
logger = logging.getLogger("elvish-rules")
permitted_starting_clusters = [
"ch",
"cl",
"cr",
"cw",
"cy",
"dh",
"dj",
"dr",
"dw",
"dy",
"fl",
"fn",
"fr",
"fw",
"fy",
"gl",
"ll",
"ly",
"ml",
"mw",
"my",
"ny",
"rh",
"ry",
"sh",
"sl",
"sm",
"sn",
"st",
"sv",
"sw",
"sy",
"th",
"tr",
"tw",
"ty",
"vy",
"wh",
"wr",
"wy",
"yh",
]
def cannot_start_with_two_consonants(language: Language, word: str) -> bool:
found = re.compile(r"(^[bcdfghklmnpqrstvwxz]{2})").search(word)
if not found:
return True
first, second = found.group(1)
if first == second and first != "l":
logger.debug(f"{word} starts with a repeated consonant.")
return False
if found.group(1) not in permitted_starting_clusters:
logger.debug("f{word} cannot start with {first}{second}")
return False
return True
def too_many_vowels(language: Language, word: str) -> bool:
found = re.compile(r"[" + "".join(language.vowels.members) + "]{4}").findall(word)
if found == []:
return True
logger.debug(f"{word} has too many contiguous vowels: {found}")
return False
def too_many_consonants(language: Language, word: str) -> bool:
found = re.compile(r"[bcdfghklmnprstvw]{3}").findall(word)
if found == []:
return True
logger.debug(f"{word} has too many contiguous consonants: {found}")
return False
def cannot_have_just_repeated_vowels(language: Language, word: str) -> bool:
if len(word) == 1:
return True
uniq = {letter for letter in word}
if len(uniq) > 1:
return True
logger.debug(f"{word} consists of only one repeated letter.")
return False
def must_have_a_vowel(language: Language, word: str) -> bool:
for vowel in language.vowels.members:
if vowel in word:
return True
logger.debug(f"{word} does not contain a vowel.")
return False
rules = {
must_have_a_vowel,
too_many_vowels,
too_many_consonants,
cannot_have_just_repeated_vowels,
cannot_start_with_two_consonants,
}

View File

@ -0,0 +1,13 @@
### Gnomish
Several distinct dialects of Gnomish exist; a provincial Rock Gnome may not
understand a Forest Gnome at all! This is "Golden Gnomish," the mother tongue
of these localized variants, and functions much like Common -- a trader's
tongue of common understanding.
*Nuio duy lao guey houe lo nouo, pia roa tai.*
**Gnomish Names:**
* Fuey Voii
* Juio See
* Tuai Reaa

View File

@ -0,0 +1,7 @@
"""
Gnomish
"""
from .base import Language
from .names import Name, NobleName
__all__ = [Language, Name, NobleName]

View File

@ -0,0 +1,48 @@
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,
)

View File

@ -0,0 +1,10 @@
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

View File

@ -0,0 +1,7 @@
import logging
from language.rules import default_rules
logger = logging.getLogger()
rules = default_rules

View File

@ -0,0 +1,13 @@
### Halfling
Halfings speak a trilling, lilting language with meaning and inflection rooted
in meter. It has proven to be nearly impossisble for most non-Halflings to
master, to the point that most cosmopolitan Halfings choose a Common nickname
by which they are known.
*Siheme'li'ba'e lo'neno'ne'o, mo'tepa'ga'i wezyropi'a lo'se'ni'metyo zevypiwee vyta'mo'a, da'na'jo'lee ti'soe fe'soe!*
**Halfling Names:**
* Hi'rina'pa'no'y Nylao Pe'lawyle'lo'a "Temperate"
* Jo'la'rogi'o Mi'li're'ne'pe'y Terinite've'e "Wise"
* Sa'la're'ta'je'o Re'dya Fetobo'a "Meritorious"

View File

@ -0,0 +1,7 @@
"""
Halfling
"""
from .base import Language
from .names import Name, NobleName
__all__ = [Language, Name, NobleName]

View File

@ -0,0 +1,54 @@
from language import types
from .rules import rules
vowels = types.equal_weights(["a'", "e'", "i'", "o'"], 1.0, blank=False) + types.equal_weights(
["a", "e", "i", "o", "y"], 0.5, blank=False
)
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="halfling",
vowels=vowels,
consonants=consonants,
prefixes=None,
suffixes=suffixes,
syllables=types.SyllableSet(
(types.Syllable(template="consonant,vowel") * 2, 0.5),
(types.Syllable(template="consonant,vowel") * 3, 0.75),
(types.Syllable(template="consonant,vowel") * 4, 1.0),
(types.Syllable(template="consonant,vowel") * 5, 1.0),
),
rules=rules,
minimum_grapheme_count=2,
)

View File

@ -0,0 +1,17 @@
from language import defaults, types
from language.languages.halfling import Language
class HalflingNameGenerator(types.NameGenerator):
def get_name(self) -> str:
return super().get_name().lower().capitalize()
Name = HalflingNameGenerator(
language=Language,
nicknames=defaults.positive_adjectives,
templates=types.NameSet(
(types.NameTemplate("name,name,name,nickname"), 1.0),
),
)
NobleName = Name

View File

@ -0,0 +1,7 @@
import logging
from language.rules import default_rules
logger = logging.getLogger()
rules = default_rules

View File

@ -0,0 +1,19 @@
### Infernal
The language of devils is a collection of gutteral barks that is unmistakable,
and unmistakably terrifying, to the ear of common folk. Traditional names
typically begin with the name of the ruler of whatever circle of Hell the devil
calls home, but some Tieflings (and others of Infernal origin) reject their
Infernal legacies and choose less noble names.
*Z'zait cocf k'd z'xz t'ugoo! Yipd k'j x'euy, z'edu k'oczu!*
**Infernal Names:**
* P'Utufus
* Golden Bdaeus
* Timeless Ujius
**Noble Infernal Names:**
* Mephistopheles T'Xieis
* Asmodeus T'Opakan
* Mammon K'Nusan

View File

@ -0,0 +1,7 @@
"""
Infernal
"""
from .base import Language
from .names import Name, NobleName
__all__ = [Language, Name, NobleName]

View File

@ -0,0 +1,52 @@
from language import defaults, types
from .rules import rules
consonants = types.WeightedSet(
("b", 1.0),
("c", 1.0),
("d", 1.0),
("f", 0.5),
("g", 0.5),
("j", 1.0),
("k", 1.0),
("l", 0.3),
("m", 0.3),
("n", 0.3),
("p", 1.0),
("r", 0.2),
("s", 0.1),
("t", 1.0),
("v", 1.0),
("x", 1.0),
("y", 1.0),
("z", 1.0),
)
prefixes = types.equal_weights(
[
"t'",
"x'",
"k'",
"p'",
"z'",
],
0.5,
)
Language = types.Language(
name="infernal",
vowels=defaults.vowels,
consonants=consonants,
prefixes=prefixes,
suffixes=None,
syllables=types.SyllableSet(
(types.Syllable(template="consonant|vowel") * 2, 0.05),
(types.Syllable(template="consonant|vowel") * 3, 1.0),
(types.Syllable(template="consonant|vowel") * 4, 0.75),
(types.Syllable(template="consonant|vowel") * 5, 0.5),
(types.Syllable(template="consonant|vowel") * 6, 0.25),
),
rules=rules,
minimum_grapheme_count=2,
)

View File

@ -0,0 +1,87 @@
from language import types
from language.languages.infernal import Language
adjectives = types.equal_weights(
[
"eternal",
"wondrous",
"luminous",
"perfect",
"essential",
"golden",
"unfailing",
"perpetual",
"infinite",
"exquisite",
"sinless",
"ultimate",
"flawless",
"timeless",
"glorious",
"absolute",
"boundless",
"true",
"incredible",
"virtuous",
"supreme",
"enchanted",
"magnificent",
"superior",
"spectacular",
"divine",
],
0.25,
)
bloodlines = types.equal_weights(
[
"Asmodeus",
"Baalzebul",
"Rimmon",
"Dispater",
"Fierna",
"Glasya",
"Levistus",
"Mammon",
"Mephistopheles",
"Zariel",
]
)
class InfernalNameGenerator(types.NameGenerator):
def __init__(self):
super().__init__(
language=Language,
templates=types.NameSet(
# (types.NameTemplate("adjective,name,nickname,surname"), 1.0),
(types.NameTemplate("name"), 0.25),
(types.NameTemplate("adjective,name"), 0.25),
),
adjectives=adjectives,
)
self.language.minimum_grapheme_count = 2
self.suffixes = types.equal_weights(["us", "ius", "eus", "a", "an", "is"], 1.0, blank=False)
def get_name(self) -> str:
return super().get_name() + self.suffixes.random()
class NobleInfernalNameGenerator(types.NameGenerator):
def __init__(self):
super().__init__(
language=Language,
templates=types.NameSet(
(types.NameTemplate("adjective,name"), 1.0),
),
adjectives=bloodlines,
)
self.language.minimum_grapheme_count = 2
self.suffixes = types.equal_weights(["us", "ius", "to", "tro", "eus", "a", "an", "is"], 1.0, blank=False)
def get_name(self) -> str:
return super().get_name() + self.suffixes.random()
Name = InfernalNameGenerator()
NobleName = NobleInfernalNameGenerator()

View File

@ -0,0 +1,7 @@
import logging
from language.rules import default_rules
logger = logging.getLogger("infernal-rules")
rules = default_rules

View File

@ -0,0 +1,14 @@
### Lizardfolk
The Lizardfolk language is a unique form of telepathy borne by pheramonne
exchange. It is literally impossible for non-Lizardfolk to speak it, though
with practice they can learn to recognize particular smells. Lizardfolk names
are collections of scent pattterns, the first of which denotes the Lizardfolk's
family origins.
*[No written language]*
**Lizardfolk Name Scents:**
* Carmelized Roast Pomegranate
* Rotting Hazelnut Citric
* Pepper Broth Grape

View File

@ -0,0 +1,7 @@
"""
Lizardfolk
"""
from .base import Language
from .names import Name, NobleName
__all__ = [Language, Name, NobleName]

View File

@ -0,0 +1,118 @@
from language import types
scents = types.equal_weights(
[
"honey",
"caramel",
"maple syrup",
"molasses",
"dark chocolate",
"chocolate",
"almond",
"hazelnut",
"peanut",
"clove",
"cinnamon",
"nutmeg",
"anise",
"malt",
"grain",
"roast",
"smoke",
"ash",
"acrid",
"rubber",
"skunk",
"petroleum",
"medicine",
"salt",
"bitter",
"phrenolic",
"meat",
"broth",
"animal",
"musty",
"earth",
"mould",
"damp",
"wood",
"paper",
"cardboard",
"stale",
"herb",
"hay",
"grass",
"peapod",
"whisky",
"wine",
"malic",
"citric",
"isovaleric",
"butyric",
"acetic",
"lime",
"lemon",
"orange",
"grapefruit",
"pear",
"peach",
"apple",
"grape",
"pineapple",
"pomegranate",
"cherry",
"coconut",
"prune",
"raisin",
"strawberry",
"blueberry",
"raspberry",
"blackberry",
"jasmine",
"rose",
"camomile",
"tobacco",
],
1.0,
blank=False,
)
family = types.equal_weights(
[
"sweet",
"floral",
"fruity",
"sour",
"fermented",
"green",
"vegetal",
"old",
"roasted",
"spiced",
"nutty",
"cocoa",
"pepper",
"pungent",
"burnt",
"carmelized",
"raw",
"rotting",
"dead",
"young",
],
1.0,
blank=False,
)
Language = types.Language(
name="lizardfolk",
vowels=scents,
consonants=family,
prefixes=None,
suffixes=None,
syllables=types.SyllableSet(
(types.Syllable(template="vowel"), 1.0),
),
rules=(),
minimum_grapheme_count=1,
)

View File

@ -0,0 +1,19 @@
from language import types
from language.languages.lizardfolk import Language
class LizardfolkNameGenerator(types.NameGenerator):
def __init__(self):
super().__init__(
language=Language,
templates=types.NameSet(
(types.NameTemplate("surname,name,name"), 1.0),
),
)
def get_surname(self) -> str:
return self.language.consonants.random().title()
Name = LizardfolkNameGenerator()
NobleName = Name

View File

@ -0,0 +1,13 @@
### Orcish
Spoken by Full- and Half-Orcs alike, Orcish bears some similarlities to
Dwarvish's clipped, mono-syllablic construction. They differ in Half-Orc's
prominent use of sibilants, perhaps as a result of being spoken by people with
large, protruding tusks.
*Dbod hanot neb, hushi dupo shiza fesha keke fucha tpu.*
**Orcish Names:**
* "Mad" Nha Pazk
* Zashi Dizh
* Pik Decht

View File

@ -0,0 +1,7 @@
"""
Orcish
"""
from .base import Language
from .names import Name, NobleName
__all__ = [Language, Name, NobleName]

View File

@ -0,0 +1,36 @@
from language import defaults, types
from .rules import rules
consonants = types.WeightedSet(
("b", 1.0),
("c", 1.0),
("d", 1.0),
("f", 0.5),
("h", 1.0),
("k", 1.0),
("m", 0.3),
("n", 0.3),
("p", 1.0),
("r", 0.2),
("s", 0.1),
("t", 1.0),
("z", 1.0),
("ch", 1.0),
("sh", 0.7),
("br", 1.0),
)
Language = types.Language(
name="orcish",
vowels=defaults.vowels,
consonants=consonants,
prefixes=None,
suffixes=None,
syllables=types.SyllableSet(
(types.Syllable(template="consonant,vowel") * 2, 1.0),
(types.Syllable(template="consonant,vowel,consonant,vowel,consonant"), 0.5),
),
rules=rules,
minimum_grapheme_count=1,
)

View File

@ -0,0 +1,60 @@
from language import defaults, types
from language.languages.orcish import Language
class OrcishNameGenerator(types.NameGenerator):
def __init__(self):
super().__init__(
language=Language,
templates=types.NameSet(
# (types.NameTemplate("adjective,name,nickname,surname"), 1.0),
(types.NameTemplate("adjective,name,surname"), 1.0),
),
affixes=None,
adjectives=defaults.adjectives,
titles=defaults.titles,
)
self.language.minimum_grapheme_count = 2
self.suffixes = types.equal_weights(
[
"acht",
"echt",
"icht",
"ocht",
"ucht",
"ak",
"ek",
"ik",
"ok",
"uk",
"ach",
"ech",
"ich",
"och",
"uch",
"atch",
"etch",
"itch",
"otch",
"utch",
"azk",
"ezk",
"izk",
"ozk",
"uzk",
"azh",
"ezh",
"izh",
"ozh",
"uzh",
],
1.0,
blank=False,
)
def get_surname(self) -> str:
return self.language.add_grapheme(word="", template="consonant").strip().title() + self.suffixes.random()
Name = OrcishNameGenerator()
NobleName = Name

View File

@ -0,0 +1,24 @@
import logging
import re
from language.rules import default_rules
from language.types import Language
logger = logging.getLogger("orcish-rules")
def cannot_start_with_repeated_consonants(language: Language, word: str) -> bool:
found = re.compile(r"(^[bcdfghklmnpqrstvwxz]{3})").search(word)
if not found:
return True
first, second, third = found.group(1)
if first == second == third:
logger.debug(f"{word} starts with a repeated consonant.")
return False
return True
rules = default_rules
rules.add(cannot_start_with_repeated_consonants)

View File

@ -0,0 +1,18 @@
### Undercommon
The language of the Drow is the defacto language of the Underdark, and is
spoken by most peoples there. Like the Drow themselves, Undercommon diverged
from an Elvish dialect in ancient times. The two still bear some resemblance,
notably in the construction of names.
*Okvösaa licopod lohiwia uüyötüe uywubit uegäsit uäpisoa, caquyee redoxöd, mraomoa.*
**Undercommon Names:**
* Tfubam Umaiuhüen
* Nanosd Aneedaöbaas
* Rerwus Amzuremolr
**Noble Undercommon (Drow) Names:**
* Uopiyie Alzejakäurn
* Opyäulol Elchöläss
* Iqyäuwed Anlcoduir

View File

@ -0,0 +1,7 @@
"""
Undercommon
"""
from .base import Language
from .names import Name, NobleName
__all__ = [Language, Name, NobleName]

View File

@ -0,0 +1,21 @@
from language import defaults, types
from .rules import rules
vowels = defaults.vowels + types.equal_weights(["ä", "ö", "ü", "äu"], 0.5, blank=False)
prefixes = defaults.vowels + types.equal_weights(["c", "g", "l", "m", "n", "r", "s", "t", "v", "z"], 1.0, blank=False)
suffixes = types.equal_weights(["a", "e", "i", "t", "s", "m", "n", "l", "r", "d", "a", "th"], 1.0, blank=False)
Language = types.Language(
name="undercommon",
vowels=vowels,
consonants=defaults.consonants,
prefixes=prefixes,
suffixes=suffixes,
syllables=types.SyllableSet(
(types.Syllable(template="vowel,consonant,vowel") * 2, 0.15),
(types.Syllable(template="consonant|vowel,consonant,vowel,consonant,vowel"), 1.0),
),
rules=rules,
minimum_grapheme_count=2,
)

View File

@ -0,0 +1,37 @@
import random
from language import defaults, types
from language.languages.undercommon import Language
PlaceName = types.NameGenerator(
language=Language,
syllables=Language.syllables,
templates=types.NameSet(
(types.NameTemplate("affix,name"), 1.0),
),
affixes=types.WeightedSet(("el", 1.0)),
adjectives=defaults.adjectives,
suffixes=Language.suffixes,
)
class DrowName(types.NameGenerator):
def __init__(self):
super().__init__(
language=Language,
syllables=Language.syllables,
templates=types.NameSet(
(types.NameTemplate("name,surname"), 1.0),
),
)
self.language.minimum_grapheme_count = 2
self.place_generator = PlaceName
self.affixes = types.equal_weights(["am", "an", "al", "um"], weight=1.0, blank=False)
def get_surname(self) -> str:
name = self.place_generator.name()[0]["name"][0]
return (self.affixes.random() + name + random.choice(["th", "s", "r", "n"])).title()
Name = DrowName()
NobleName = Name

View File

@ -0,0 +1,88 @@
import logging
import re
from language.rules import default_rules
from language.types import Language
logger = logging.getLogger()
valid_consonant_sequences = [
"cc",
"ht",
"kd",
"kl",
"km",
"kp",
"kt",
"kv",
"kw",
"ky",
"lc",
"ld",
"lf",
"ll",
"lm",
"lp",
"lt",
"lv",
"lw",
"ly",
"mb",
"mm",
"mp",
"my",
"nc",
"nd",
"ng",
"nn",
"nt",
"nw",
"ny",
"ps",
"pt",
"rc",
"rd",
"rm",
"rn",
"rp",
"rr",
"rs",
"rt",
"rw",
"ry",
"sc",
"ss",
"ts",
"tt",
"th",
"tw",
"ty",
]
def valid_sequences(language: Language, word: str) -> bool:
found = re.compile(r"([bcdfghjklmnpqrstvwxz]{2})").findall(word)
if not found:
return True
invalid = [seq for seq in found if seq not in valid_consonant_sequences]
if invalid:
logger.debug(f"{word} contains invalid consonant sequences: {invalid}")
return False
return True
def too_many_vowels(language: Language, word: str) -> bool:
found = re.compile(r"[" + "".join(language.vowels.members) + r"]{3}").findall(word)
if found == []:
return True
logger.debug(f"{word} has too many contiguous vowels: {found}")
return False
rules = default_rules.union(
{
valid_sequences,
too_many_vowels,
}
)

43
language/rules.py Normal file
View File

@ -0,0 +1,43 @@
import logging
import re
from language.types import Language
logger = logging.getLogger()
def too_many_vowels(language: Language, word: str) -> bool:
found = re.compile(r"[aeiou]{3}").findall(word)
if found == []:
return True
logger.debug(f"{word} has too many contiguous vowels: {found}")
return False
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
def cannot_have_just_repeated_vowels(language: Language, word: str) -> bool:
if len(word) == 1:
return True
uniq = {letter for letter in word}
if len(uniq) > 1:
return True
logger.debug(f"{word} consists of only one repeated letter.")
return False
def must_have_a_vowel(language: Language, word: str) -> bool:
for vowel in language.vowels.members:
if vowel in word:
return True
logger.debug(f"{word} does not contain a vowel.")
return False
default_rules = {must_have_a_vowel, too_many_vowels, too_many_consonants, cannot_have_just_repeated_vowels}

461
language/types.py Normal file
View File

@ -0,0 +1,461 @@
import inspect
import random
from collections import defaultdict
from typing import Union
class LanguageError(Exception):
"""
Thrown when an error is encountered in language construction.
"""
class ImprobableTemplateError(Exception):
"""
Thrown when too many successive attempts to create a word which passes all
language rules fails.
"""
class WeightedSet:
"""
A set in which members each have a weight, used for selecting at random.
Usage:
>>> ws = WeightedSet(('foo', 1.0), ('bar', 0.5))
>>> ws.random()
('foo', 1.0)
"""
def __init__(self, *weighted_members: tuple):
self.members = []
self.weights = []
if weighted_members:
self.members, self.weights = list(zip(*weighted_members))
def random(self) -> str:
return random.choices(self.members, self.weights)[0]
def __add__(self, obj):
ws = WeightedSet()
ws.members = self.members + obj.members
ws.weights = self.weights + obj.weights
return ws
def __str__(self):
return f"{self.members}\n{self.weights}"
class Syllable:
"""
One syllable of a word. Used to populate a SyllableSet.
A syllable template is a string consisting of one or more grapheme types
separated by a vertical pipe (|). Multiple template strings can be
concatenated together with commas. When words are constructed, each
syllable is populated with a random sequence chosen by
Language.add_grapheme().
A syllable template must contain at least one 'vowel'.
Syllables can be multiplied by integers to produce repeated templates.
Usage:
# A syllable consisting of either a vowel or a consonant, followed by
# a vowel, followed by either a vowel or consonant.
>>> foo = Syllable(template='vowel|consonant,vowel,consonant|vowel')
# Example multiplication
>>> print(Syllable(template='vowel|consonant') * 3)
vowel|consonant vowel|consonant vowel|consonant
"""
def __init__(self, template: str = "vowel|consonant"):
self.template = template
self.validate()
def validate(self):
if "vowel" not in self.template:
raise LanguageError(
f"Invalid syllable template {self.template}!\n"
"Syllables must have at least one vowel in the template."
)
def __mul__(self, count: int):
return Syllable(template=",".join([self.template] * count))
__rmul__ = __mul__
def __str__(self):
return self.template
class SyllableSet(WeightedSet):
"""
A WeightedSet that selects random syllables.
Usage:
>>> word = SyllableSet(
(Syllable('vowel'), 1.0),
(Syllable('vowel|consonant') * 2, 1.0),
(syllable('vowel|consonant,vowel|consonant') * 3, 0.75)
)
>>> word.random()
vowel consonant consonant vowel
"""
def random(self) -> iter:
for syllable in random.choices(self.members, self.weights)[0].template.split(","):
grapheme_template = random.choice(syllable.split("|"))
yield grapheme_template.lower()
class Language:
"""
A class representing a language.
Usage:
>>> Common = Language(
name="common",
vowels=WeightedSet(("a", 1.0), ("e", 1.0), ("i", 1.0), ...),
consonants=WeightedSet(("b", 0.5), ("c", 0.5), ("d", 0.5), ...),
prefixes=WeightedSet(("re", 0.5), ("de", 0.5), ("", 1.0), ...),
suffixes=WeightedSet(("ed", 0.5), ("ing", 0.5), ("", 1.0), ...),
syllables=SyllableSet(
(Syllable('consonant|vowel'), 1.0),
(Syllable('consonant|vowel') * 2, 0.75),
...
),
rules=set(callable1, callable2, ...),
minimum_grapheme_count=2,
)
>>> Common.word()
reibing
How Words Are Constructed:
The main interface for callers is word(), which returns a
randomly-generated word in the language according to the following
algorithm:
1. Choose a random syllable from the syllable set
2. For each grapheme in the syllable
3. Choose a random grapheme template
4. Choose a random sequence from the language for that grapheme
5. Validate the word against the language rules
6. Repeat 1-5 until a valid word is generated
7. Add a prefix and suffix, if they are defined
The following graphemes are supported by default:
- vowel
- consonant
- prefix
- suffix
When graphemes are chosen, the following rules are applied:
- Every syllable must have at least one vowel
- A syllable may never have three consecutive consonants
How Words Are Validated:
Once a word has been constructed by populating syllable templates, it is
tested against one or more language rules.
The default rules are defined in language.rules.default_rules; they are:
- the word must contain at least one vowel
- the word must not contain 3 or more contiguous english vowels
- the word must not contain 3 or more contiguous english consonants
- the word must not consist of just one vowel, repeated
Since it is possible to craft Syllables resulting in grapheme
selections that rarely or never yield valid words, or rules that
reject every word, an ImprobableTemplateError will be thrown if
10 successive attempts to create a valid word fail.
Extending Languages:
Graphemes are populated by means of callbacks which select a member
of the associated weighted set at random. Graphemes can be any string,
so long as the Language class has a matching callback.
To add support for a new grapheme type, define a method on your
Language class called get_grapheme_TYPE, where TYPE is the string
used in your Syllable templates. Examine test cases in test_types.py
for examples.
"""
def __init__(
self,
name: str,
vowels: WeightedSet,
consonants: WeightedSet,
prefixes: WeightedSet,
suffixes: WeightedSet,
syllables: SyllableSet,
rules: set = set(),
minimum_grapheme_count: int = 1,
):
"""
Args:
name - friendly name for the language
vowels - the weighted set of vowel graphemes
consonants - the weighted set of consonant graphemes
prefixes - the weighted set of prefix graphemes
suffixes - the weighted set of suffix graphemes
rules - a set of rules callbacks; see above.
minimum_grapheme_count - the minimum number of graphemes in each word
"""
self.name = name
self.vowels = vowels
self.consonants = consonants
self.prefixes = prefixes
self.suffixes = suffixes
self.rules = rules
self.syllables = syllables
self.minimum_grapheme_count = minimum_grapheme_count
self.validate_syllable_set()
self.handlers = dict([(n, v) for (n, v) in inspect.getmembers(self, inspect.ismethod) if n.startswith("get_")])
def validate(self, word: str) -> bool:
"""
Returns true if the given word is possible in the current language.
"""
if not word:
return False
for rule in self.rules:
if not rule(self, word):
return False
return True
def validate_syllable_set(self):
for syllable in self.syllables.members:
if len(syllable.template.split(",")) < self.minimum_grapheme_count:
raise ImprobableTemplateError(
f"Syllable {syllable} does not define enough graphemes ({self.minimum_grapheme_count} required)."
)
def validate_graphemes(self, graphemes: list) -> bool:
if len(graphemes) < self.minimum_grapheme_count:
return False
last = ""
count = 0
for g in graphemes:
if g == last:
count += 1
if count == 3:
return False
else:
count = 1
last = g
return True
def word(self, count: int = 1) -> list:
"""
Yields words composed of randomized phonemes built from a random word template.
"""
words = []
for _ in range(count):
random_word = ""
attempts = 0
while not self.validate(random_word):
if attempts == 10:
raise ImprobableTemplateError(
f"Exhausted all attempts to create a valid word. Last attempt: {random_word}. "
"If you're getting this a lot, try enabling debugging to see what rules are failing."
)
graphemes = []
random_word = ""
while not self.validate_graphemes(graphemes):
graphemes = list(self.syllables.random())
for grapheme in graphemes:
random_word = self.add_grapheme(random_word, grapheme)
attempts += 1
if self.prefixes:
random_word = self.get_grapheme_prefix() + random_word
if self.suffixes:
random_word = random_word + self.get_grapheme_suffix()
words.append(random_word)
return words
def add_grapheme(self, word: str, template: str) -> str:
"""
Returns a random grapheme of a supported type. The class must support a method of the name:
get_grapheme_{template}
"""
template = template.lower()
try:
return word + self.handlers[f"get_grapheme_{template}"]()
except KeyError:
raise NotImplementedError(
f"No handler found for grapheme template '{template}'. "
f"Do you need to define get_grapheme_{template}()?\n"
"Supported handlers: " + self.handlers.keys
)
def get_grapheme_consonant(self) -> str:
return self.consonants.random()
def get_grapheme_vowel(self) -> str:
return self.vowels.random()
def get_grapheme_prefix(self) -> str:
return self.prefixes.random()
def get_grapheme_suffix(self) -> str:
return self.suffixes.random()
def text(self, count: int = 25) -> str:
phrases = []
phrase = []
for word in self.word(count):
phrase.append(str(word))
if len(phrase) >= random.randint(1, 12):
phrases.append(" ".join(phrase))
phrase = []
if phrase:
phrases.append(" ".join(phrase))
paragraph = phrases[0].capitalize()
for phrase in phrases[1:]:
if random.choice([0, 0, 1]):
paragraph = paragraph + random.choice("?!.") + " " + phrase.capitalize()
else:
paragraph = paragraph + ", " + phrase
paragraph = paragraph + random.choice("?!.")
return paragraph
def copy(self):
return self.__class__(
name=self.name,
vowels=self.vowels,
consonants=self.consonants,
prefixes=self.prefixes,
suffixes=self.suffixes,
rules=self.rules,
syllables=self.syllables,
minimum_grapheme_count=self.minimum_grapheme_count,
)
def __str__(self) -> str:
return self.word()[0]
NameSet = SyllableSet
class Name(defaultdict):
def __str__(self):
return self["fullname"][0]
class NameTemplate(Syllable):
def validate(self):
pass
class NameGenerator:
def __init__(
self,
language: Language,
templates: NameSet,
syllables: Union[SyllableSet, None] = None,
names: Union[WeightedSet, None] = None,
surnames: Union[WeightedSet, None] = None,
nicknames: Union[WeightedSet, None] = None,
adjectives: Union[WeightedSet, None] = None,
titles: Union[WeightedSet, None] = None,
counts: Union[WeightedSet, None] = None,
affixes: Union[WeightedSet, None] = None,
suffixes: Union[WeightedSet, None] = None,
):
self.language = language.copy()
if syllables:
self.language.syllables = syllables
self.templates = templates
self._names = names
self._surnames = surnames
self._nicknames = nicknames
self._adjectives = adjectives
self._titles = titles
self._counts = counts
self._suffixes = suffixes
self._affixes = affixes
self.handlers = dict([(n, v) for (n, v) in inspect.getmembers(self, inspect.ismethod) if n.startswith("get_")])
def name(self, count: int = 1) -> list:
"""
Generate Name instances.
"""
names = []
for _ in range(count):
name = Name(list)
fullname = []
for part in self.templates.random():
thisname = self.add_part(part).strip()
if not thisname:
continue
name[part].append(thisname)
fullname.append(thisname)
name["fullname"] = " ".join(fullname)
names.append(name)
return names
def add_part(self, template: str) -> str:
template = template.lower()
try:
return self.handlers[f"get_{template}"]()
except KeyError:
raise NotImplementedError(
f"No handler found for name template '{template}' on class {self.__class__.__name__}. "
f"Do you need to define get_{template}()?\nSupported Handlers: "
+ ",".join(n for n in dir(self) if n.startswith("get_"))
)
def get_name(self) -> str:
name = (self._names.random() if self._names else self.language.word())[0]
return name.title()
def get_surname(self) -> str:
name = (self._surnames.random() if self._surnames else self.language.word())[0]
if self._suffixes:
name = name + self._suffixes.random()
if len(name) == 1:
name = f"{name}."
return name.title()
def get_adjective(self) -> str:
return (self._adjectives.random() if self._adjectives else "").title()
def get_affix(self) -> str:
return self._affixes.random() if self._affixes else ""
def get_title(self) -> str:
return (self._titles.random() if self._titles else "").title()
def get_the(self) -> str:
return "the"
def get_count(self) -> str:
return self._counts.random() if self._counts else ""
def get_nickname(self) -> str:
name = (self._nicknames.random() if self._nicknames else "").title()
if name:
return '"' + name + '"'
return ""
def get_initial(self) -> str:
return
def __str__(self) -> str:
return self.name()[0]["fullname"]
def equal_weights(terms: list, weight: float = 1.0, blank: bool = True) -> WeightedSet:
ws = WeightedSet(*[(term, weight) for term in terms])
if blank:
ws = WeightedSet(("", 1.0)) + ws
return ws

47
pyproject.toml Normal file
View File

@ -0,0 +1,47 @@
[tool.poetry]
name = "dnd-name-generator"
version = "1.0"
description = "Fantasy language generators for D&D"
authors = ["evilchili <evilchili@gmail.com>"]
license = "The Unlicense"
packages = [
{ include = 'language' },
]
[tool.poetry.dependencies]
python = "^3.10"
typer = "latest"
rich = "latest"
dice = "latest"
[tool.poetry.dev-dependencies]
pytest = "latest"
black = "^23.3.0"
isort = "^5.12.0"
pyproject-autoflake = "^1.0.2"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.black]
line-length = 120
target-version = ['py310']
[tool.isort]
multi_line_output = 3
line_length = 120
include_trailing_comma = true
[tool.autoflake]
check = false # return error code if changes are needed
in-place = true # make changes to files instead of printing diffs
recursive = true # drill down directories recursively
remove-all-unused-imports = true # remove all unused imports (not just those from the standard library)
ignore-init-module-imports = true # exclude __init__.py when removing unused imports
remove-duplicate-keys = true # remove all duplicate keys in objects
remove-unused-variables = true # remove unused variables
[tool.poetry.scripts]
fanlang = "language.cli:app"