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 Both real-world and fantasy name generation can be extended to include additional
information via your game's `settings.py` 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 ## Usage
Import the module where you need it with the following: 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. 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, 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 returned value will be a list of strings.
The module is especially useful for naming newly-created NPCs, like so: The module is especially useful for naming newly-created NPCs, like so:
```py ```py
@ -29,6 +32,37 @@ npc_name = namegen.full_name()
npc_obj = create_object(key=npc_name, typeclass="typeclasses.characters.NPC") 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 ## Generating Real Names
The contrib offers three functions for generating random real-world 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" ] NAMEGEN_LAST_NAMES = [ "Beeblebrox", "Son of Odin" ]
``` ```
If you want to replace all of the built-in name lists with your own, set Set `NAMEGEN_REPLACE_LISTS = True` if you want your custom lists above to entirely replace the built-in lists rather than extend them.
`NAMEGEN_REPLACE_LISTS = True`
## Generating Fantasy Names ## Generating Fantasy Names
@ -123,19 +156,23 @@ NAMEGEN_FANTASY_RULES = {
} }
``` ```
Then you could generate names following that ruleset with Then you could generate names following that ruleset with `namegen.fantasy_name(style="example_style")`.
`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 #### syllable
The "syllable" field defines the structure of each syllable. C is consonant, V is vowel, 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 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 *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 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 structure - e.g. an apostrophe - will be read and inserted as written. The
"alien" style rules in the module for an example of both. "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 #### consonants
A simple list of consonant phonemes that can be chosen from. Multi-character strings are 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) 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 Both real-world and fantasy name generation can be extended to include additional
information via your game's `settings.py` information via your game's `settings.py`
## Usage
Import the module where you need it with the following: Available Methods:
```py
from evennia.contrib.utils.name_generator import namegen
```
By default, all of the functions will return a string with one generated name. first_name - Selects a random a first (personal) name from the name lists.
If you specify more than one, or pass `return_list=True` as a keyword argument, last_name - Selects a random last (family) name from the name lists.
the returned value will be a list of strings. 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: Method examples:
```py
npc_name = namegen.full_name()
npc_obj = create_object(key=npc_name, typeclass="typeclasses.characters.NPC")
```
## 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) >>> namegen.first_name(num=5)
['Genesis', 'Tali', 'Budur', 'Dominykas', 'Kamau'] ['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) >>> namegen.full_name(parts=3, surname_first=True)
'Ó Muircheartach Torunn Dyson' 'Ó Muircheartach Torunn Dyson'
>>> namegen.full_name(gender='f') >>> namegen.full_name(gender='f')
'Wikolia Ó Deasmhumhnaigh' '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 Available Settings (define these in your `settings.py`)
and then second value is the gender flag - 'm' for masculine-only, 'f' for feminine-
only, and 'mf' for either one.
`NAMEGEN_LAST_NAMES` should be a list of strings, where each item is an available NAMEGEN_FIRST_NAMES - Option to add a new list of first (personal) names.
surname. 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 = [ NAMEGEN_FIRST_NAMES = [
("Evennia", 'mf'), ("Evennia", 'mf'),
("Green Tea", 'f'), ("Green Tea", 'f'),
] ]
NAMEGEN_LAST_NAMES = [ "Beeblebrox", "Son of Odin" ] 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 = { NAMEGEN_FANTASY_RULES = {
"example_style": { "example_style": {
"syllable": "(C)VC", "syllable": "(C)VC",
"consonants": [ 'z','z','ph','sh','r','n' ], "consonants": [ 'z','z','ph','sh','r','n' ],
"start": ['m'], "start": ['m'],
"end": ['x','n'], "end": ['x','n'],
"vowels": [ "e","e","e","a","i","i","u","o", ], "vowels": [ "e","e","e","a","i","i","u","o", ],
"length": (2,4), "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 import random
@ -171,6 +71,8 @@ import re
from os import path from os import path
from django.conf import settings from django.conf import settings
from evennia.utils.utils import is_iter
# Load name data from Behind the Name lists # Load name data from Behind the Name lists
dirpath = path.dirname(path.abspath(__file__)) dirpath = path.dirname(path.abspath(__file__))
_FIRSTNAME_LIST = [] _FIRSTNAME_LIST = []
@ -181,6 +83,7 @@ _SURNAME_LIST = []
with open(path.join(dirpath, "btn_surnames.txt"),'r', encoding='utf-8') as file: 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("#") ] _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. # Define phoneme structure for built-in fantasy name generators.
_FANTASY_NAME_STRUCTURES = { _FANTASY_NAME_STRUCTURES = {
"harsh": { "harsh": {
@ -208,7 +111,7 @@ _FANTASY_NAME_STRUCTURES = {
"length": (1,5), "length": (1,5),
}, },
} }
_RE_DOUBLES = re.compile(r'(\w)\1{2,}') _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, 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. 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 # validate num first
num = int(num) num = int(num)
if num < 1: if num < 1:
raise ValueError("Number of names to generate must be positive.") raise ValueError("Number of names to generate must be positive.")
if style not in _FANTASY_NAME_STRUCTURES: style_dict = _validate(style)
raise ValueError(f"Invalid style name: '{style}'.")
style_dict = _FANTASY_NAME_STRUCTURES[style]
syllable = [] syllable = []
weight = 8 weight = 8
@ -347,7 +270,7 @@ def first_name(num=1, gender=None, return_list=False, ):
return results 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. 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 familys = total_mids - personals
# then get the names for each # then get the names for each
personal_mids = first_name(num=personals, gender=gender, return_list=True) 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.... # splice them together according to surname_first....
middle_names = family_mids+personal_mids if surname_first else personal_mids+family_mids 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 # ...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 # get personal and family names
personal_names = first_name(num=num, gender=gender, return_list=True) 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 # attach personal/family names to the list of name lists, according to surname_first
if surname_first: if surname_first:
name_lists = [family_names] + name_lists + [personal_names] name_lists = [last_names] + name_lists + [personal_names]
else: 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 # lastly, zip them all up and join them together
names = list(zip(*name_lists)) names = list(zip(*name_lists))