add validation, update docs

This commit is contained in:
InspectorCaracal 2022-07-27 12:02:50 -06:00
parent 874c564db5
commit 9f4de7bd1c
2 changed files with 99 additions and 139 deletions

View file

@ -12,6 +12,10 @@ Fantasy names are generated from basic phonetic rules, using CVC syllable syntax
Both real-world and fantasy name generation can be extended to include additional
information via your game's `settings.py`
## Installation
This is a stand-alone utility. Just import this module (`from evennia.contrib.utils import name_generator`) and use its functions wherever you like.
## Usage
Import the module where you need it with the following:
@ -20,8 +24,7 @@ from evennia.contrib.utils.name_generator import namegen
```
By default, all of the functions will return a string with one generated name.
If you specify more than one, or pass `return_list=True` as a keyword argument,
the returned value will be a list of strings.
If you specify more than one, or pass `return_list=True` as a keyword argument, the returned value will be a list of strings.
The module is especially useful for naming newly-created NPCs, like so:
```py
@ -29,6 +32,37 @@ npc_name = namegen.full_name()
npc_obj = create_object(key=npc_name, typeclass="typeclasses.characters.NPC")
```
## Available Settings
These settings can all be defined in your game's `server/conf/settings.py` file.
- `NAMEGEN_FIRST_NAMES` adds a new list of first (personal) names.
- `NAMEGEN_LAST_NAMES` adds a new list of last (family) names.
- `NAMEGEN_REPLACE_LISTS` - set to `True` if you want to use only the names defined in your settings.
- `NAMEGEN_FANTASY_RULES` lets you add new phonetic rules for generating entirely made-up names. See the section "Custom Fantasy Name style rules" for details on how this should look.
Examples:
```py
NAMEGEN_FIRST_NAMES = [
("Evennia", 'mf'),
("Green Tea", 'f'),
]
NAMEGEN_LAST_NAMES = [ "Beeblebrox", "Son of Odin" ]
NAMEGEN_FANTASY_RULES = {
"example_style": {
"syllable": "(C)VC",
"consonants": [ 'z','z','ph','sh','r','n' ],
"start": ['m'],
"end": ['x','n'],
"vowels": [ "e","e","e","a","i","i","u","o", ],
"length": (2,4),
}
}
```
## Generating Real Names
The contrib offers three functions for generating random real-world names:
@ -84,8 +118,7 @@ NAMEGEN_FIRST_NAMES = [
NAMEGEN_LAST_NAMES = [ "Beeblebrox", "Son of Odin" ]
```
If you want to replace all of the built-in name lists with your own, set
`NAMEGEN_REPLACE_LISTS = True`
Set `NAMEGEN_REPLACE_LISTS = True` if you want your custom lists above to entirely replace the built-in lists rather than extend them.
## Generating Fantasy Names
@ -123,19 +156,23 @@ NAMEGEN_FANTASY_RULES = {
}
```
Then you could generate names following that ruleset with
`namegen.fantasy_name(style="example_style")`.
Then you could generate names following that ruleset with `namegen.fantasy_name(style="example_style")`.
The keys `syllable`, `consonants`, `vowels`, and `length` must be present, and `length` must be the minimum and maximum syllable counts. `start` and `end` are optional.
#### syllable
The "syllable" field defines the structure of each syllable. C is consonant, V is vowel,
and parentheses mean it's optional. So, the example "(C)VC" means that every syllable
and parentheses mean it's optional. So, the example `(C)VC` means that every syllable
will always have a vowel followed by a consonant, and will *sometimes* have another
consonant at the beginning.
consonant at the beginning. e.g. `en`, `bak`
*Note:* While it's not standard, the contrib lets you nest parentheses, with each layer
being less likely to show up. Additionally, any other characters put into the syllable
structure - e.g. an apostrophe - will be read and inserted as written. Check out the
"alien" style rules in the module for an example of both.
structure - e.g. an apostrophe - will be read and inserted as written. The
"alien" style rules in the module gives an example of both: the syllable structure is `C(C(V))(')(C)`
which results in syllables such as `khq`, `xho'q`, and `q'` with a much lower frequency of vowels than
`C(C)(V)(')(C)` would have given.
#### consonants
A simple list of consonant phonemes that can be chosen from. Multi-character strings are

View file

@ -1,5 +1,5 @@
"""
# Random Name Generator
Random Name Generator
Contribution by InspectorCaracal (2022)
@ -13,157 +13,57 @@ Fantasy names are generated from basic phonetic rules, using CVC syllable syntax
Both real-world and fantasy name generation can be extended to include additional
information via your game's `settings.py`
## Usage
Import the module where you need it with the following:
```py
from evennia.contrib.utils.name_generator import namegen
```
Available Methods:
By default, all of the functions will return a string with one generated name.
If you specify more than one, or pass `return_list=True` as a keyword argument,
the returned value will be a list of strings.
first_name - Selects a random a first (personal) name from the name lists.
last_name - Selects a random last (family) name from the name lists.
full_name - Generates a randomized full name, optionally including middle names, by selecting first/last names from the name lists.
fantasy_name - Generates a completely new made-up name based on phonetic rules.
The module is especially useful for naming newly-created NPCs, like so:
```py
npc_name = namegen.full_name()
npc_obj = create_object(key=npc_name, typeclass="typeclasses.characters.NPC")
```
Method examples:
## Generating Real Names
The contrib offers three functions for generating random real-world names:
`first_name()`, `family_name()`, and `full_name()`. If you want more than one name
generated at once, you can use the `num` keyword argument to specify how many.
Example:
```
>>> namegen.first_name(num=5)
['Genesis', 'Tali', 'Budur', 'Dominykas', 'Kamau']
```
The `first_name` function also takes a `gender` keyword argument to filter names
by gender association. 'f' for feminine, 'm' for masculine, 'mf' for feminine
_and_ masculine, or the default `None` to match any gendering.
The `full_name` function also takes the `gender` keyword, as well as `parts` which
defines how many names make up the full name. The minimum is two: a first name and
a last name. You can also generate names with the family name first by setting
the keyword arg `surname_first` to `True`
Example:
```
>>> namegen.full_name()
'Keeva Bernat'
>>> namegen.full_name(parts=4)
'Suzu Shabnam Kafka Baier'
>>> namegen.full_name(parts=3, surname_first=True)
'Ó Muircheartach Torunn Dyson'
>>> namegen.full_name(gender='f')
'Wikolia Ó Deasmhumhnaigh'
```
### Adding your own names
>>> namegen.fantasy_name(num=3, style="fluid")
['Aewalisash', 'Ayi', 'Iaa']
You can add additional names with the settings `NAMEGEN_FIRST_NAMES` and
`NAMEGEN_LAST_NAMES`
`NAMEGEN_FIRST_NAMES` should be a list of tuples, where the first value is the name
and then second value is the gender flag - 'm' for masculine-only, 'f' for feminine-
only, and 'mf' for either one.
Available Settings (define these in your `settings.py`)
`NAMEGEN_LAST_NAMES` should be a list of strings, where each item is an available
surname.
NAMEGEN_FIRST_NAMES - Option to add a new list of first (personal) names.
NAMEGEN_LAST_NAMES - Option to add a new list of last (family) names.
NAMEGEN_REPLACE_LISTS - Set to True if you want to use ONLY your name lists and not the ones that come with the contrib.
NAMEGEN_FANTASY_RULES - Option to add new fantasy-name style rules.
Must be a dictionary that includes "syllable", "consonants", "vowels", and "length" - see the example.
"start" and "end" keys are optional.
Settings examples:
Examples:
```py
NAMEGEN_FIRST_NAMES = [
("Evennia", 'mf'),
("Green Tea", 'f'),
]
NAMEGEN_LAST_NAMES = [ "Beeblebrox", "Son of Odin" ]
```
If you want to replace all of the built-in name lists with your own, set
`NAMEGEN_REPLACE_LISTS = True`
## Generating Fantasy Names
Generating completely made-up names is done with the `fantasy_name` function. The
contrib comes with three built-in styles of names which you can use, or you can
put a dictionary of custom name rules into `settings.py`
Generating a fantasy name takes the ruleset key as the "style" keyword, and can
return either a single name or multiple names. By default, it will return a
single name in the built-in "harsh" style.
```py
>>> namegen.fantasy_name()
'Vhon'
>>> namegen.fantasy_name(num=3, style="fluid")
['Aewalisash', 'Ayi', 'Iaa']
```
### Custom Fantasy Name style rules
The style rules are contained in a dictionary of dictionaries, where the style name
is the key and the style rules are the dictionary value.
The following is how you would add a custom style to `settings.py`:
```py
NAMEGEN_FANTASY_RULES = {
"example_style": {
"example_style": {
"syllable": "(C)VC",
"consonants": [ 'z','z','ph','sh','r','n' ],
"start": ['m'],
"end": ['x','n'],
"vowels": [ "e","e","e","a","i","i","u","o", ],
"length": (2,4),
}
}
}
```
Then you could generate names following that ruleset with
`namegen.fantasy_name(style="example_style")`.
#### syllable
The "syllable" field defines the structure of each syllable. C is consonant, V is vowel,
and parentheses mean it's optional. So, the example "(C)VC" means that every syllable
will always have a vowel followed by a consonant, and will *sometimes* have another
consonant at the beginning.
*Note:* While it's not standard, the contrib lets you nest parentheses, with each layer
being less likely to show up. Additionally, any other characters put into the syllable
structure - e.g. an apostrophe - will be read and inserted as written. Check out the
"alien" style rules in the module for an example of both.
#### consonants
A simple list of consonant phonemes that can be chosen from. Multi-character strings are
perfectly acceptable, such as "th", but each one will be treated as a single consonant.
The function uses a naive form of weighting, where you make a phoneme more likely to
occur by putting more copies of it into the list.
#### start and end
These are **optional** lists for the first and last letters of a syllable, if they're
a consonant. You can add on additional consonants which can only occur at the beginning
or end of a syllable, or you can add extra copies of already-defined consonants to
increase the frequency of them at the start/end of syllables.
They can be left out of custom rulesets entirely.
#### vowels
Works exactly like consonants, but is instead used for the vowel selection. Single-
or multi-character strings are equally fine, and you can increase the frequency of
any given vowel by putting it into the list multiple times.
#### length
A tuple with the minimum and maximum number of syllables a name can have.
When setting this, keep in mind how long your syllables can get! 4 syllables might
not seem like very many, but if you have a (C)(V)VC structure with one- and
two-letter phonemes, you can get up to eight characters per syllable.
"""
import random
@ -171,6 +71,8 @@ import re
from os import path
from django.conf import settings
from evennia.utils.utils import is_iter
# Load name data from Behind the Name lists
dirpath = path.dirname(path.abspath(__file__))
_FIRSTNAME_LIST = []
@ -181,6 +83,7 @@ _SURNAME_LIST = []
with open(path.join(dirpath, "btn_surnames.txt"),'r', encoding='utf-8') as file:
_SURNAME_LIST = [ line.strip() for line in file if line and not line.startswith("#") ]
_REQUIRED_KEYS = { "syllable", "consonants", "vowels", "length" }
# Define phoneme structure for built-in fantasy name generators.
_FANTASY_NAME_STRUCTURES = {
"harsh": {
@ -208,7 +111,7 @@ _FANTASY_NAME_STRUCTURES = {
"length": (1,5),
},
}
}
_RE_DOUBLES = re.compile(r'(\w)\1{2,}')
@ -240,14 +143,34 @@ def fantasy_name(num=1, style="harsh", return_list=False):
return_list (bool) - Whether to always return a list. `False` by default,
which returns a string if there is only one value and a list if more.
"""
def _validate(style_name):
if style_name not in _FANTASY_NAME_STRUCTURES:
raise ValueError(f"Invalid style name: '{style_name}'. Available style names: {' '.join(_FANTASY_NAME_STRUCTURES.keys())}")
style_dict = _FANTASY_NAME_STRUCTURES[style_name]
if type(style_dict) is not dict:
raise ValueError(f"Style {style_name} must be a dictionary.")
keys = set(style_dict.keys())
missing_keys = _REQUIRED_KEYS - keys
if len(set):
raise KeyError(f"Style dictionary {style_name} is missing required keys: {' '.join(missing_keys)}")
if not (is_iter(style_dict['consonants']) and is_iter(style_dict['vowels'])):
raise ValueError(f"'consonants' and 'vowels' keys for style {style_name} must have iterable values.")
if not (is_iter(style_dict['length']) and len(style_dict['length']) == 2):
raise ValueError(f"'length' key for {style_name} must have a minimum and maximum number of syllables.")
return style_dict
# validate num first
num = int(num)
if num < 1:
raise ValueError("Number of names to generate must be positive.")
if style not in _FANTASY_NAME_STRUCTURES:
raise ValueError(f"Invalid style name: '{style}'.")
style_dict = _FANTASY_NAME_STRUCTURES[style]
style_dict = _validate(style)
syllable = []
weight = 8
@ -347,7 +270,7 @@ def first_name(num=1, gender=None, return_list=False, ):
return results
def family_name(num=1, return_list=False):
def last_name(num=1, return_list=False):
"""
Generate family names, also known as surnames or last names.
@ -405,7 +328,7 @@ def full_name(num=1, parts=2, gender=None, return_list=False, surname_first=Fals
familys = total_mids - personals
# then get the names for each
personal_mids = first_name(num=personals, gender=gender, return_list=True)
family_mids = family_name(num=familys, return_list=True) if familys else []
family_mids = last_name(num=familys, return_list=True) if familys else []
# splice them together according to surname_first....
middle_names = family_mids+personal_mids if surname_first else personal_mids+family_mids
# ...and then split into `num`-length lists to be used for the final names
@ -413,13 +336,13 @@ def full_name(num=1, parts=2, gender=None, return_list=False, surname_first=Fals
# get personal and family names
personal_names = first_name(num=num, gender=gender, return_list=True)
family_names = family_name(num=num, return_list=True)
last_names = last_name(num=num, return_list=True)
# attach personal/family names to the list of name lists, according to surname_first
if surname_first:
name_lists = [family_names] + name_lists + [personal_names]
name_lists = [last_names] + name_lists + [personal_names]
else:
name_lists = [personal_names] + name_lists + [family_names]
name_lists = [personal_names] + name_lists + [last_names]
# lastly, zip them all up and join them together
names = list(zip(*name_lists))