From 45f4d6e4019f703dd5c564676655be6fe8179dc9 Mon Sep 17 00:00:00 2001 From: evilchili Date: Fri, 24 Nov 2023 08:48:03 -0500 Subject: [PATCH] initial import --- LICENSE | 24 + README.md | 129 +++ language/__init__.py | 1 + language/cli.py | 72 ++ language/defaults.py | 864 +++++++++++++++++++++ language/languages/__init__.py | 14 + language/languages/abyssal/README.md | 16 + language/languages/abyssal/__init__.py | 7 + language/languages/abyssal/base.py | 18 + language/languages/abyssal/names.py | 11 + language/languages/celestial/README.md | 15 + language/languages/celestial/__init__.py | 7 + language/languages/celestial/base.py | 21 + language/languages/celestial/names.py | 11 + language/languages/common/README.md | 16 + language/languages/common/__init__.py | 7 + language/languages/common/base.py | 158 ++++ language/languages/common/names.py | 82 ++ language/languages/common/rules.py | 115 +++ language/languages/draconic/README.md | 23 + language/languages/draconic/__init__.py | 7 + language/languages/draconic/base.py | 47 ++ language/languages/draconic/names.py | 88 +++ language/languages/draconic/rules.py | 7 + language/languages/dwarvish/README.md | 14 + language/languages/dwarvish/__init__.py | 7 + language/languages/dwarvish/base.py | 54 ++ language/languages/dwarvish/names.py | 25 + language/languages/dwarvish/rules.py | 24 + language/languages/elvish/README.md | 20 + language/languages/elvish/__init__.py | 7 + language/languages/elvish/base.py | 129 +++ language/languages/elvish/names.py | 77 ++ language/languages/elvish/rules.py | 109 +++ language/languages/gnomish/README.md | 13 + language/languages/gnomish/__init__.py | 7 + language/languages/gnomish/base.py | 48 ++ language/languages/gnomish/names.py | 10 + language/languages/gnomish/rules.py | 7 + language/languages/halfling/README.md | 13 + language/languages/halfling/__init__.py | 7 + language/languages/halfling/base.py | 54 ++ language/languages/halfling/names.py | 17 + language/languages/halfling/rules.py | 7 + language/languages/infernal/README.md | 19 + language/languages/infernal/__init__.py | 7 + language/languages/infernal/base.py | 52 ++ language/languages/infernal/names.py | 87 +++ language/languages/infernal/rules.py | 7 + language/languages/lizardfolk/README.md | 14 + language/languages/lizardfolk/__init__.py | 7 + language/languages/lizardfolk/base.py | 118 +++ language/languages/lizardfolk/names.py | 19 + language/languages/orcish/README.md | 13 + language/languages/orcish/__init__.py | 7 + language/languages/orcish/base.py | 36 + language/languages/orcish/names.py | 60 ++ language/languages/orcish/rules.py | 24 + language/languages/undercommon/README.md | 18 + language/languages/undercommon/__init__.py | 7 + language/languages/undercommon/base.py | 21 + language/languages/undercommon/names.py | 37 + language/languages/undercommon/rules.py | 88 +++ language/rules.py | 43 + language/types.py | 461 +++++++++++ pyproject.toml | 47 ++ 66 files changed, 3601 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 language/__init__.py create mode 100644 language/cli.py create mode 100644 language/defaults.py create mode 100644 language/languages/__init__.py create mode 100644 language/languages/abyssal/README.md create mode 100644 language/languages/abyssal/__init__.py create mode 100644 language/languages/abyssal/base.py create mode 100644 language/languages/abyssal/names.py create mode 100644 language/languages/celestial/README.md create mode 100644 language/languages/celestial/__init__.py create mode 100644 language/languages/celestial/base.py create mode 100644 language/languages/celestial/names.py create mode 100644 language/languages/common/README.md create mode 100644 language/languages/common/__init__.py create mode 100644 language/languages/common/base.py create mode 100644 language/languages/common/names.py create mode 100644 language/languages/common/rules.py create mode 100644 language/languages/draconic/README.md create mode 100644 language/languages/draconic/__init__.py create mode 100644 language/languages/draconic/base.py create mode 100644 language/languages/draconic/names.py create mode 100644 language/languages/draconic/rules.py create mode 100644 language/languages/dwarvish/README.md create mode 100644 language/languages/dwarvish/__init__.py create mode 100644 language/languages/dwarvish/base.py create mode 100644 language/languages/dwarvish/names.py create mode 100644 language/languages/dwarvish/rules.py create mode 100644 language/languages/elvish/README.md create mode 100644 language/languages/elvish/__init__.py create mode 100644 language/languages/elvish/base.py create mode 100644 language/languages/elvish/names.py create mode 100644 language/languages/elvish/rules.py create mode 100644 language/languages/gnomish/README.md create mode 100644 language/languages/gnomish/__init__.py create mode 100644 language/languages/gnomish/base.py create mode 100644 language/languages/gnomish/names.py create mode 100644 language/languages/gnomish/rules.py create mode 100644 language/languages/halfling/README.md create mode 100644 language/languages/halfling/__init__.py create mode 100644 language/languages/halfling/base.py create mode 100644 language/languages/halfling/names.py create mode 100644 language/languages/halfling/rules.py create mode 100644 language/languages/infernal/README.md create mode 100644 language/languages/infernal/__init__.py create mode 100644 language/languages/infernal/base.py create mode 100644 language/languages/infernal/names.py create mode 100644 language/languages/infernal/rules.py create mode 100644 language/languages/lizardfolk/README.md create mode 100644 language/languages/lizardfolk/__init__.py create mode 100644 language/languages/lizardfolk/base.py create mode 100644 language/languages/lizardfolk/names.py create mode 100644 language/languages/orcish/README.md create mode 100644 language/languages/orcish/__init__.py create mode 100644 language/languages/orcish/base.py create mode 100644 language/languages/orcish/names.py create mode 100644 language/languages/orcish/rules.py create mode 100644 language/languages/undercommon/README.md create mode 100644 language/languages/undercommon/__init__.py create mode 100644 language/languages/undercommon/base.py create mode 100644 language/languages/undercommon/names.py create mode 100644 language/languages/undercommon/rules.py create mode 100644 language/rules.py create mode 100644 language/types.py create mode 100644 pyproject.toml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/LICENSE @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..408e3c6 --- /dev/null +++ b/README.md @@ -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. diff --git a/language/__init__.py b/language/__init__.py new file mode 100644 index 0000000..edf123a --- /dev/null +++ b/language/__init__.py @@ -0,0 +1 @@ +from .languages import supported_languages diff --git a/language/cli.py b/language/cli.py new file mode 100644 index 0000000..2769f64 --- /dev/null +++ b/language/cli.py @@ -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() diff --git a/language/defaults.py b/language/defaults.py new file mode 100644 index 0000000..81da34c --- /dev/null +++ b/language/defaults.py @@ -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, +) diff --git a/language/languages/__init__.py b/language/languages/__init__.py new file mode 100644 index 0000000..14c802a --- /dev/null +++ b/language/languages/__init__.py @@ -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__])) +) diff --git a/language/languages/abyssal/README.md b/language/languages/abyssal/README.md new file mode 100644 index 0000000..2cd8f6b --- /dev/null +++ b/language/languages/abyssal/README.md @@ -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 diff --git a/language/languages/abyssal/__init__.py b/language/languages/abyssal/__init__.py new file mode 100644 index 0000000..7a0313d --- /dev/null +++ b/language/languages/abyssal/__init__.py @@ -0,0 +1,7 @@ +""" +Abyssal +""" +from .base import Language +from .names import Name, NobleName + +__all__ = [Language, Name, NobleName] diff --git a/language/languages/abyssal/base.py b/language/languages/abyssal/base.py new file mode 100644 index 0000000..f0fd195 --- /dev/null +++ b/language/languages/abyssal/base.py @@ -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), + ), +) diff --git a/language/languages/abyssal/names.py b/language/languages/abyssal/names.py new file mode 100644 index 0000000..0cc5803 --- /dev/null +++ b/language/languages/abyssal/names.py @@ -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 diff --git a/language/languages/celestial/README.md b/language/languages/celestial/README.md new file mode 100644 index 0000000..69b9013 --- /dev/null +++ b/language/languages/celestial/README.md @@ -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 diff --git a/language/languages/celestial/__init__.py b/language/languages/celestial/__init__.py new file mode 100644 index 0000000..41e7b04 --- /dev/null +++ b/language/languages/celestial/__init__.py @@ -0,0 +1,7 @@ +""" +Celestial +""" +from .base import Language +from .names import Name, NobleName + +__all__ = [Language, Name, NobleName] diff --git a/language/languages/celestial/base.py b/language/languages/celestial/base.py new file mode 100644 index 0000000..0730fc6 --- /dev/null +++ b/language/languages/celestial/base.py @@ -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", "ȧ", "ė", "ị", "ȯ", "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), + ), +) diff --git a/language/languages/celestial/names.py b/language/languages/celestial/names.py new file mode 100644 index 0000000..c97749d --- /dev/null +++ b/language/languages/celestial/names.py @@ -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 diff --git a/language/languages/common/README.md b/language/languages/common/README.md new file mode 100644 index 0000000..0af1ac1 --- /dev/null +++ b/language/languages/common/README.md @@ -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 diff --git a/language/languages/common/__init__.py b/language/languages/common/__init__.py new file mode 100644 index 0000000..5f408ab --- /dev/null +++ b/language/languages/common/__init__.py @@ -0,0 +1,7 @@ +""" +Common +""" +from .base import Language +from .names import Name, NobleName + +__all__ = [Language, Name, NobleName] diff --git a/language/languages/common/base.py b/language/languages/common/base.py new file mode 100644 index 0000000..fa35023 --- /dev/null +++ b/language/languages/common/base.py @@ -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, +) diff --git a/language/languages/common/names.py b/language/languages/common/names.py new file mode 100644 index 0000000..1201e97 --- /dev/null +++ b/language/languages/common/names.py @@ -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 diff --git a/language/languages/common/rules.py b/language/languages/common/rules.py new file mode 100644 index 0000000..1d7e2e2 --- /dev/null +++ b/language/languages/common/rules.py @@ -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, + } +) diff --git a/language/languages/draconic/README.md b/language/languages/draconic/README.md new file mode 100644 index 0000000..afc45da --- /dev/null +++ b/language/languages/draconic/README.md @@ -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 diff --git a/language/languages/draconic/__init__.py b/language/languages/draconic/__init__.py new file mode 100644 index 0000000..969988a --- /dev/null +++ b/language/languages/draconic/__init__.py @@ -0,0 +1,7 @@ +""" +Draconic +""" +from .base import Language +from .names import Name, NobleName + +__all__ = [Language, Name, NobleName] diff --git a/language/languages/draconic/base.py b/language/languages/draconic/base.py new file mode 100644 index 0000000..ed2c21f --- /dev/null +++ b/language/languages/draconic/base.py @@ -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, +) diff --git a/language/languages/draconic/names.py b/language/languages/draconic/names.py new file mode 100644 index 0000000..d9ce3d2 --- /dev/null +++ b/language/languages/draconic/names.py @@ -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() diff --git a/language/languages/draconic/rules.py b/language/languages/draconic/rules.py new file mode 100644 index 0000000..18d9ba7 --- /dev/null +++ b/language/languages/draconic/rules.py @@ -0,0 +1,7 @@ +import logging + +from language.rules import default_rules + +logger = logging.getLogger("draconic-rules") + +rules = default_rules diff --git a/language/languages/dwarvish/README.md b/language/languages/dwarvish/README.md new file mode 100644 index 0000000..1607668 --- /dev/null +++ b/language/languages/dwarvish/README.md @@ -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 diff --git a/language/languages/dwarvish/__init__.py b/language/languages/dwarvish/__init__.py new file mode 100644 index 0000000..ea15678 --- /dev/null +++ b/language/languages/dwarvish/__init__.py @@ -0,0 +1,7 @@ +""" +Dwarvish +""" +from .base import Language +from .names import Name, NobleName + +__all__ = [Language, Name, NobleName] diff --git a/language/languages/dwarvish/base.py b/language/languages/dwarvish/base.py new file mode 100644 index 0000000..ef5afe6 --- /dev/null +++ b/language/languages/dwarvish/base.py @@ -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, +) diff --git a/language/languages/dwarvish/names.py b/language/languages/dwarvish/names.py new file mode 100644 index 0000000..19c3abd --- /dev/null +++ b/language/languages/dwarvish/names.py @@ -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 diff --git a/language/languages/dwarvish/rules.py b/language/languages/dwarvish/rules.py new file mode 100644 index 0000000..11f9b46 --- /dev/null +++ b/language/languages/dwarvish/rules.py @@ -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) diff --git a/language/languages/elvish/README.md b/language/languages/elvish/README.md new file mode 100644 index 0000000..dd8d0fa --- /dev/null +++ b/language/languages/elvish/README.md @@ -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 diff --git a/language/languages/elvish/__init__.py b/language/languages/elvish/__init__.py new file mode 100644 index 0000000..94bb23a --- /dev/null +++ b/language/languages/elvish/__init__.py @@ -0,0 +1,7 @@ +""" +Elvish +""" +from .base import Language +from .names import Name, NobleName + +__all__ = [Language, Name, NobleName] diff --git a/language/languages/elvish/base.py b/language/languages/elvish/base.py new file mode 100644 index 0000000..116ffbd --- /dev/null +++ b/language/languages/elvish/base.py @@ -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, +) diff --git a/language/languages/elvish/names.py b/language/languages/elvish/names.py new file mode 100644 index 0000000..9ec76d6 --- /dev/null +++ b/language/languages/elvish/names.py @@ -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() diff --git a/language/languages/elvish/rules.py b/language/languages/elvish/rules.py new file mode 100644 index 0000000..12019a5 --- /dev/null +++ b/language/languages/elvish/rules.py @@ -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, +} diff --git a/language/languages/gnomish/README.md b/language/languages/gnomish/README.md new file mode 100644 index 0000000..9024b6d --- /dev/null +++ b/language/languages/gnomish/README.md @@ -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 diff --git a/language/languages/gnomish/__init__.py b/language/languages/gnomish/__init__.py new file mode 100644 index 0000000..6a57e44 --- /dev/null +++ b/language/languages/gnomish/__init__.py @@ -0,0 +1,7 @@ +""" +Gnomish +""" +from .base import Language +from .names import Name, NobleName + +__all__ = [Language, Name, NobleName] diff --git a/language/languages/gnomish/base.py b/language/languages/gnomish/base.py new file mode 100644 index 0000000..a7e7d99 --- /dev/null +++ b/language/languages/gnomish/base.py @@ -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, +) diff --git a/language/languages/gnomish/names.py b/language/languages/gnomish/names.py new file mode 100644 index 0000000..8a70831 --- /dev/null +++ b/language/languages/gnomish/names.py @@ -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 diff --git a/language/languages/gnomish/rules.py b/language/languages/gnomish/rules.py new file mode 100644 index 0000000..09d965f --- /dev/null +++ b/language/languages/gnomish/rules.py @@ -0,0 +1,7 @@ +import logging + +from language.rules import default_rules + +logger = logging.getLogger() + +rules = default_rules diff --git a/language/languages/halfling/README.md b/language/languages/halfling/README.md new file mode 100644 index 0000000..2f0c311 --- /dev/null +++ b/language/languages/halfling/README.md @@ -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" diff --git a/language/languages/halfling/__init__.py b/language/languages/halfling/__init__.py new file mode 100644 index 0000000..291767c --- /dev/null +++ b/language/languages/halfling/__init__.py @@ -0,0 +1,7 @@ +""" +Halfling +""" +from .base import Language +from .names import Name, NobleName + +__all__ = [Language, Name, NobleName] diff --git a/language/languages/halfling/base.py b/language/languages/halfling/base.py new file mode 100644 index 0000000..487f537 --- /dev/null +++ b/language/languages/halfling/base.py @@ -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, +) diff --git a/language/languages/halfling/names.py b/language/languages/halfling/names.py new file mode 100644 index 0000000..0287da0 --- /dev/null +++ b/language/languages/halfling/names.py @@ -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 diff --git a/language/languages/halfling/rules.py b/language/languages/halfling/rules.py new file mode 100644 index 0000000..09d965f --- /dev/null +++ b/language/languages/halfling/rules.py @@ -0,0 +1,7 @@ +import logging + +from language.rules import default_rules + +logger = logging.getLogger() + +rules = default_rules diff --git a/language/languages/infernal/README.md b/language/languages/infernal/README.md new file mode 100644 index 0000000..472fdf9 --- /dev/null +++ b/language/languages/infernal/README.md @@ -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 diff --git a/language/languages/infernal/__init__.py b/language/languages/infernal/__init__.py new file mode 100644 index 0000000..624861e --- /dev/null +++ b/language/languages/infernal/__init__.py @@ -0,0 +1,7 @@ +""" +Infernal +""" +from .base import Language +from .names import Name, NobleName + +__all__ = [Language, Name, NobleName] diff --git a/language/languages/infernal/base.py b/language/languages/infernal/base.py new file mode 100644 index 0000000..82bc309 --- /dev/null +++ b/language/languages/infernal/base.py @@ -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, +) diff --git a/language/languages/infernal/names.py b/language/languages/infernal/names.py new file mode 100644 index 0000000..0b76d68 --- /dev/null +++ b/language/languages/infernal/names.py @@ -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() diff --git a/language/languages/infernal/rules.py b/language/languages/infernal/rules.py new file mode 100644 index 0000000..9e8416e --- /dev/null +++ b/language/languages/infernal/rules.py @@ -0,0 +1,7 @@ +import logging + +from language.rules import default_rules + +logger = logging.getLogger("infernal-rules") + +rules = default_rules diff --git a/language/languages/lizardfolk/README.md b/language/languages/lizardfolk/README.md new file mode 100644 index 0000000..b37f766 --- /dev/null +++ b/language/languages/lizardfolk/README.md @@ -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 diff --git a/language/languages/lizardfolk/__init__.py b/language/languages/lizardfolk/__init__.py new file mode 100644 index 0000000..e8c9611 --- /dev/null +++ b/language/languages/lizardfolk/__init__.py @@ -0,0 +1,7 @@ +""" +Lizardfolk +""" +from .base import Language +from .names import Name, NobleName + +__all__ = [Language, Name, NobleName] diff --git a/language/languages/lizardfolk/base.py b/language/languages/lizardfolk/base.py new file mode 100644 index 0000000..d3b6a09 --- /dev/null +++ b/language/languages/lizardfolk/base.py @@ -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, +) diff --git a/language/languages/lizardfolk/names.py b/language/languages/lizardfolk/names.py new file mode 100644 index 0000000..55f79f9 --- /dev/null +++ b/language/languages/lizardfolk/names.py @@ -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 diff --git a/language/languages/orcish/README.md b/language/languages/orcish/README.md new file mode 100644 index 0000000..5531f84 --- /dev/null +++ b/language/languages/orcish/README.md @@ -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 diff --git a/language/languages/orcish/__init__.py b/language/languages/orcish/__init__.py new file mode 100644 index 0000000..436fba3 --- /dev/null +++ b/language/languages/orcish/__init__.py @@ -0,0 +1,7 @@ +""" +Orcish +""" +from .base import Language +from .names import Name, NobleName + +__all__ = [Language, Name, NobleName] diff --git a/language/languages/orcish/base.py b/language/languages/orcish/base.py new file mode 100644 index 0000000..a9a697b --- /dev/null +++ b/language/languages/orcish/base.py @@ -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, +) diff --git a/language/languages/orcish/names.py b/language/languages/orcish/names.py new file mode 100644 index 0000000..98d700f --- /dev/null +++ b/language/languages/orcish/names.py @@ -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 diff --git a/language/languages/orcish/rules.py b/language/languages/orcish/rules.py new file mode 100644 index 0000000..74989d1 --- /dev/null +++ b/language/languages/orcish/rules.py @@ -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) diff --git a/language/languages/undercommon/README.md b/language/languages/undercommon/README.md new file mode 100644 index 0000000..c2b112b --- /dev/null +++ b/language/languages/undercommon/README.md @@ -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 diff --git a/language/languages/undercommon/__init__.py b/language/languages/undercommon/__init__.py new file mode 100644 index 0000000..c47509c --- /dev/null +++ b/language/languages/undercommon/__init__.py @@ -0,0 +1,7 @@ +""" +Undercommon +""" +from .base import Language +from .names import Name, NobleName + +__all__ = [Language, Name, NobleName] diff --git a/language/languages/undercommon/base.py b/language/languages/undercommon/base.py new file mode 100644 index 0000000..53ffcb4 --- /dev/null +++ b/language/languages/undercommon/base.py @@ -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, +) diff --git a/language/languages/undercommon/names.py b/language/languages/undercommon/names.py new file mode 100644 index 0000000..04093f6 --- /dev/null +++ b/language/languages/undercommon/names.py @@ -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 diff --git a/language/languages/undercommon/rules.py b/language/languages/undercommon/rules.py new file mode 100644 index 0000000..a45f1b6 --- /dev/null +++ b/language/languages/undercommon/rules.py @@ -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, + } +) diff --git a/language/rules.py b/language/rules.py new file mode 100644 index 0000000..2fc18f0 --- /dev/null +++ b/language/rules.py @@ -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} diff --git a/language/types.py b/language/types.py new file mode 100644 index 0000000..2d74fe1 --- /dev/null +++ b/language/types.py @@ -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 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7084da3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,47 @@ +[tool.poetry] +name = "dnd-name-generator" +version = "1.0" +description = "Fantasy language generators for D&D" +authors = ["evilchili "] +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" +