initial import of legacy npc codebase
This commit is contained in:
parent
7a77f847ed
commit
db7aa06a80
0
npc/__init__.py
Normal file
0
npc/__init__.py
Normal file
75
npc/cli.py
Normal file
75
npc/cli.py
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
from npc.generator.base import generate_npc, npc_type
|
||||||
|
from npc import languages
|
||||||
|
from rich import print
|
||||||
|
|
||||||
|
import random
|
||||||
|
import typer
|
||||||
|
|
||||||
|
|
||||||
|
app = typer.Typer()
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def npc(ancestry=None, name=None, pronouns=None, title=None,
|
||||||
|
nickname=None, whereabouts="Unknown", STR=None, DEX=None, CON=None,
|
||||||
|
INT=None, WIS=None, CHA=None, randomize=False):
|
||||||
|
"""
|
||||||
|
Generate a basic NPC.
|
||||||
|
"""
|
||||||
|
print(generate_npc(
|
||||||
|
ancestry=ancestry,
|
||||||
|
names=name.split() if name else [],
|
||||||
|
pronouns=pronouns,
|
||||||
|
title=title,
|
||||||
|
nickname=nickname,
|
||||||
|
whereabouts=whereabouts,
|
||||||
|
STR=STR,
|
||||||
|
DEX=DEX,
|
||||||
|
CON=CON,
|
||||||
|
INT=INT,
|
||||||
|
WIS=WIS,
|
||||||
|
CHA=CHA,
|
||||||
|
randomize=randomize
|
||||||
|
).character_sheet)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def names(ancestry=None, count=1):
|
||||||
|
for _ in range(int(count)):
|
||||||
|
print(npc_type(ancestry)().full_name)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def text(language='common', words=50):
|
||||||
|
|
||||||
|
mod = getattr(languages, language, None)
|
||||||
|
if not mod:
|
||||||
|
print(f'Unsupported Language: {language}.')
|
||||||
|
return
|
||||||
|
lang_class = getattr(mod, language.capitalize(), None)
|
||||||
|
if not lang_class:
|
||||||
|
print(f'Unsupported Language: {language} in {mod}.')
|
||||||
|
return
|
||||||
|
lang = lang_class()
|
||||||
|
|
||||||
|
phrases = []
|
||||||
|
phrase = []
|
||||||
|
for word in [lang.word() for _ in range(int(words))]:
|
||||||
|
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
|
||||||
|
print(f"{paragraph}.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app()
|
0
npc/generator/__init__.py
Normal file
0
npc/generator/__init__.py
Normal file
377
npc/generator/base.py
Normal file
377
npc/generator/base.py
Normal file
|
@ -0,0 +1,377 @@
|
||||||
|
from importlib import import_module
|
||||||
|
from npc.generator import traits
|
||||||
|
import os
|
||||||
|
import glob
|
||||||
|
import random
|
||||||
|
import dice
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
|
||||||
|
_available_npc_types = {}
|
||||||
|
|
||||||
|
|
||||||
|
def a_or_an(s):
|
||||||
|
return 'an' if s[0] in 'aeiouh' else 'a'
|
||||||
|
|
||||||
|
|
||||||
|
class BaseNPC:
|
||||||
|
"""
|
||||||
|
The base class for NPCs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# define this on your subclass
|
||||||
|
language = None
|
||||||
|
|
||||||
|
_names = []
|
||||||
|
|
||||||
|
def __init__(self, names=[], title=None, pronouns=None, nickname=None, whereabouts='Unknown', randomize=False,
|
||||||
|
STR=None, DEX=None, CON=None, INT=None, WIS=None, CHA=None):
|
||||||
|
|
||||||
|
# identity
|
||||||
|
self._names = []
|
||||||
|
self._pronouns = pronouns
|
||||||
|
self._nickname = nickname
|
||||||
|
self._title = title
|
||||||
|
|
||||||
|
self._whereabouts = whereabouts
|
||||||
|
|
||||||
|
# appearance
|
||||||
|
self._eyes = None
|
||||||
|
self._hair = None
|
||||||
|
self._face = None
|
||||||
|
self._body = None
|
||||||
|
self._nose = None
|
||||||
|
self._lips = None
|
||||||
|
self._teeth = None
|
||||||
|
self._skin_tone = None
|
||||||
|
self._skin_color = None
|
||||||
|
self._facial_hair = None
|
||||||
|
self._facial_structure = None
|
||||||
|
self._eyebrows = None
|
||||||
|
self._age = None
|
||||||
|
self._voice = None
|
||||||
|
|
||||||
|
self._tail = False
|
||||||
|
self._horns = False
|
||||||
|
self._fangs = False
|
||||||
|
self._wings = False
|
||||||
|
|
||||||
|
# character
|
||||||
|
self._flaw = None
|
||||||
|
self._goal = None
|
||||||
|
self._personality = None
|
||||||
|
|
||||||
|
stats = (10, 10, 10, 10, 10, 10)
|
||||||
|
if randomize:
|
||||||
|
stats = self._roll_stats()
|
||||||
|
self.STR = STR if STR else stats[0]
|
||||||
|
self.DEX = DEX if DEX else stats[1]
|
||||||
|
self.CON = CON if DEX else stats[2]
|
||||||
|
self.INT = INT if DEX else stats[3]
|
||||||
|
self.WIS = WIS if DEX else stats[4]
|
||||||
|
self.CHA = CHA if DEX else stats[5]
|
||||||
|
|
||||||
|
self._HP = None
|
||||||
|
|
||||||
|
def _roll_stats(self):
|
||||||
|
stats = [15, 14, 13, 12, 10, 8]
|
||||||
|
random.shuffle(stats)
|
||||||
|
r = random.random()
|
||||||
|
if r < 0.3:
|
||||||
|
i = random.choice(range(len(stats)))
|
||||||
|
stats[i] += (random.choice([-1, 1]) * random.randint(1, 3))
|
||||||
|
return stats
|
||||||
|
|
||||||
|
@property
|
||||||
|
def HP(self):
|
||||||
|
if not self._HP:
|
||||||
|
self._HP = str(sum(dice.roll('2d8')) + 2) + ' (2d8+2)'
|
||||||
|
return self._HP
|
||||||
|
|
||||||
|
@property
|
||||||
|
def names(self):
|
||||||
|
if not self._names:
|
||||||
|
self._names = [str(x) for x in self.language.person()]
|
||||||
|
return self._names
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self):
|
||||||
|
name = ' '.join([n.capitalize() for n in self.names])
|
||||||
|
if self.title:
|
||||||
|
name = self.title.capitalize() + ' ' + name
|
||||||
|
if self.nickname:
|
||||||
|
name = f'{name} "{self.nickname}"'
|
||||||
|
return name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pronouns(self):
|
||||||
|
if not self._pronouns:
|
||||||
|
self._pronouns = random.choice([
|
||||||
|
'he/him',
|
||||||
|
'she/her',
|
||||||
|
'they/they',
|
||||||
|
])
|
||||||
|
return self._pronouns
|
||||||
|
|
||||||
|
@property
|
||||||
|
def title(self):
|
||||||
|
return self._title
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nickname(self):
|
||||||
|
if self._nickname is None and hasattr(self.language, 'nicknames'):
|
||||||
|
try:
|
||||||
|
self._nickname = random.choice(self.language.nicknames).capitalize()
|
||||||
|
except IndexError:
|
||||||
|
self._nickname = False
|
||||||
|
return self._nickname
|
||||||
|
|
||||||
|
@property
|
||||||
|
def whereabouts(self):
|
||||||
|
return self._whereabouts
|
||||||
|
|
||||||
|
@property
|
||||||
|
def flaw(self):
|
||||||
|
if self._flaw is None:
|
||||||
|
self._flaw = random.choice(traits.flaws)
|
||||||
|
return self._flaw
|
||||||
|
|
||||||
|
@property
|
||||||
|
def goal(self):
|
||||||
|
if self._goal is None:
|
||||||
|
self._goal = random.choice(traits.goals)
|
||||||
|
return self._goal
|
||||||
|
|
||||||
|
@property
|
||||||
|
def personality(self):
|
||||||
|
if self._personality is None:
|
||||||
|
self._personality = ', '.join([
|
||||||
|
random.choice(traits.personality),
|
||||||
|
random.choice(traits.personality),
|
||||||
|
random.choice(traits.personality),
|
||||||
|
])
|
||||||
|
return self._personality
|
||||||
|
|
||||||
|
@property
|
||||||
|
def eyes(self):
|
||||||
|
if self._eyes is None:
|
||||||
|
self._eyes = ', '.join([random.choice(traits.eye_shape), random.choice(traits.eye_color)])
|
||||||
|
return self._eyes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def skin_color(self):
|
||||||
|
if self._skin_color is None:
|
||||||
|
self._skin_color = random.choice(traits.skin_color)
|
||||||
|
return self._skin_color
|
||||||
|
|
||||||
|
@property
|
||||||
|
def skin_tone(self):
|
||||||
|
if self._skin_tone is None:
|
||||||
|
self._skin_tone = random.choice(traits.skin_tone)
|
||||||
|
return self._skin_tone
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hair(self):
|
||||||
|
if self._hair is None:
|
||||||
|
self._hair = ' '.join([random.choice(traits.hair_style), random.choice(traits.hair_color)])
|
||||||
|
return self._hair
|
||||||
|
|
||||||
|
@property
|
||||||
|
def face(self):
|
||||||
|
if not self._face:
|
||||||
|
self._face = random.choice(traits.face)
|
||||||
|
return self._face
|
||||||
|
|
||||||
|
@property
|
||||||
|
def facial_structure(self):
|
||||||
|
if self._facial_structure is None:
|
||||||
|
self._facial_structure = random.choice(traits.facial_structure)
|
||||||
|
return self._facial_structure
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lips(self):
|
||||||
|
if self._lips is None:
|
||||||
|
self._lips = random.choice(traits.lips)
|
||||||
|
return self._lips
|
||||||
|
|
||||||
|
@property
|
||||||
|
def teeth(self):
|
||||||
|
if self._teeth is None:
|
||||||
|
self._teeth = random.choice(traits.teeth)
|
||||||
|
return self._teeth
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nose(self):
|
||||||
|
if self._nose is None:
|
||||||
|
self._nose = random.choice(traits.nose)
|
||||||
|
return self._nose
|
||||||
|
|
||||||
|
@property
|
||||||
|
def eyebrows(self):
|
||||||
|
if self._eyebrows is None:
|
||||||
|
self._eyebrows = random.choice(traits.eyebrows)
|
||||||
|
return self._eyebrows
|
||||||
|
|
||||||
|
@property
|
||||||
|
def facial_hair(self):
|
||||||
|
if self._facial_hair is None:
|
||||||
|
self._facial_hair = random.choice(traits.facial_hair)
|
||||||
|
return self._facial_hair
|
||||||
|
|
||||||
|
@property
|
||||||
|
def body(self):
|
||||||
|
if self._body is None:
|
||||||
|
self._body = random.choice(traits.body)
|
||||||
|
return self._body
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tail(self):
|
||||||
|
if self._tail is None:
|
||||||
|
self._tail = random.choice(traits.tail)
|
||||||
|
return self._tail
|
||||||
|
|
||||||
|
@property
|
||||||
|
def horns(self):
|
||||||
|
if self._horns is None:
|
||||||
|
self._horns = random.choice(traits.horns)
|
||||||
|
return self._horns
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wings(self):
|
||||||
|
if self._wings is None:
|
||||||
|
self._wings = random.choice(traits.wings)
|
||||||
|
return self._wings
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fangs(self):
|
||||||
|
if self._fangs is None:
|
||||||
|
self._fangs = random.choice(traits.fangs)
|
||||||
|
return self._fangs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def age(self):
|
||||||
|
if not self._age:
|
||||||
|
self._age = random.choice(traits.age)
|
||||||
|
return self._age
|
||||||
|
|
||||||
|
@property
|
||||||
|
def voice(self):
|
||||||
|
if not self._voice:
|
||||||
|
self._voice = random.choice(traits.voice)
|
||||||
|
return self._voice
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self):
|
||||||
|
desc = (
|
||||||
|
f"{self.full_name} ({self.pronouns}) is {a_or_an(self.age)} {self.age}, {self.body} "
|
||||||
|
f"{self.ancestry.lower()} with {self.hair} hair, {self.eyes} eyes and {self.skin_color} skin."
|
||||||
|
)
|
||||||
|
trait = None
|
||||||
|
while not trait:
|
||||||
|
trait = random.choice([
|
||||||
|
f'{self.eyebrows} eyebrows' if self.eyebrows else None,
|
||||||
|
self.facial_hair if self.facial_hair else None,
|
||||||
|
f'a {self.nose} nose' if self.nose else None,
|
||||||
|
f'{self.lips} lips' if self.lips else None,
|
||||||
|
f'{self.teeth} teeth' if self.teeth else None,
|
||||||
|
self.facial_structure if self.facial_structure else None,
|
||||||
|
])
|
||||||
|
desc = desc + ' ' + f"Their face is {self.face}, with {trait}."
|
||||||
|
if self.tail:
|
||||||
|
desc = desc + f" Their tail is {self.tail}."
|
||||||
|
if self.horns:
|
||||||
|
desc = desc + f" Their horns are {self.horns}."
|
||||||
|
return desc
|
||||||
|
|
||||||
|
@property
|
||||||
|
def character_sheet(self):
|
||||||
|
desc = '\n'.join(textwrap.wrap(self.description, width=120))
|
||||||
|
return f"""\
|
||||||
|
|
||||||
|
{desc}
|
||||||
|
|
||||||
|
Physical Traits:
|
||||||
|
|
||||||
|
Face: {self.face}, {self.eyebrows} eyebrows, {self.nose} nose, {self.lips} lips,
|
||||||
|
{self.teeth} teeth, {self.facial_hair}
|
||||||
|
Eyes: {self.eyes}
|
||||||
|
Skin: {self.skin_tone}, {self.skin_color}
|
||||||
|
Hair: {self.hair}
|
||||||
|
Body: {self.body}
|
||||||
|
Tail: {self.tail}
|
||||||
|
Horns: {self.horns}
|
||||||
|
Fangs: {self.fangs}
|
||||||
|
Wings: {self.wings}
|
||||||
|
Voice: {self.voice}
|
||||||
|
|
||||||
|
Stats:
|
||||||
|
AC 10
|
||||||
|
HP {self.HP}
|
||||||
|
STR {self.STR}
|
||||||
|
DEX {self.DEX}
|
||||||
|
CON {self.CON}
|
||||||
|
INT {self.INT}
|
||||||
|
WIS {self.WIS}
|
||||||
|
CHA {self.CHA}
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
Personality: {self.personality}
|
||||||
|
Flaw: {self.flaw}
|
||||||
|
Goal: {self.goal}
|
||||||
|
|
||||||
|
Whereabouts: {self.whereabouts}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{self.full_name}"
|
||||||
|
|
||||||
|
|
||||||
|
def available_npc_types():
|
||||||
|
"""
|
||||||
|
Load all available NPC submodules and return a dictionary keyed by module name.
|
||||||
|
"""
|
||||||
|
if not _available_npc_types:
|
||||||
|
for filename in glob.glob(os.path.join(os.path.dirname(os.path.abspath(__file__)), '*.py')):
|
||||||
|
module_name = os.path.basename(filename)[:-3]
|
||||||
|
if module_name not in ['base', '__init__', 'traits']:
|
||||||
|
_available_npc_types[module_name] = import_module(f'npc.generator.{module_name}').NPC
|
||||||
|
return _available_npc_types
|
||||||
|
|
||||||
|
|
||||||
|
def npc_type(ancestry=None):
|
||||||
|
"""
|
||||||
|
Return the NPC class for the specified ancestry, or a random one.
|
||||||
|
"""
|
||||||
|
if not ancestry:
|
||||||
|
non_humans = [x for x in available_npc_types() if x != 'human']
|
||||||
|
if random.random() <= 0.7:
|
||||||
|
ancestry = 'human'
|
||||||
|
else:
|
||||||
|
ancestry = random.choice(non_humans)
|
||||||
|
return available_npc_types()[ancestry]
|
||||||
|
|
||||||
|
|
||||||
|
def generate_npc(ancestry=None, names=[], pronouns=None, title=None, nickname=None, whereabouts="Unknown",
|
||||||
|
STR=0, DEX=0, CON=0, INT=0, WIS=0, CHA=0, randomize=False):
|
||||||
|
"""
|
||||||
|
Return a randomized NPC. Any supplied keyword parameters will override the generated values.
|
||||||
|
|
||||||
|
By default, NPC stats are all 10 (+0). If randomize is True, the NPC will be given random stats from the standard
|
||||||
|
distribution, but overrides will still take precedence.
|
||||||
|
"""
|
||||||
|
return npc_type(ancestry)(
|
||||||
|
names=names,
|
||||||
|
pronouns=pronouns,
|
||||||
|
title=title,
|
||||||
|
nickname=nickname,
|
||||||
|
whereabouts=whereabouts,
|
||||||
|
STR=STR,
|
||||||
|
DEX=DEX,
|
||||||
|
CON=CON,
|
||||||
|
INT=INT,
|
||||||
|
WIS=WIS,
|
||||||
|
CHA=CHA,
|
||||||
|
randomize=randomize
|
||||||
|
)
|
101
npc/generator/dragon.py
Normal file
101
npc/generator/dragon.py
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
from npc.languages import draconic
|
||||||
|
from npc.generator.base import BaseNPC, a_or_an
|
||||||
|
from npc.generator import traits
|
||||||
|
import textwrap
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
class NPC(BaseNPC):
|
||||||
|
|
||||||
|
ancestry = 'Dragon'
|
||||||
|
language = draconic.Dragon()
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self._tail = None
|
||||||
|
self._horns = None
|
||||||
|
self._fangs = None
|
||||||
|
self._wings = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nickname(self):
|
||||||
|
if not self._nickname:
|
||||||
|
self._nickname = "the " + random.choice(traits.personality)
|
||||||
|
return self._nickname
|
||||||
|
|
||||||
|
@property
|
||||||
|
def age(self):
|
||||||
|
if not self._age:
|
||||||
|
self._age = random.choice([
|
||||||
|
'wyrmling',
|
||||||
|
'young',
|
||||||
|
'adult',
|
||||||
|
'ancient',
|
||||||
|
])
|
||||||
|
return self._age
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pronouns(self):
|
||||||
|
if not self._pronouns:
|
||||||
|
self._pronouns = 'they/they'
|
||||||
|
return self._pronouns
|
||||||
|
|
||||||
|
@property
|
||||||
|
def skin_color(self):
|
||||||
|
if not self._skin_color:
|
||||||
|
self._skin_color = random.choice([
|
||||||
|
'red',
|
||||||
|
'white',
|
||||||
|
'green',
|
||||||
|
'black',
|
||||||
|
'blue',
|
||||||
|
'brass',
|
||||||
|
'bronze',
|
||||||
|
'copper',
|
||||||
|
'silver',
|
||||||
|
'gold',
|
||||||
|
])
|
||||||
|
return self._skin_color
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self):
|
||||||
|
trait = random.choice([
|
||||||
|
f'{self.eyes} eyes',
|
||||||
|
f'{self.tail} tail',
|
||||||
|
f'{self.eyebrows} eyebrows',
|
||||||
|
f'{self.teeth} fangs',
|
||||||
|
self.facial_structure,
|
||||||
|
])
|
||||||
|
return (
|
||||||
|
f"{self.full_name} ({self.pronouns}) is {a_or_an(self.age)} {self.age} {self.skin_color} "
|
||||||
|
f"{self.ancestry.lower()} with {a_or_an(self.nose)} {self.nose} snout, {self.body} body and {trait}."
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def character_sheet(self):
|
||||||
|
desc = '\n'.join(textwrap.wrap(self.description, width=120))
|
||||||
|
return f"""\
|
||||||
|
|
||||||
|
{desc}
|
||||||
|
|
||||||
|
Physical Traits:
|
||||||
|
|
||||||
|
Face: {self.face}, {self.eyebrows} eyebrows, {self.nose} nose, {self.lips} lips,
|
||||||
|
{self.teeth} teeth, {self.facial_hair}
|
||||||
|
Eyes: {self.eyes}
|
||||||
|
Skin: {self.skin_tone}, {self.skin_color}
|
||||||
|
Hair: {self.hair}
|
||||||
|
Body: {self.body}
|
||||||
|
Tail: {self.tail}
|
||||||
|
Voice: {self.voice}
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
Personality: {self.personality}
|
||||||
|
Flaw: {self.flaw}
|
||||||
|
Goal: {self.goal}
|
||||||
|
|
||||||
|
Whereabouts: {self.whereabouts}
|
||||||
|
|
||||||
|
"""
|
15
npc/generator/drow.py
Normal file
15
npc/generator/drow.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
from npc.languages import undercommon
|
||||||
|
from npc.generator.base import BaseNPC
|
||||||
|
|
||||||
|
|
||||||
|
class NPC(BaseNPC):
|
||||||
|
|
||||||
|
ancestry = 'Drow'
|
||||||
|
language = undercommon.DrowPerson()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self):
|
||||||
|
return ' '.join([
|
||||||
|
str(self.names[0]).capitalize(),
|
||||||
|
str(self.names[1]).capitalize()
|
||||||
|
])
|
12
npc/generator/dwarf.py
Normal file
12
npc/generator/dwarf.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from npc.languages import dwarvish
|
||||||
|
from npc.generator.base import BaseNPC
|
||||||
|
|
||||||
|
|
||||||
|
class NPC(BaseNPC):
|
||||||
|
|
||||||
|
ancestry = 'Dwarf'
|
||||||
|
language = dwarvish.Dwarvish()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self):
|
||||||
|
return ' '.join([str(x).capitalize() for x in self.language.person()])
|
16
npc/generator/elf.py
Normal file
16
npc/generator/elf.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from npc.languages import elven
|
||||||
|
from npc.generator.base import BaseNPC
|
||||||
|
|
||||||
|
|
||||||
|
class NPC(BaseNPC):
|
||||||
|
|
||||||
|
ancestry = 'Elf'
|
||||||
|
language = elven.ElvenPerson()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self):
|
||||||
|
return ' '.join([
|
||||||
|
str(self.names[0]).capitalize(),
|
||||||
|
str(self.names[1]).lower(),
|
||||||
|
str(self.names[2]).capitalize()
|
||||||
|
])
|
10
npc/generator/halfling.py
Normal file
10
npc/generator/halfling.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import random
|
||||||
|
|
||||||
|
from npc.languages import halfling
|
||||||
|
from npc.generator.base import BaseNPC
|
||||||
|
|
||||||
|
|
||||||
|
class NPC(BaseNPC):
|
||||||
|
|
||||||
|
ancestry = 'Halfling'
|
||||||
|
language = halfling.Halfling()
|
12
npc/generator/halforc.py
Normal file
12
npc/generator/halforc.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from npc.languages import orcish
|
||||||
|
from npc.generator.base import BaseNPC
|
||||||
|
|
||||||
|
|
||||||
|
class NPC(BaseNPC):
|
||||||
|
|
||||||
|
ancestry = 'Half-Orc'
|
||||||
|
language = orcish.HalfOrcPerson()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self):
|
||||||
|
return ' '.join([str(x).capitalize() for x in self.language.person()])
|
16
npc/generator/highelf.py
Normal file
16
npc/generator/highelf.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from npc.languages import elven
|
||||||
|
from npc.generator.base import BaseNPC
|
||||||
|
|
||||||
|
|
||||||
|
class NPC(BaseNPC):
|
||||||
|
|
||||||
|
ancestry = 'Elf'
|
||||||
|
language = elven.HighElvenPerson()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self):
|
||||||
|
return ' '.join([
|
||||||
|
str(self.names[0]).capitalize(),
|
||||||
|
str(self.names[1]).lower(),
|
||||||
|
str(self.names[2]).capitalize()
|
||||||
|
])
|
6
npc/generator/hightiefling.py
Normal file
6
npc/generator/hightiefling.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from npc.languages import infernal
|
||||||
|
from npc.generator import tiefling
|
||||||
|
|
||||||
|
|
||||||
|
class NPC(tiefling.NPC):
|
||||||
|
language = infernal.HighTiefling()
|
12
npc/generator/human.py
Normal file
12
npc/generator/human.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from npc.languages import common
|
||||||
|
from npc.generator.base import BaseNPC
|
||||||
|
|
||||||
|
|
||||||
|
class NPC(BaseNPC):
|
||||||
|
|
||||||
|
ancestry = 'Human'
|
||||||
|
language = common.CommonPerson()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self):
|
||||||
|
return ' '.join([str(x).capitalize() for x in self.language.person()])
|
39
npc/generator/tiefling.py
Normal file
39
npc/generator/tiefling.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
from npc.languages import infernal
|
||||||
|
from npc.generator.base import BaseNPC
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
class NPC(BaseNPC):
|
||||||
|
ancestry = 'Tiefling'
|
||||||
|
language = infernal.Tiefling()
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._tail = None
|
||||||
|
self._horns = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def skin_color(self):
|
||||||
|
if not self._skin_color:
|
||||||
|
self._skin_color = random.choice([
|
||||||
|
'reddish',
|
||||||
|
'white',
|
||||||
|
'green',
|
||||||
|
'black',
|
||||||
|
'blue',
|
||||||
|
'brassy',
|
||||||
|
'bronze',
|
||||||
|
'coppery',
|
||||||
|
'silvery',
|
||||||
|
'gold',
|
||||||
|
])
|
||||||
|
return self._skin_color
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self):
|
||||||
|
name = ' '.join([n.capitalize() for n in self.names])
|
||||||
|
if self.title:
|
||||||
|
name = self.title.capitalize() + ' ' + name
|
||||||
|
if self.nickname:
|
||||||
|
name = name + ' ' + self.nickname.capitalize()
|
||||||
|
return name
|
1580
npc/generator/traits.py
Normal file
1580
npc/generator/traits.py
Normal file
File diff suppressed because it is too large
Load Diff
27
npc/languages/__init__.py
Normal file
27
npc/languages/__init__.py
Normal 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
npc/languages/abyssal.py
Normal file
32
npc/languages/abyssal.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
from npc.languages.base import BaseLanguage
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class Abyssal(BaseLanguage):
|
||||||
|
|
||||||
|
vowels = ['a', 'e', 'i', 'o', 'u', 'î', 'ê', 'â', 'û', 'ô', 'ä', 'ö', 'ü', 'ä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
npc/languages/base.py
Normal file
194
npc/languages/base.py
Normal 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
npc/languages/celestial.py
Normal file
32
npc/languages/celestial.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
from npc.languages.base import BaseLanguage
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class Celestial(BaseLanguage):
|
||||||
|
|
||||||
|
vowels = ['a', 'e', 'i', 'o', 'u', 'î', 'ê', 'â', 'û', 'ô', 'ä', 'ö', 'ü', 'ä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
npc/languages/common.py
Normal file
94
npc/languages/common.py
Normal 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
npc/languages/draconic.py
Normal file
67
npc/languages/draconic.py
Normal 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
npc/languages/dwarvish.py
Normal file
42
npc/languages/dwarvish.py
Normal 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
npc/languages/elven.py
Normal file
166
npc/languages/elven.py
Normal 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
npc/languages/gnomish.py
Normal file
24
npc/languages/gnomish.py
Normal 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
npc/languages/halfling.py
Normal file
53
npc/languages/halfling.py
Normal 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
npc/languages/infernal.py
Normal file
108
npc/languages/infernal.py
Normal 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
npc/languages/orcish.py
Normal file
57
npc/languages/orcish.py
Normal 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
npc/languages/undercommon.py
Normal file
122
npc/languages/undercommon.py
Normal 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
|
25
pyproject.toml
Normal file
25
pyproject.toml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
[tool.poetry]
|
||||||
|
name = "dnd_npcs"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "NPC tools for the telisar homebrew campaign setting"
|
||||||
|
authors = ["evilchili <evilchili@gmail.com>"]
|
||||||
|
license = "The Unlicense"
|
||||||
|
packages = [
|
||||||
|
{ include = 'npc' }
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.10"
|
||||||
|
typer = "latest"
|
||||||
|
rich = "latest"
|
||||||
|
dice = "latest"
|
||||||
|
|
||||||
|
[tool.poetry.dev-dependencies]
|
||||||
|
pytest = "latest"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.poetry.scripts]
|
||||||
|
npc = "npc.cli:app"
|
Loading…
Reference in New Issue
Block a user