377 lines
12 KiB
Python
377 lines
12 KiB
Python
import inspect
|
|
import random
|
|
from collections import defaultdict
|
|
from typing import Union
|
|
from random_sets.sets import WeightedSet, equal_weights
|
|
|
|
|
|
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 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
|
|
"""
|
|
|
|
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"]
|
|
|