diff --git a/evennia/contrib/utils/name_generator/README.md b/evennia/contrib/utils/name_generator/README.md index 866fa63928..d368a9d7f5 100644 --- a/evennia/contrib/utils/name_generator/README.md +++ b/evennia/contrib/utils/name_generator/README.md @@ -128,15 +128,80 @@ 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. +single name in the built-in "harsh" style. The contrib also comes with "fluid" and "alien" styles. ```py >>> namegen.fantasy_name() 'Vhon' +>>> namegen.fantasy_name(num=3, style="harsh") +['Kha', 'Kizdhu', 'Godögäk'] >>> namegen.fantasy_name(num=3, style="fluid") ['Aewalisash', 'Ayi', 'Iaa'] +>>> namegen.fantasy_name(num=5, style="alien") +["Qz'vko'", "Xv'w'hk'hxyxyz", "Wxqv'hv'k", "Wh'k", "Xbx'qk'vz"] ``` +### Multi-Word Fantasy Names + +The `fantasy_name` function will only generate one name-word at a time, so for multi-word names +you'll need to combine pieces together. Depending on what kind of end result you want, there are +several approaches. + + +#### The simple approach + +If all you need is for it to have multiple parts, you can generate multiple names at once and `join` them. + +```py +>>> name = " ".join(namegen.fantasy_name(num=2) +>>> print(name) +Dezhvözh Khäk +``` + +If you want a little more variation between first/last names, you can also generate names for +different styles and then combine them. + +```py +>>> name = "{first} {last}".format( first=namegen.fantasy_name(style="fluid"), last=namegen.fantasy_name(style="harsh") ) +>>> print(name) +Ofasa Käkudhu +``` + +#### "Nakku Silversmith" + +One common fantasy name practice is profession- or title-based surnames. To achieve this effect, +you can use the `last_name` function with a custom list of last names and combine it with your generated +fantasy name. + +Example: +```py +NAMEGEN_LAST_NAMES = [ "Silversmith", "the Traveller", "Destroyer of Worlds" ] +NAMEGEN_REPLACE_LISTS = True + +>>> name = "{first} {last}".format( first=namegen.fantasy_name(), last=namegen.last_name() ) +>>> print(name) +Tözhkheko the Traveller +``` + +#### Elarion d'Yrinea, Thror Obinson + +Another common flavor of fantasy names is to use a surname suffix or prefix. For that, you'll +need to add in the extra bit yourself. + +Examples: +```py +>>> names = namegen.fantasy_name(num=2) +>>> name = f"{names[0]} za'{names[1]}" +>>> print(name) +Tithe za'Dhudozkok + +>>> names = namegen.fantasy_name(num=2) +>>> name = f"{names[0]} {names[1]}son" +>>> print(name) +Kön Ködhöddoson +``` + + ### Custom Fantasy Name style rules The style rules are contained in a dictionary of dictionaries, where the style name @@ -187,12 +252,16 @@ a consonant. You can add on additional consonants which can only occur at the be 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. +For example, in the `example_style` above, we have a `start` of m, and `end` of x and n. +Taken with the rest of the consonants/vowels, this means you can have the syllables of `mez` +but not `zem`, and you can have `phex` or `phen` but not `xeph` or `neph`. + 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. +Vowels is a simple list of vowel phonemes - exactly like consonants, but instead used for the +vowel selection. Single-or multi-character strings are equally fine. It uses the same naive weighting system +as consonants - 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. diff --git a/evennia/contrib/utils/name_generator/namegen.py b/evennia/contrib/utils/name_generator/namegen.py index e506ec6331..e205e702c3 100644 --- a/evennia/contrib/utils/name_generator/namegen.py +++ b/evennia/contrib/utils/name_generator/namegen.py @@ -71,8 +71,6 @@ 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 = [] @@ -157,9 +155,9 @@ def fantasy_name(num=1, style="harsh", return_list=False): 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 (type(style_dict['consonants']) is list and type(style_dict['vowels']) is list): + raise TypeError(f"'consonants' and 'vowels' for style {style_name} must be lists.") + 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.") diff --git a/evennia/contrib/utils/name_generator/tests.py b/evennia/contrib/utils/name_generator/tests.py index 7f7a8ab128..b8b0f7354d 100644 --- a/evennia/contrib/utils/name_generator/tests.py +++ b/evennia/contrib/utils/name_generator/tests.py @@ -6,6 +6,26 @@ Tests for the Random Name Generator from evennia.utils.test_resources import BaseEvenniaTest from . import namegen +_INVALID_STYLES = { + "missing_keys": { + "consonants": ['c','d'], + "length": (1,2), + }, + "invalid_vowels": { + "syllable": "CVC", + "consonants": ['c','d'], + "vowels": "aeiou", + "length": (1,2), + }, + "invalid_length": { + "syllable": "CVC", + "consonants": ['c','d'], + "vowels": ['a','e'], + "length": 2, + }, +} + +namegen._FANTASY_NAME_STRUCTURES |= _INVALID_STYLES class TestNameGenerator(BaseEvenniaTest): def test_fantasy_name(self): @@ -38,7 +58,19 @@ class TestNameGenerator(BaseEvenniaTest): with self.assertRaises(ValueError): namegen.fantasy_name(style="dummy") - + + def test_structure_validation(self): + """ + Verify that validation raises the correct errors for invalid inputs. + """ + with self.assertRaises(KeyError): + namegen.fantasy_name(style="missing_keys") + + with self.assertRaises(TypeError): + namegen.fantasy_name(style="invalid_vowels") + + with self.assertRaises(ValueError): + namegen.fantasy_name(style="invalid_length") def test_first_name(self): """