diff --git a/evennia/contrib/utils/name_generator/README.md b/evennia/contrib/utils/name_generator/README.md index bc4295553f..866fa63928 100644 --- a/evennia/contrib/utils/name_generator/README.md +++ b/evennia/contrib/utils/name_generator/README.md @@ -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 diff --git a/evennia/contrib/utils/name_generator/namegen.py b/evennia/contrib/utils/name_generator/namegen.py index 8f1a3f1c49..e506ec6331 100644 --- a/evennia/contrib/utils/name_generator/namegen.py +++ b/evennia/contrib/utils/name_generator/namegen.py @@ -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))