initial import of legacy npc codebase

This commit is contained in:
evilchili
2022-08-03 00:19:13 -07:00
parent 7a77f847ed
commit db7aa06a80
29 changed files with 3314 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
from npc.languages import abyssal
from npc.languages import celestial
from npc.languages import common
from npc.languages import draconic
from npc.languages import dwarvish
from npc.languages import elven
from npc.languages import gnomish
from npc.languages import halfling
from npc.languages import infernal
from npc.languages import orcish
from npc.languages import undercommon
__ALL__ = [
'abyssal',
'base',
'celestial',
'common',
'draconic',
'dwarvish',
'elven',
'gnomish',
'halfling',
'infernal',
'orcish',
'undercommon',
]
+32
View File
@@ -0,0 +1,32 @@
from npc.languages.base import BaseLanguage
import re
class Abyssal(BaseLanguage):
vowels = ['a', 'e', 'i', 'o', 'u', 'î', 'ê', 'â', 'û', 'ô', 'ä', 'ö', 'ü', 'äu', 'ȧ', 'ė', '', 'ȯ', '']
consonants = ['c', 'g', 'j', 'k', 'p', 'ss', 't']
syllable_template = ('V', 'v', 'c', 'v')
_invalid_sequences = re.compile(
r'[' + ''.join(vowels) + ']{5}|' +
r'[' + ''.join(consonants) + ']{3}'
)
syllable_weights = [3, 2]
minimum_length = 2
def validate_sequence(self, sequence, total_syllables):
too_short = len(''.join(sequence)) < self.minimum_length
if too_short:
return False
t = ''.join(sequence)
if self._invalid_sequences.match(t):
self._logger.debug(f"Invalid sequence: {t}")
return False
return True
+194
View File
@@ -0,0 +1,194 @@
import random
import logging
from collections import namedtuple
grapheme = namedtuple('Grapheme', ['char', 'weight'])
class LanguageException(Exception):
"""
Thrown when language validators fail.
"""
class SyllableFactory:
def __init__(self, template, weights, prefixes, vowels, consonants, suffixes, affixes):
self.template = template
self.weights = weights
self.grapheme = {
'chars': {
'p': [x.char for x in prefixes],
'c': [x.char for x in consonants],
'v': [x.char for x in vowels],
's': [x.char for x in suffixes],
'a': [x.char for x in affixes]
},
'weights': {
'p': [x.weight for x in prefixes],
'c': [x.weight for x in consonants],
'v': [x.weight for x in vowels],
's': [x.weight for x in suffixes],
'a': [x.weight for x in affixes]
}
}
def _filtered_graphemes(self, key):
return [(k, v) for (k, v) in self.grapheme['chars'].items() if k in key]
def graphemes(self, key='apcvs'):
for _, chars in self._filtered_graphemes(key):
for char in chars:
yield char
def is_valid(self, chars, key='apcvs'):
for grapheme_type, _ in self._filtered_graphemes(key):
if chars in self.grapheme['chars'][grapheme_type]:
return True
return False
def get(self):
"""
Generate a single syllable
"""
syllable = ''
for t in self.template:
if t.islower() and random.random() < 0.5:
continue
if '|' in t:
t = random.choice(t.split('|'))
t = t.lower()
syllable = syllable + random.choices(self.grapheme['chars'][t], self.grapheme['weights'][t])[0]
return syllable
def __str__(self):
return self.get()
class WordFactory:
def __init__(self, language):
self.language = language
def random_syllable_count(self):
return 1 + random.choices(range(len(self.language.syllable.weights)), self.language.syllable.weights)[0]
def get(self):
total_syllables = self.random_syllable_count()
seq = []
while not self.language.validate_sequence(seq, total_syllables):
seq = [self.language.syllable.get()]
while len(seq) < total_syllables - 2:
seq.append(self.language.syllable.get())
if len(seq) < total_syllables:
seq.append(self.language.syllable.get())
return ''.join(seq)
def __str__(self):
return self.get()
class BaseLanguage:
"""
Words are created by combining syllables selected from random phonemes according to templates, each containing one
or more of the following grapheme indicators:
c - an optional consonant
C - a required consonant
v - an optional vowel
V - a required consonant
The simplest possible syllable consists of a single grapheme, and the simplest possible word a single syllable.
Words can also be generated from affixes; these are specified by the special template specifiers 'a'/'A'.
Examples:
('c', 'V') - a syllable consisting of exactly one vowel, possibly preceeded by a single consonant
('C', 'c', 'V', 'v') - a syllable consisting of one or two consonants followed by one or two vowels
('a', 'C', 'V') - a syllable consisting of an optional affix, a consonant and a vowel.
Word length is determined by the number of syllables, which is chosen at random using relative weights:
[2, 2, 1] - Names may contain one, two or three syllables, but are half as likely to contain three.
[0, 1] - Names must have exactly two syllables
"""
affixes = []
vowels = []
consonants = []
prefixes = vowels + consonants
suffixes = vowels + consonants
syllable_template = ('C', 'V')
syllable_weights = [1, 1]
minimum_length = 3
def __init__(self):
self._logger = logging.getLogger()
self.syllable = SyllableFactory(
template=self.syllable_template,
weights=self.syllable_weights,
prefixes=[grapheme(char=c, weight=1) for c in self.__class__.prefixes],
suffixes=[grapheme(char=c, weight=1) for c in self.__class__.suffixes],
vowels=[grapheme(char=c, weight=1) for c in self.__class__.vowels],
consonants=[grapheme(char=c, weight=1) for c in self.__class__.consonants],
affixes=[grapheme(char=c, weight=1) for c in self.__class__.affixes]
)
def _valid_syllable(self, syllable, text, key='apcvs', reverse=False):
length = 0
for seq in reverse(sorted(syllable.graphemes(key=key), key=len)):
length = len(seq)
substr = text[-1 * length:] if reverse else text[0:length]
if substr == seq:
return length
return False
def is_valid(self, text):
for part in text.lower().split(' '):
if part in self.affixes:
continue
if len(part) < self.minimum_length:
self._logger.debug(f"'{part}' too short; must be {self.minimum_length} characters.")
return False
first_offset = self._valid_syllable(self.syllable, text=part, key='p')
if first_offset is False:
self._logger.debug(f"'{part}' is not a valid syllable.")
return False
last_offset = self._valid_syllable(self.last_syllable, text=part, key='s', reverse=True)
if last_offset is False:
self._logger.debug(f"'{part}' is not a valid syllable.")
return False
last_offset = len(part) - last_offset
while first_offset < last_offset:
middle = part[first_offset:last_offset]
new_offset = self._valid_syllable(self.syllable, text=middle, key='cv')
if new_offset is False:
self._logger.debug(f"'{middle}' is not a valid middle sequence.")
return False
first_offset = first_offset + new_offset
return True
def validate_sequence(self, sequence, total_syllables):
return len(''.join(sequence)) > self.minimum_length
def word(self):
return WordFactory(language=self)
def place(self):
return self.word()
def person(self):
return (self.word(), self.word())
+32
View File
@@ -0,0 +1,32 @@
from npc.languages.base import BaseLanguage
import re
class Celestial(BaseLanguage):
vowels = ['a', 'e', 'i', 'o', 'u', 'î', 'ê', 'â', 'û', 'ô', 'ä', 'ö', 'ü', 'äu', 'ȧ', 'ė', '', 'ȯ', '']
consonants = ['b', 'sc', 'f', 'h', 'l', 'm', 'n', 'r', 's', 'v']
syllable_template = ('V', 'v', 'c', 'c', 'v', 'v')
_invalid_sequences = re.compile(
r'[' + ''.join(vowels) + ']{5}|' +
r'[' + ''.join(consonants) + ']{3}'
)
syllable_weights = [3, 2]
minimum_length = 5
def validate_sequence(self, sequence, total_syllables):
too_short = len(''.join(sequence)) < self.minimum_length
if too_short:
return False
t = ''.join(sequence)
if self._invalid_sequences.match(t):
self._logger.debug(f"Invalid sequence: {t}")
return False
return True
+94
View File
@@ -0,0 +1,94 @@
import re
import random
from npc.languages.base import BaseLanguage, WordFactory
class Common(BaseLanguage):
vowels = ['a', 'e', 'i', 'o', 'u']
consonants = [
'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'q', 'r', 's', 't',
'v', 'w', 'x', 'y', 'z'
]
_middle_clusters = re.compile(
r'bs|ct|ch|ck|dd|ff|gh|gs|ms|ns|ps|qu|rb|rd|rf|rk|rl|rm|rn|rp|rs|rt|ry' +
r'|sh|sk|ss|st|sy|th|tk|ts|tt|ty|ws|yd|yk|yl|ym|yn|yp|yr|ys|yt|yz|mcd' +
r'|[' + ''.join(vowels) + '][' + ''.join(consonants) + ']' +
r'|[' + ''.join(consonants) + '][' + ''.join(vowels) + ']' +
r'|[' + ''.join(vowels) + ']{1,2}'
)
_invalid_sequences = re.compile(
r'[' + ''.join(vowels) + ']{3}|' +
r'[' + ''.join(consonants) + ']{4}'
)
suffixes = [
'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',
]
prefixes = [s[::-1] for s in suffixes]
affixes = []
syllable_template = ('p', 'c|v', 's')
minimum_length = 1
def validate_sequence(self, sequence, total_syllables):
too_short = len(''.join(sequence)) < self.minimum_length
if too_short:
return False
t = ''.join(sequence)
if self._invalid_sequences.match(t):
self._logger.debug(f"Invalid sequence: {t}")
return False
for pos in range(len(t)):
seq = t[pos:pos+2]
if len(seq) != 2:
return True
if not self._middle_clusters.match(seq):
self._logger.debug(f"Invalid sequence: {seq}")
return False
class CommonSurname(Common):
syllable_template = ('P|C', 'v', 'S')
syllable_weights = [1]
word_suffixes = [
'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',
]
def word(self):
return str(WordFactory(self)) + random.choice(self.word_suffixes)
class CommonPerson(Common):
syllable_template = ('p', 'C', 'V', 's')
syllable_weights = [3, 1]
minimum_length = 2
def person(self):
return (WordFactory(language=self), CommonSurname().word())
+67
View File
@@ -0,0 +1,67 @@
from npc.languages.base import BaseLanguage, WordFactory
import random
import re
class Draconic(BaseLanguage):
vowels = ["a'", "aa", "ah", "e'", "ee", "ei", "ey", "i'", "ii", "ir", "o'", "u'", "uu"]
consonants = [
'd', 'f', 'g', 'h', 'j', 'k', 'l',
'n', 'r', 's', 't', 'v', 'x', 'y', 'z',
]
syllable_template = ('C', 'V')
_invalid_sequences = re.compile(
r'[' + ''.join(vowels) + ']{3}|' +
r'[' + ''.join(consonants) + ']{4}'
)
syllable_weights = [0, 0, 1, 2, 2, 1]
minimum_length = 3
def validate_sequence(self, sequence, total_syllables):
too_short = len(''.join(sequence)) < self.minimum_length
if too_short:
return False
t = ''.join(sequence)
if self._invalid_sequences.match(t):
self._logger.debug(f"Invalid sequence: {t}")
return False
return True
class Dragon(Draconic):
syllable_template = ('v', 'C', 'V')
syllable_weights = [0, 1, 2]
vowels = ['a', 'e', 'i', 'o', 'u']
last_vowels = vowels
last_consonants = ['th', 'x', 'ss', 'z']
minimum_length = 2
_invalid_sequences = re.compile(
r'[' + ''.join(last_vowels) + ']{2}|' +
r'[' + ''.join(Draconic.consonants) + ']{2}'
)
def names(self):
prefix = str(WordFactory(self))
suffix = ''
while not self.validate_sequence(suffix, 1):
suffix = ''.join([
random.choice(self.last_vowels),
random.choice(self.last_consonants),
random.choice(['us', 'ux', 'as', 'ax', 'is', 'ix', 'es', 'ex'])
])
return [prefix + suffix]
person = names
+42
View File
@@ -0,0 +1,42 @@
import random
from npc.languages.base import BaseLanguage
class Dwarvish(BaseLanguage):
consonants = [
'b', 'p', 'ph', 'd', 't', 'th', 'j', 'c', 'ch', 'g', 'k', 'kh', 'v', 'f', 'z', 's', 'zh', 'sh', 'hy', 'h', 'r',
'l', 'y', 'w', 'm', 'n'
]
vowels = [
'a', 'e', 'i', 'o', 'u', 'î', 'ê', 'â', 'û', 'ô'
]
affixes = []
first_consonants = consonants
first_vowels = vowels
first_affixes = affixes
last_vowels = vowels
last_consonants = consonants
last_affixes = affixes
syllable_template = ('C', 'V', 'c')
syllable_weights = [4, 1]
name_suffixes = ['son', 'sson', 'zhon', 'dottir', 'dothir', 'dottyr']
def person(self):
words = super().person()
suffix = random.choice(Dwarvish.name_suffixes)
return (str(words[0]), f"{words[1]}{suffix}")
def is_valid(self, text):
for suffix in self.name_suffixes:
if text.endswith(suffix):
text = text[0:len(suffix)]
break
return super().is_valid(text)
+166
View File
@@ -0,0 +1,166 @@
import random
import re
from npc.languages.base import BaseLanguage, WordFactory
class Elven(BaseLanguage):
"""
Phonetics for the Elven language in Telisar. Inspired by Tolkein's Quenya language, but with naming conventions
following Twirrim's conventions in-game.
"""
vowels = ['a', 'e', 'i', 'o', 'u']
consonants = ['b', 'c', 'd', 'f', 'g', 'h', 'k', 'l', 'm', 'n', 'p', 'r', 's', 't', 'v', 'w', 'y', 'z']
affixes = []
first_vowels = ['a', 'e', 'i', 'o', 'u', 'y']
first_consonants = ['c', 'g', 'l', 'm', 'n', 'r', 's', 't', 'v', 'z']
first_affixes = []
last_vowels = ['a', 'i', 'e']
last_consonants = ['t', 's', 'm', 'n', 'l', 'r', 'd', 'a', 'th']
last_affixes = []
syllable_template = ('c', 'v', 'c', 'V', 'C', 'v')
minimum_length = 4
_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'
]
_invalid_sequences = re.compile(
r'[' + ''.join(vowels) + ']{3}|' +
r'[' + ''.join(consonants) + ']{4}'
)
def validate_sequence(self, sequence, *args, **kwargs):
"""
Ensure the specified sequence of syllables results in valid letter combinations.
"""
too_short = len(''.join(sequence)) < self.minimum_length
if too_short:
return False
# the whole string must be checked against the invalid sequences pattern
chars = ''.join(sequence)
if self._invalid_sequences.match(chars):
self._logger.debug(f"Invalid sequence: {chars}")
return False
# Now step through the sequence, two letters at a time, and verify that
# all pairs of consonants are valid.
for offset in range(0, len(chars), 2):
seq = chars[offset:2]
if not seq:
break
if seq[0] in self.consonants and seq[1] in self.consonants:
if seq not in self._valid_consonant_sequences:
self._logger.debug(f"Invalid sequence: {seq}")
return False
return True
class ElvenPlaceName(Elven):
"""
Place names are a restricted subset of Elven; the initial syllables are constructed as normal, but place names
end in a sequence consisting of exactly one vowel and one consonant.
"""
syllable_template = ('v', 'C', 'v')
syllable_weights = [2, 1]
first_consonants = Elven.first_consonants + ['q']
minimum_length = 2
affixes = ['el']
def word(self):
prefix = str(WordFactory(self))
suffix = []
while not self.validate_sequence(suffix):
suffix = [
random.choice(self.last_vowels),
random.choice(self.last_consonants + ['ss']),
]
return prefix + ''.join(suffix)
def full_name(self):
return 'el '.join(self.names)
class HighElvenSurname(Elven):
"""
High Elven names follow the same naming conventions as more modern names, but ancient place names were longer, and
suffixes always followed a pattern of vowel, consonant, two vowels, and a final consonant, but the rules for
each are much more restrictive. In practice just a few suffixes are permitted: ieth, ies, ier, ien, iath, ias, iar,
ian, ioth, ios, ior, and ion.
"""
syllable_template = ('v', 'C', 'v')
syllable_weights = [1, 2, 2]
minimum_length = 2
def word(self):
prefix = str(WordFactory(self))
suffix = ''
while not self.validate_sequence(suffix):
suffix = ''.join([
random.choice(self.last_vowels),
random.choice(self.last_consonants + ['ss']),
random.choice([
'ie',
'ia',
'io',
]),
random.choice(['th', 's', 'r', 'n'])
])
return prefix + suffix
class ElvenPerson(Elven):
"""
A modern Elven name. Surnames follow the same convention as High Elven in including place names, though over time
the social function of denoting where renown was earned has been lost. An elf who names himself "am Uman", for
example, would be seen as either foolish or obnoxious, or both. Like "Johnny New York."
"""
syllable_template = ('c', 'V', 'C', 'v')
syllable_weights = [1, 2]
last_affixes = ['am', 'an', 'al', 'um']
def place(self):
return ElvenPlaceName().word()
def word(self):
return (
super().word(),
random.choice(self.last_affixes),
self.place()
)
person = word
class HighElvenPerson(ElvenPerson):
"""
Given names in High Elven and modern Elven follow the same conventions, but a High Elven surname is generally
chosen by the individual, to indicate "the place where renown is earned." So the High-Elven Elstuviar am
Vakaralithien implies a place or organization named Vakarlithien where the elf Elstuviar was first recognized by
their peers for worthy accompliments.
"""
syllable_weights = [2, 2, 2]
def word(self):
return (
super(Elven, self).word(),
random.choice(self.last_affixes),
HighElvenSurname().word()
)
person = word
+24
View File
@@ -0,0 +1,24 @@
from npc.languages.base import BaseLanguage
class Gnomish(BaseLanguage):
vowels = ['a', 'e', 'i', 'o', 'u', 'y']
consonants = ['b', 'd', 'f', 'g', 'h', 'j', 'l', 'm', 'n', 'p', 'r', 's', 't', 'v', 'w', 'z']
affixes = []
first_vowels = vowels
first_consonants = consonants
first_affixes = affixes
last_vowels = ['a', 'e', 'i', 'o', 'y']
last_consonants = consonants
last_affixes = affixes
syllable_template = ('C', 'V', 'v')
syllable_weights = [3, 1]
minimum_length = 1
def person(self):
return (self.word(), self.word())
+53
View File
@@ -0,0 +1,53 @@
from npc.languages.base import BaseLanguage
class Halfling(BaseLanguage):
vowels = ["a'", "e'", "i'" "o'", 'a', 'e', 'i', 'o', 'y']
consonants = ['b', 'd', 'f', 'g', 'h', 'j', 'l', 'm', 'n', 'p', 'r', 's', 't', 'v', 'w', 'z']
affixes = []
first_vowels = vowels
first_consonants = consonants
first_affixes = affixes
last_vowels = ['a', 'e', 'i', 'o', 'y']
last_consonants = consonants
last_affixes = affixes
syllable_template = ('c', 'V')
syllable_weights = [0, 1, 2, 3, 2, 1]
nicknames = [
'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'
]
def person(self):
return (self.word(), self.word(), self.word())
+108
View File
@@ -0,0 +1,108 @@
from npc.languages.base import BaseLanguage
import random
import re
class Infernal(BaseLanguage):
vowels = ['a', 'e', 'i', 'o', 'u']
consonants = [
'b', 'c', 'd', 'f', 'g', 'j', 'k', 'l', 'm',
'n', 'p', 'r', 's', 't', 'v', 'x', 'y', 'z',
"t'h", "t'j", "t'z", "x't", "x'z", "x'j"
]
syllable_template = ('C', 'V')
_invalid_sequences = re.compile(
r'[' + ''.join(vowels) + ']{3}|' +
r'[' + ''.join(consonants) + ']{4}'
)
syllable_weights = [3, 2]
minimum_length = 1
def validate_sequence(self, sequence, total_syllables):
too_short = len(''.join(sequence)) < self.minimum_length
if too_short:
return False
t = ''.join(sequence)
if self._invalid_sequences.match(t):
self._logger.debug(f"Invalid sequence: {t}")
return False
return True
class Tiefling(Infernal):
"""
Tiefling names are formed using an infernal root and a few common suffixes.
"""
nicknames = [
'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',
] + ['' for _ in range(50)]
def person(self):
suffix = random.choice([
'us',
'ius'
'to',
'tro'
'eus',
'a',
'an',
'is',
])
return [str(self.word()) + suffix]
class HighTiefling(Tiefling):
"""
"High" Tieflings revere their bloodlines and take their lineage as part of their name.
"""
nicknames = []
def person(self):
bloodline = random.choice([
'Asmodeus',
'Baalzebul',
'Rimmon',
'Dispater',
'Fierna',
'Glasya',
'Levistus',
'Mammon',
'Mephistopheles',
'Zariel',
])
return [bloodline] + super().person()
+57
View File
@@ -0,0 +1,57 @@
import re
from npc.languages.base import BaseLanguage
class Orcish(BaseLanguage):
vowels = ['a', 'e', 'i', 'o', 'u']
consonants = ['b', 'c', 'ch', 'd', 'f', 'h', 'k', 'm', 'n', 'p', 'r', 's', 'sh', 't', 'z']
affixes = []
first_vowels = vowels
first_consonants = consonants
first_affixes = affixes
last_vowels = vowels
last_consonants = consonants
last_affixes = affixes
syllable_template = ('C', 'c', 'V')
syllable_weights = [2, 4, 0.5]
_middle_clusters = re.compile(
r'\S?[' +
r'bd|bk|br|bs|' +
r'ch|ck|cp|cr|cs|ct|' +
r'db|dk|ds|' +
r'fr|ft|' +
r'kr|ks|kz|' +
r'ms|' +
r'ns|nt|nz|' +
r'ps|pt|' +
r'rk|rt|rz|' +
r'sc|sh|sk|sr|st|' +
r'tc|th|tr|ts|tz' +
r']\S?'
)
def validate_sequence(self, sequence, total_syllables):
too_short = len(''.join(sequence)) < self.minimum_length
if too_short:
return False
seq = ''.join(sequence[-2:])
if not self._middle_clusters.match(seq):
self._logger.debug(f"Invalid sequence: {sequence[-2:]}")
return False
return True
class OrcishPerson(Orcish):
pass
class HalfOrcPerson(Orcish):
syllable_template = ('C', 'V', 'c')
first_consonants = ['b', 'c', 'd', 'k', 'p', 't', 'z']
last_consonants = Orcish.consonants + ['sht', 'cht', 'zt', 'zch']
+122
View File
@@ -0,0 +1,122 @@
import random
import re
from npc.languages.base import BaseLanguage, WordFactory
class Undercommon(BaseLanguage):
vowels = ['a', 'e', 'i', 'o', 'u', 'a', 'e', 'i', 'o', 'u', 'ä', 'ö', 'ü', 'äu']
consonants = ['b', 'c', 'd', 'f', 'g', 'h', 'k', 'l', 'm', 'n', 'p', 'r', 's', 't', 'v', 'w', 'y', 'z']
affixes = []
first_vowels = ['a', 'e', 'i', 'o', 'u', 'y']
first_consonants = ['c', 'g', 'l', 'm', 'n', 'r', 's', 't', 'v', 'z']
first_affixes = []
last_vowels = ['a', 'i', 'e']
last_consonants = ['t', 's', 'm', 'n', 'l', 'r', 'd', 'a', 'th']
last_affixes = []
syllable_template = ('c', 'v', 'c', 'V', 'C', 'v')
minimum_length = 4
_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'
]
_invalid_sequences = re.compile(
r'[' + ''.join(vowels) + ']{3}|' +
r'[' + ''.join(consonants) + ']{4}'
)
def validate_sequence(self, sequence, *args, **kwargs):
"""
Ensure the specified sequence of syllables results in valid letter combinations.
"""
too_short = len(''.join(sequence)) < self.minimum_length
if too_short:
return False
# the whole string must be checked against the invalid sequences pattern
chars = ''.join(sequence)
if self._invalid_sequences.match(chars):
self._logger.debug(f"Invalid sequence: {chars}")
return False
# Now step through the sequence, two letters at a time, and verify that
# all pairs of consonants are valid.
for offset in range(0, len(chars), 2):
seq = chars[offset:2]
if not seq:
break
if seq[0] in self.consonants and seq[1] in self.consonants:
if seq not in self._valid_consonant_sequences:
self._logger.debug(f"Invalid sequence: {seq}")
return False
return True
class DrowPlaceName(Undercommon):
syllable_template = ('v', 'C', 'v')
syllable_weights = [2, 1]
first_consonants = Undercommon.first_consonants + ['q']
minimum_length = 2
affixes = ['el']
def word(self):
prefix = str(WordFactory(self))
suffix = []
while not self.validate_sequence(suffix):
suffix = [
random.choice(self.last_vowels),
random.choice(self.last_consonants + ['ss']),
]
return prefix + ''.join(suffix)
def full_name(self):
return 'el '.join(self.names)
class DrowSurname(Undercommon):
syllable_template = ('v', 'C', 'v')
syllable_weights = [1, 2, 2]
minimum_length = 2
def word(self):
prefix = str(WordFactory(self))
suffix = ''
while not self.validate_sequence(suffix):
suffix = ''.join([
random.choice(self.last_vowels),
random.choice(self.last_consonants + ['ss']),
random.choice([
'ie',
'ia',
'io',
]),
random.choice(['th', 's', 'r', 'n'])
])
return prefix + suffix
class DrowPerson(Undercommon):
syllable_template = ('c', 'V', 'C', 'v')
syllable_weights = [1, 2]
def place(self):
return DrowPlaceName().word()
def word(self):
return (
super().word(),
DrowSurname().word(),
)
person = word