diff --git a/CHANGELOG.md b/CHANGELOG.md index 6658661b77..1642caadea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,6 +97,8 @@ Up requirements to Django 3.2+, Twisted 21+ not support this (useful for getting a complete log). - Make `@lazy_property` decorator create read/delete-protected properties. This is because it's used for handlers, and e.g. self.locks=[] is a common beginner mistake. +- Add `$pron()` inlinefunc for pronoun parsing in actor-stance strings using + `msg_contents`. diff --git a/docs/source/Components/Default-Commands.md b/docs/source/Components/Default-Commands.md index 4769c731f8..8391304bc6 100644 --- a/docs/source/Components/Default-Commands.md +++ b/docs/source/Components/Default-Commands.md @@ -16,8 +16,8 @@ with [EvEditor](./EvEditor.md), flipping pages in [EvMore](./EvMore.md) or using - [**__unloggedin_look_command** [look, l]](evennia.commands.default.unloggedin.CmdUnconnectedLook) (cmdset: [UnloggedinCmdSet](evennia.commands.default.cmdset_unloggedin.UnloggedinCmdSet), help-category: _General_) - [**about** [version]](evennia.commands.default.system.CmdAbout) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _System_) -- [**access** [hierarchy, groups]](evennia.commands.default.general.CmdAccess) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _General_) -- [**accounts** [account, listaccounts]](evennia.commands.default.system.CmdAccounts) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _System_) +- [**access** [groups, hierarchy]](evennia.commands.default.general.CmdAccess) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _General_) +- [**accounts** [listaccounts, account]](evennia.commands.default.system.CmdAccounts) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _System_) - [**addcom** [aliaschan, chanalias]](evennia.commands.default.comms.CmdAddCom) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _Comms_) - [**alias** [setobjalias]](evennia.commands.default.building.CmdSetObjAlias) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_) - [**allcom**](evennia.commands.default.comms.CmdAllCom) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _Comms_) @@ -39,7 +39,7 @@ with [EvEditor](./EvEditor.md), flipping pages in [EvMore](./EvMore.md) or using - [**create**](evennia.commands.default.building.CmdCreate) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_) - [**create** [cr, cre]](evennia.commands.default.unloggedin.CmdUnconnectedCreate) (cmdset: [UnloggedinCmdSet](evennia.commands.default.cmdset_unloggedin.UnloggedinCmdSet), help-category: _General_) - [**cwho**](evennia.commands.default.comms.CmdCWho) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _Comms_) -- [**delcom** [delaliaschan, delchanalias]](evennia.commands.default.comms.CmdDelCom) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _Comms_) +- [**delcom** [delchanalias, delaliaschan]](evennia.commands.default.comms.CmdDelCom) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _Comms_) - [**desc** [describe]](evennia.commands.default.building.CmdDesc) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_) - [**destroy** [delete, del]](evennia.commands.default.building.CmdDestroy) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_) - [**dig**](evennia.commands.default.building.CmdDig) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_) @@ -65,17 +65,17 @@ with [EvEditor](./EvEditor.md), flipping pages in [EvMore](./EvMore.md) or using - [**mvattr**](evennia.commands.default.building.CmdMvAttr) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_) - [**name** [rename]](evennia.commands.default.building.CmdName) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_) - [**nick** [nickname, nicks]](evennia.commands.default.general.CmdNick) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _General_) -- [**objects** [listobjects, db, stats, listobjs]](evennia.commands.default.building.CmdObjects) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _System_) +- [**objects** [db, stats, listobjs, listobjects]](evennia.commands.default.building.CmdObjects) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _System_) - [**ooc** [unpuppet]](evennia.commands.default.account.CmdOOC) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _General_) - [**open**](evennia.commands.default.building.CmdOpen) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_) - [**option** [options]](evennia.commands.default.account.CmdOption) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _General_) - [**page** [tell]](evennia.commands.default.comms.CmdPage) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _Comms_) - [**password**](evennia.commands.default.account.CmdPassword) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _General_) -- [**pose** [:, emote]](evennia.commands.default.general.CmdPose) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _General_) +- [**pose** [emote, :]](evennia.commands.default.general.CmdPose) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _General_) - [**py** [!]](evennia.commands.default.system.CmdPy) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _System_) - [**quell** [unquell]](evennia.commands.default.account.CmdQuell) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _General_) - [**quit**](evennia.commands.default.account.CmdQuit) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _General_) -- [**quit** [q, qu]](evennia.commands.default.unloggedin.CmdUnconnectedQuit) (cmdset: [UnloggedinCmdSet](evennia.commands.default.cmdset_unloggedin.UnloggedinCmdSet), help-category: _General_) +- [**quit** [qu, q]](evennia.commands.default.unloggedin.CmdUnconnectedQuit) (cmdset: [UnloggedinCmdSet](evennia.commands.default.cmdset_unloggedin.UnloggedinCmdSet), help-category: _General_) - [**reload** [restart]](evennia.commands.default.system.CmdReload) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _System_) - [**reset** [reboot]](evennia.commands.default.system.CmdReset) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _System_) - [**rss2chan**](evennia.commands.default.comms.CmdRSS2Chan) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _Comms_) diff --git a/docs/source/Components/FuncParser.md b/docs/source/Components/FuncParser.md index 38167f3ac0..cb51d80c6b 100644 --- a/docs/source/Components/FuncParser.md +++ b/docs/source/Components/FuncParser.md @@ -319,9 +319,11 @@ The `caller` is required, it's the the object to do the access-check for. The `a These are used to implement actor-stance emoting. They are used by the [DefaultObject.msg_contents](evennia.objects.objects.DefaultObject.msg_contents) method -by default. +by default. You can read a lot more about this on the page +[Change messages per receiver](../Concepts/Change-Messages-Per-Receiver.md). -These all require extra kwargs be passed into the parser: +On the parser side, all these inline functions require extra kwargs be passed into the parser +(done by `msg_contents` by default): ```python parser.parse(string, caller=, receiver=, mapping={'key': , ...}) @@ -337,10 +339,13 @@ references to other objects accessible via these callables. result of `you_obj.get_display_name(looker=receiver)`. This allows for a single string to echo differently depending on who sees it, and also to reference other people in the same way. - `$You([key])` - same as `$you` but always capitalized. -- `$conj(verb)` ([code](evennia.utils.funcparser.funcparser_callable_conjugate)) -- conjugates a verb between 4nd person presens to 3rd person presence depending on who +- `$conj(verb)` ([code](evennia.utils.funcparser.funcparser_callable_conjugate)) - conjugates a verb + between 2nd person presens to 3rd person presence depending on who sees the string. For example `"$You() $conj(smiles)".` will show as "You smile." and "Tom smiles." depending on who sees it. This makes use of the tools in [evennia.utils.verb_conjugation](evennia.utils.verb_conjugation) to do this, and only works for English verbs. +- `$pron(pronoun [,options])` ([code](evennia.utils.funcparser.funcparser_callable_pronoun)) - Dynamically + map pronouns (like his, herself, you, its etc) between 1st/2nd person to 3rd person. ### Example diff --git a/docs/source/Concepts/Change-Messages-Per-Receiver.md b/docs/source/Concepts/Change-Messages-Per-Receiver.md new file mode 100644 index 0000000000..ca452f9f52 --- /dev/null +++ b/docs/source/Concepts/Change-Messages-Per-Receiver.md @@ -0,0 +1,220 @@ +# Sending different messages depending on viewpoint and receiver + +Sending messages to everyong in a location is handled by the +[msg_contents](evennia.objects.objects.DefaultObject.msg_contents) method on +all [Objects](../Components/Objects.md). It's most commonly called on rooms. + +```python +room.msg_contents("Anna walks into the room.") +``` + +You can also embed references in the string: + +```python + +room.msg_contents("{anna} walks into the room.", + from_obj=caller, + mapping={'anna': anna_object}) +``` + +Use `exclude=object_or_list_of_object` to skip sending the message one or more targets. + +The advantage of this is that `anna_object.get_display_name(looker)` will be called +for every onlooker; this allows the `{anna}` stanza to be different depending on who +sees the strings. How this is to work depends on the _stance_ of your game. + +The stance indicates how your game echoes its messages to the player. Knowing how you want to +handle the stance is important for a text game. There are two main stances that are usually considered, +_Actor stance_ and _Director stance_. + +| Stance | You see | Others in the same location see | +| --- | --- | --- | +| Actor stance | You pick up the stone | Anna picks up the stone | +|Director stance | Anna picks up the stone | Anna picks up the stone | + +It's not unheard of to mix the two stances - with commands from the game being told +in Actor stance while Director stance is used for complex emoting and roleplaying. One should +usually try to be consistent however. + +## Director Stance + +While not so common as Actor stance, director stance has the advantage of simplicity, particularly +in roleplaying MUDs where longer roleplaying emotes are used. It is also a pretty simple stance to +implement technically since everyone sees the same text, regardless of viewpoint. + +Here's an example of a flavorful text to show the room: + + Tom picks up the gun, whistling to himself. + +Everyone will see this string, both Tom and others. Here's how to send it to everyone in +the room. + +```python +text = "Tom picks up the gun, whistling to himself." +room.msg_contents(text) +``` + +One may want to expand on it by making the name `Tom` be seen differently by different people, +but the English grammar of the sentence does not change. Not only is this pretty easy to do +technically, it's also easy to write for the player. + +## Actor Stance + +This means that the game addresses "you" when it does things. In actor stance, whenever you perform +an action, you should get a different message than those _observing_ you doing that action. + + Tom picks up the gun, whistling to himself. + +This is what _others_ should see. The player themselves should see this: + + You pick up the gun, whistling to yourself. + +Not only do you need to map "Tom" to "You" above, there are also grammatical differences - +"Tom walks" vs "You walk" and "himself" vs "yourself". This is a lot more complex to handle. For a +developer making simple "You/Tom pick/picks up the stone" messages, you could in principle hand-craft +the strings from every view point, but there's a better way. + +The `msg_contents` method helps by parsing the ingoing string with a +[FuncParser functions](../Components/FuncParser.md) with some very specific `$inline-functions`. The inline funcs +basically provides you with a mini-language for building _one_ string that will change +appropriately depending on who sees it. + + +```python +text = "$You() $conj(pick) up the gun, whistling to $pron(yourself)." +room.msg_contents(text, from_obj=caller, mapping={"gun": gun_object}) +``` + +These are the inline-functions available: + +- `$You()/$you()` - this is a reference to 'you' in the text. It will be replaced with "You/you" for + the one sending the text and with the return from `caller.get_display_name(looker)` for everyone else. +- `$conj(verb)` - this will conjugate the given verb depending on who sees the string (like `pick` + to `picks`). Enter the root form of the verb. +- `$pron(pronoun[,options])` - A pronoun is a word you want to use instead of a proper noun, like + _him_, _herself_, _its_, _me_, _I_, _their_ and so on. The `options` is a space- or comma-separated + set of options to help the system map your pronoun from 1st/2nd person to 3rd person and vice versa. + See next section. + +### More on $pron() + +The `$pron()` inline func maps between 1st/2nd person (I/you) to 3rd person (he/she etc). In short, +it translates between this table ... + +| | Subject Pronoun | Object Pronoun | Possessive Adjective | Possessive Pronoun | Reflexive Pronoun | +| --- | --- | --- | --- | --- | --- | +| **1st person** | I | me | my | mine | myself | +| **1st person plural** | we | us | our | ours | ourselves | +| **2nd person** | you | you | your | yours | yourself | +| **2nd person plural** | you | you | your | yours | yourselves | + +... to this table (in both directions): + +| | Subject Pronoun | Object Pronoun | Possessive Adjective | Possessive Pronoun | Reflexive Pronoun | +| --- | --- | --- | --- | --- | --- | +| **3rd person male** | he | him | his | his | himself | +| **3rd person female** | she | her | her | hers | herself | +| **3rd person neutral** | it | it | its | theirs* | itself | +| **3rd person plural** | they | them | their | theirs | themselves | + +> *) The neutral 3rd person possessive pronoun is not actually used in English. We set it to "theirs" +> just to have something to show should someone accidentally ask for a neutral possessive pronoun. + +Some mappings are easy. For example, if you write `$pron(yourselves)` then the 3rd-person +form is always `themselves`. But because English grammar is the way it is, not all mappings +are 1:1. For example, if you write +`$pron(you)`, Evennia will not know which 3rd-persion equivalent this should map to - you need to +provide more info to help out. This can either be provided as a second space-separated option +to `$pron` or the system will try to figure it out on its own. + +- `pronoun_type` - this is one of the columns in the table and can be set as a `$pron` option. + + - `subject pronoun` (aliases `subject` or `sp`) + - `object pronoun` (aliases `object` or `op`) + - `possessive adjective` (aliases `adjective` or `pa`) + - `possessive pronoun` (aliases `pronoun` or `pp`). + + (There is no need to specify reflexive pronouns since they + are all uniquely mapped 1:1). Speciying the pronoun-type is mainly needed when using `you`, + since the same 'you' is used to represent all sorts of things in English grammar. + If not specified and the mapping is not clear, a 'subject pronoun' (he/she/it/they) is assumed. +- `gender` - set in `$pron` option as + + - `male`, or `m` + - `female'` or `f` + - `neutral`, or `n` + - `plural`, or `p` (yes plural is considered a 'gender' for this purpose). + + If not set as an option the system will + look for a callable or property `.gender` on the current `from_obj`. A callable will be called + with no arguments and is expected to return a string 'male/female/neutral/plural'. If none + is found, a neutral gender is assumed. +- `viewpoint`- set in `$pron` option as + + - `1st person` (aliases `1st` or `1`) + - `2nd person` (aliases `2nd` or `2`) + + This is only needed if you want to have 1st person perspective - if + not, 2nd person is assumed wherever the viewpoint is unclear. + +`$pron()` examples: + +| Input | you see | others see | note | +| --- | --- | ---| --- | +| `$pron(I, male)` | I | he | | +| `$pron(I, f)` | I | she | | +| `$pron(my)` | my | its | figures out it's an possessive adjective, assumes neutral | +| `$pron(you)` | you | it | assumes neutral subject pronoun | +| `$pron(you, f)` | you | she | female specified, assumes subject pronoun | +| `$pron(you,op f)` | you | her | | +| `$pron(you,op p)` | you | them | | +| `$pron(you, f op)` | you | her | specified female and objective pronoun| +| `$pron(yourself)` | yourself | itself | | +| `$pron(its)` | your | its | | +| `$Pron(its)` | Your | Its | Using $Pron always capitalizes | +| `$pron(her)` | you | her | 3rd person -> 2nd person | +| `$pron(her, 1)` | I | her | 3rd person -> 1st person | +| `$pron(its, 1st)` | my | its | 3rd person -> 1st person | + + +Note the three last examples - instead of specifying the 2nd person form you +can also specify the 3rd-person and do a 'reverse' lookup - you will still see the proper 1st/2nd text. +So writing `$pron(her)` instead of `$pron(you, op f)` gives the same result. + +The [$pron inlinefunc api is found here](evennia.utils.funcparser.funcparser_callable_pronoun) + +# Referencing other objects + +There is one more inlinefunc understood by `msg_contents`. This can be used natively to spruce up +your strings (for both director- and actor stance): + +- `$Obj(name)/$obj(name)` references another entity, which must be supplied + in the `mapping` keyword argument to `msg_contents`. The object's `.get_display_name(looker)` will be + called and inserted instead. This is essentially the same as using the `{anna}` marker we used + in the first example at the top of this page, but using `$Obj/$obj` allows you to easily + control capitalization. + +This is used like so: + +```python +# director stance +text = "Tom picks up the $obj(gun), whistling to himself" + +# actor stance +text = "$You() $conj(pick) up the $obj(gun), whistling to $pron(yourself)" + +room.msg_contents(text, from_obj=caller, mapping={"gun": gun_object}) +``` +Depending on your game, Tom may now see himself picking up `A rusty old gun`, whereas an onlooker +with a high gun smith skill may instead see him picking up `A rare-make Smith & Wesson model 686 +in poor condition" ...` + +# Recog systems and roleplaying + +The `$funcparser` inline functions are very powerful for the game developer, but they may +be a bit too much to write for the regular player. + +The [rpsystem contrib](evennia.contribs.rpsystem) implements a full dynamic emote/pose and recognition +system with short-descriptions and disguises. It uses director stance with a custom markup +language, like `/me` `/gun` and `/tall man` to refer to players and objects in the location. It can be +worth checking out for inspiration. \ No newline at end of file diff --git a/docs/source/api/evennia.server.md b/docs/source/api/evennia.server.md index 5a8fc21fb4..e80c01b070 100644 --- a/docs/source/api/evennia.server.md +++ b/docs/source/api/evennia.server.md @@ -19,6 +19,7 @@ evennia.server evennia.server.initial_setup evennia.server.inputfuncs evennia.server.manager + evennia.server.markup evennia.server.models evennia.server.server evennia.server.serversession diff --git a/docs/source/api/evennia.utils.verb_conjugation.md b/docs/source/api/evennia.utils.verb_conjugation.md index a3359e1408..e2fb6e5fe0 100644 --- a/docs/source/api/evennia.utils.verb_conjugation.md +++ b/docs/source/api/evennia.utils.verb_conjugation.md @@ -13,6 +13,7 @@ evennia.utils.verb\_conjugation :maxdepth: 6 evennia.utils.verb_conjugation.conjugate + evennia.utils.verb_conjugation.pronouns evennia.utils.verb_conjugation.tests ``` \ No newline at end of file diff --git a/docs/source/api/evennia.utils.verb_conjugation.pronouns.md b/docs/source/api/evennia.utils.verb_conjugation.pronouns.md new file mode 100644 index 0000000000..1456e857ea --- /dev/null +++ b/docs/source/api/evennia.utils.verb_conjugation.pronouns.md @@ -0,0 +1,10 @@ +```{eval-rst} +evennia.utils.verb\_conjugation.pronouns +=============================================== + +.. automodule:: evennia.utils.verb_conjugation.pronouns + :members: + :undoc-members: + :show-inheritance: + +``` \ No newline at end of file diff --git a/docs/source/toc.md b/docs/source/toc.md index b7ccdaf8d2..7148bd9d03 100644 --- a/docs/source/toc.md +++ b/docs/source/toc.md @@ -58,6 +58,7 @@ Concepts/Async-Process Concepts/Banning Concepts/Bootstrap-&-Evennia Concepts/Building-Permissions +Concepts/Change-Messages-Per-Receiver Concepts/Clickable-Links Concepts/Colors Concepts/Concepts-Overview @@ -320,6 +321,7 @@ api/evennia.server.game_index_client.service api/evennia.server.initial_setup api/evennia.server.inputfuncs api/evennia.server.manager +api/evennia.server.markup api/evennia.server.models api/evennia.server.portal api/evennia.server.portal.amp @@ -393,6 +395,7 @@ api/evennia.utils.utils api/evennia.utils.validatorfuncs api/evennia.utils.verb_conjugation api/evennia.utils.verb_conjugation.conjugate +api/evennia.utils.verb_conjugation.pronouns api/evennia.utils.verb_conjugation.tests api/evennia.web api/evennia.web.admin diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index e43947e660..7632ec3353 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -38,13 +38,8 @@ _COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) # the sessid_max is based on the length of the db_sessid csv field (excluding commas) _SESSID_MAX = 16 if _MULTISESSION_MODE in (1, 3) else 1 -_MSG_CONTENTS_PARSER = funcparser.FuncParser( - { - "you": funcparser.funcparser_callable_you, - "You": funcparser.funcparser_callable_You, - "conj": funcparser.funcparser_callable_conjugate - } -) +# init the actor-stance funcparser for msg_contents +_MSG_CONTENTS_PARSER = funcparser.FuncParser(funcparser.ACTOR_STANCE_CALLABLES) class ObjectSessionHandler: @@ -729,8 +724,9 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): on the valid OOB outmessage form `(message, {kwargs})`, where kwargs are optional data passed to the `text` outputfunc. The message will be parsed for `{key}` formatting and - `$You/$you()/$You(key)` and `$conj(verb)` inline function callables. - The `key` is taken from the `mapping` kwarg {"key": object, ...}`. + `$You/$you()/$You()`, `$obj(name)`, `$conj(verb)` and `$pron(pronoun, option)` + inline function callables. + The `name` is taken from the `mapping` kwarg {"name": object, ...}`. The `mapping[key].get_display_name(looker=recipient)` will be called for that key for every recipient of the string. exclude (list, optional): A list of objects not to send to. diff --git a/evennia/utils/funcparser.py b/evennia/utils/funcparser.py index 31fed57b8d..033a06c350 100644 --- a/evennia/utils/funcparser.py +++ b/evennia/utils/funcparser.py @@ -53,6 +53,7 @@ from evennia.utils.utils import ( safe_convert_to_types) from evennia.utils import search from evennia.utils.verb_conjugation.conjugate import verb_actor_stance_components +from evennia.utils.verb_conjugation.pronouns import pronoun_to_viewpoints # setup @@ -1082,8 +1083,8 @@ def funcparser_callable_you(*args, caller=None, receiver=None, mapping=None, cap if hasattr(caller, "get_display_name") else str(caller)) -def funcparser_callable_You(*args, you=None, receiver=None, mapping=None, capitalize=True, - **kwargs): +def funcparser_callable_you_capitalize( + *args, you=None, receiver=None, mapping=None, capitalize=True, **kwargs): """ Usage: $You() - capitalizes the 'you' output. @@ -1094,6 +1095,8 @@ def funcparser_callable_You(*args, you=None, receiver=None, mapping=None, capita def funcparser_callable_conjugate(*args, caller=None, receiver=None, **kwargs): """ + Usage: $conj(word, [options]) + Conjugate a verb according to if it should be 2nd or third person. Keyword Args: @@ -1130,6 +1133,146 @@ def funcparser_callable_conjugate(*args, caller=None, receiver=None, **kwargs): return second_person_str if caller == receiver else third_person_str +def funcparser_callable_pronoun(*args, caller=None, receiver=None, capitalize=False, **kwargs): + """ + + Usage: $prop(word, [options]) + + Adjust pronouns to the expected form. Pronouns are words you use instead of a + proper name, such as 'him', 'herself', 'theirs' etc. These look different + depending on who sees the outgoing string. + + The parser maps between this table ... + + ==================== ======= ======= ========== ========== =========== + 1st/2nd person Subject Object Possessive Possessive Reflexive + Pronoun Pronoun Adjective Pronoun Pronoun + ==================== ======= ======= ========== ========== =========== + 1st person I me my mine myself + 1st person plural we us our ours ourselves + 2nd person you you your yours yourself + 2nd person plural you you your yours yourselves + ==================== ======= ======= ========== ========== =========== + + ... and this table (and vice versa). + + ==================== ======= ======= ========== ========== =========== + 3rd person Subject Object Possessive Possessive Reflexive + Pronoun Pronoun Adjective Pronoun Pronoun + ==================== ======= ======= ========== ========== =========== + 3rd person male he him his his himself + 3rd person female she her her hers herself + 3rd person neutral it it its itself + 3rd person plural they them their theirs themselves + ==================== ======= ======= ========== ========== =========== + + This system will examine `caller` for either a property or a callable `.gender` to + get a default gender fallback (if not specified in the call). If a callable, + `.gender` will be called without arguments and should return a string + `male`/`female`/`neutral`/`plural` (plural is considered a gender for this purpose). + If no `gender` property/callable is found, `neutral` is used as a fallback. + + The pronoun-type default (if not spefified in call) is `subject pronoun`. + + Args: + pronoun (str): Input argument to parsed call. This can be any of the pronouns + in the table above. If given in 1st/second form, they will be mappped to + 3rd-person form for others viewing the message (but will need extra input + via the `gender`, see below). If given on 3rd person form, this will be + mapped to 2nd person form for `caller` unless `viewpoint` is specified + in options. + options (str, optional): A space- or comma-separated string detailing `pronoun_type`, + `gender`/`plural` and/or `viewpoint` to help the mapper differentiate between + non-unique cases (such as if `you` should become `him` or `they`). + Allowed values are: + + - `subject pronoun`/`subject`/`sp` (I, you, he, they) + - `object pronoun`/`object/`/`op` (me, you, him, them) + - `possessive adjective`/`adjective`/`pa` (my, your, his, their) + - `possessive pronoun`/`pronoun`/`pp` (mine, yours, his, theirs) + - `male`/`m` + - `female`/`f` + - `neutral`/`n` + - `plural`/`p` + - `1st person`/`1st`/`1` + - `2nd person`/`2nd`/`2` + - `3rd person`/`3rd`/`3` + + Keyword Args: + + caller (Object): The object creating the string. If this has a property 'gender', + it will be checked for a string 'male/female/neutral' to determine + the 3rd person gender (but if `pronoun_type` contains a gender + component, that takes precedence). Provided automatically to the + funcparser. + receiver (Object): The recipient of the string. This being the same as + `caller` or not helps determine 2nd vs 3rd-person forms. This is + provided automatically by the funcparser. + capitalize (bool): The input retains its capitalization. If this is set the output is + always capitalized. + + Examples: + + ====================== ============= =========== + Input caller sees others see + ====================== ============= =========== + $pron(I, m) I he + $pron(you,fo) you her + $pron(yourself) yourself itself + $pron(its) your its + $pron(you,op,p) you them + ====================== ============= =========== + + Notes: + There is no option to specify reflexive pronouns since they are all unique + and the mapping can always be auto-detected. + + """ + if not args: + return '' + + pronoun, *options = args + # options is either multiple args or a space-separated string + if len(options) == 1: + options = options[0] + + # default differentiators + default_pronoun_type = "subject pronoun" + default_gender = "neutral" + default_viewpoint = "2nd person" + + if hasattr(caller, "gender"): + if callable(caller, gender): + default_gender = caller.gender() + else: + default_gender = caller.gender + + if "viewpoint" in kwargs: + # passed into FuncParser initialization + default_viewpoint = kwargs["viewpoint"] + + pronoun_1st_or_2nd_person, pronoun_3rd_person = pronoun_to_viewpoints( + pronoun, options, + pronoun_type=default_pronoun_type, gender=default_gender, viewpoint=default_viewpoint) + + if capitalize: + pronoun_1st_or_2nd_person = pronoun_1st_or_2nd_person.capitalize() + pronoun_3rd_person = pronoun_3rd_person.capitalize() + + return pronoun_1st_or_2nd_person if caller == receiver else pronoun_3rd_person + + +def funcparser_callable_pronoun_capitalize( + *args, caller=None, receiver=None, capitalize=True, **kwargs): + """ + Usage: $Pron(word) - always maps to a capitalized word. + + """ + return funcparser_callable_pronoun( + *args, caller=caller, receiver=receiver, capitalize=capitalize, **kwargs) + + + # these are made available as callables by adding 'evennia.utils.funcparser' as # a callable-path when initializing the FuncParser. @@ -1176,6 +1319,10 @@ SEARCHING_CALLABLES = { ACTOR_STANCE_CALLABLES = { # requires `you`, `receiver` and `mapping` to be passed into parser "you": funcparser_callable_you, - "You": funcparser_callable_You, + "You": funcparser_callable_you_capitalize, + "obj": funcparser_callable_you, + "Obj": funcparser_callable_you_capitalize, "conj": funcparser_callable_conjugate, + "pron": funcparser_callable_pronoun, + "Pron": funcparser_callable_pronoun_capitalize, } diff --git a/evennia/utils/gametime.py b/evennia/utils/gametime.py index f10eddfdb3..7b4316f5a3 100644 --- a/evennia/utils/gametime.py +++ b/evennia/utils/gametime.py @@ -4,6 +4,7 @@ The gametime module handles the global passage of time in the mud. It also supplies some useful methods to convert between in-mud time and real-world time as well allows to get the total runtime of the server and the current uptime. + """ import time @@ -244,8 +245,11 @@ def schedule( script (Script): The created Script handling the sceduling. Examples: - schedule(func, min=5, sec=0) # Will call 5 minutes past the next (in-game) hour. - schedule(func, hour=2, min=30, sec=0) # Will call the next (in-game) day at 02:30. + :: + + schedule(func, min=5, sec=0) # Will call 5 minutes past the next (in-game) hour. + schedule(func, hour=2, min=30, sec=0) # Will call the next (in-game) day at 02:30. + """ seconds = real_seconds_until(sec=sec, min=min, hour=hour, day=day, month=month, year=year) script = create_script( diff --git a/evennia/utils/tests/test_funcparser.py b/evennia/utils/tests/test_funcparser.py index 3fc41aee93..76089acf08 100644 --- a/evennia/utils/tests/test_funcparser.py +++ b/evennia/utils/tests/test_funcparser.py @@ -5,6 +5,7 @@ Test the funcparser module. """ import time +from unittest.mock import MagicMock, patch from ast import literal_eval from simpleeval import simple_eval from parameterized import parameterized @@ -306,6 +307,10 @@ class TestDefaultCallables(TestCase): ("$You() $conj(smile) at $You(char1).", "You smile at You.", "Char1 smiles at Char1."), ("$You() $conj(smile) at $You(char2).", "You smile at Char2.", "Char1 smiles at You."), ("$You(char2) $conj(smile) at $you(char1).", "Char2 smile at you.", "You smiles at Char1."), + ("$You() $conj(smile) to $pron(yourself,m).", "You smile to yourself.", + "Char1 smiles to himself."), + ("$You() $conj(smile) to $pron(herself).", "You smile to yourself.", + "Char1 smiles to herself.") # reverse reference ]) def test_conjugate(self, string, expected_you, expected_them): """ @@ -464,3 +469,5 @@ class TestCallableSearch(test_resources.EvenniaTest): ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True) self.assertEqual(expected, ret) + + diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 53d410098d..91a5dbf057 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -2628,3 +2628,40 @@ def strip_unsafe_input(txt, session=None, bypass_perms=None): txt = strip_tags(txt) txt = _STRIP_UNSAFE_TOKENS(txt) return txt + + +def copy_word_case(base_word, new_word): + """ + Converts a word to use the same capitalization as a first word. + + Args: + base_word (str): A word to get the capitalization from. + new_word (str): A new word to capitalize in the same way as `base_word`. + + Returns: + str: The `new_word` with capitalization matching the first word. + + Notes: + This is meant for words. Longer sentences may get unexpected results. + + If the two words have a mix of capital/lower letters _and_ `new_word` + is longer than `base_word`, the excess will retain its original case. + + """ + + # Word + if base_word.istitle(): + return new_word.title() + # word + elif base_word.islower(): + return new_word.lower() + # WORD + elif base_word.isupper(): + return new_word.upper() + else: + # WorD - a mix. Handle each character + maxlen = len(base_word) + shared, excess = new_word[:maxlen], new_word[maxlen - 1:] + return "".join(char.upper() if base_word[ic].isupper() else char.lower() + for ic, char in enumerate(new_word)) + excess + diff --git a/evennia/utils/verb_conjugation/pronouns.py b/evennia/utils/verb_conjugation/pronouns.py new file mode 100644 index 0000000000..f1fdd2c86f --- /dev/null +++ b/evennia/utils/verb_conjugation/pronouns.py @@ -0,0 +1,560 @@ +""" +English pronoun mapping between 1st/2nd person and 3rd person perspective (and vice-versa). + +This file is released under the Evennia regular BSD License. +(Griatch 2021) + +Pronouns are words you use instead of a proper name, such as 'him', 'herself', 'theirs' etc. These +look different depending on who sees the outgoing string. This mapping maps between 1st/2nd case and +the 3rd person case and back. In some cases, the mapping is not unique; it is assumed the system can +differentiate between the options in some other way. + + +==================== ======= ======== ========== ========== =========== +viewpoint/pronouns Subject Object Possessive Possessive Reflexive + Pronoun Pronoun Adjective Pronoun Pronoun +==================== ======= ======== ========== ========== =========== +1st person I me my mine myself +1st person plural we us our ours ourselves +2nd person you you your yours yourself +2nd person plural you you your yours yourselves + +3rd person male he him his his himself +3rd person female she her her hers herself +3rd person neutral it it its theirs* itself +3rd person plural they them their theirs themselves +==================== ======= ======== ========== ========== =========== + +> `*`) Not formally used, we use `theirs` here as a filler. + +""" +from evennia.utils.utils import copy_word_case + +DEFAULT_PRONOUN_TYPE = "object_pronoun" +DEFAULT_VIEWPOINT = "2nd person" +DEFAULT_GENDER = "neutral" + +PRONOUN_MAPPING = { + # 1st/2nd person -> 3rd person mappings + "I": { + "subject pronoun": { + "3rd person": { + "male": "he", + "female": "she", + "neutral": "it" + } + } + }, + "me": { + "object pronoun": { + "3rd person": { + "male": "him", + "female": "her", + "neutral": "it" + } + } + }, + "my": { + "possessive adjective": { + "3rd person": { + "male": "his", + "female": "her", + "neutral": "its" + } + } + }, + "mine": { + "possessive pronoun": { + "3rd person": { + "male": "his", + "female": "hers", + "neutral": "theirs", # colloqial, + } + } + }, + "myself": { + "reflexive_pronoun": { + "3rd person": { + "male": "himself", + "female": "herself", + "neutral": "itself", + "plural": "themselves", + } + } + }, + "you": { + "subject pronoun": { + "3rd person": { + "male": "he", + "female": "she", + "neutral": "it", + "plural": "they", + } + }, + "object pronoun": { + "3rd person": { + "male": "him", + "female": "her", + "neutral": "it", + "plural": "them", + } + } + }, + "your": { + "possessive adjective": { + "3rd person": { + "male": "his", + "female": "her", + "neutral": "its", + "plural": "their", + } + } + }, + "yours": { + "possessive pronoun": { + "3rd person": { + "male": "his", + "female": "hers", + "neutral": "theirs", # colloqial + "plural": "theirs" + } + } + }, + "yourself": { + "reflexive_pronoun": { + "3rd person": { + "male": "himself", + "female": "herself", + "neutral": "itself", + } + } + }, + "we": { + "subject pronoun": { + "3rd person": { + "plural": "they" + } + } + }, + "us": { + "object pronoun": { + "3rd person": { + "plural": "them" + } + } + }, + "our": { + "possessive adjective": { + "3rd person": { + "plural": "their" + } + } + }, + "ours": { + "possessive pronoun": { + "3rd person": { + "plural": "theirs" + } + } + }, + "ourselves": { + "reflexive pronoun": { + "3rd person": { + "plural": "themselves" + } + } + }, + "ours": { + "possessive pronoun": { + "3rd person": { + "plural": "theirs" + } + } + }, + "ourselves": { + "reflexive pronoun": { + "3rd person": { + "plural": "themselves" + } + } + }, + "yourselves": { + "reflexive_pronoun": { + "3rd person": { + "plural": "themselves" + } + } + }, + # 3rd person to 1st/second person mappings + "he": { + "subject pronoun": { + "1st person": { + "neutral": "I", + "plural": "we" # pluralis majestatis + }, + "2nd person": { + "neutral": "you", + "plural": "you" # pluralis majestatis + } + } + }, + "him": { + "object pronoun": { + "1st person": { + "neutral": "me", + "plural": "us" # pluralis majestatis + }, + "2nd person": { + "neutral": "you", + "plural": "you" # pluralis majestatis + }, + } + }, + "his": { + "possessive adjective": { + "1st person": { + "neutral": "my", + "plural": "our" # pluralis majestatis + }, + "2nd person": { + "neutral": "your", + "plural": "your" # pluralis majestatis + } + }, + "possessive pronoun": { + "1st person": { + "neutral": "mine", + "plural": "ours" # pluralis majestatis + }, + "2nd person": { + "neutral": "yours", + "plural": "yours" # pluralis majestatis + } + } + }, + "himself": { + "reflexive pronoun": { + "1st person": { + "neutral": "myself", + "plural": "ourselves" # pluralis majestatis + }, + "2nd person": { + "neutral": "yours", + "plural": "yours" # pluralis majestatis + } + }, + }, + "she": { + "subject pronoun": { + "1st person": { + "neutral": "I", + "plural": "you" # pluralis majestatis + }, + "2nd person": { + "neutral": "you", + "plural": "we" # pluralis majestatis + } + } + }, + "her": { + "object pronoun": { + "1st person": { + "neutral": "me", + "plural": "us" # pluralis majestatis + }, + "2nd person": { + "neutral": "you", + "plural": "you" # pluralis majestatis + } + }, + "possessive adjective": { + "1st person": { + "neutral": "my", + "plural": "our" # pluralis majestatis + }, + "2nd person": { + "neutral": "your", + "plural": "your" # pluralis majestatis + } + }, + }, + "hers": { + "possessive pronoun": { + "1st person": { + "neutral": "mine", + "plural": "ours" # pluralis majestatis + }, + "2nd person": { + "neutral": "yours", + "plural": "yours" # pluralis majestatis + } + } + }, + "herself": { + "reflexive pronoun": { + "1st person": { + "neutral": "myself", + "plural": "ourselves" # pluralis majestatis + }, + "2nd person": { + "neutral": "yourself", + "plural": "yourselves" # pluralis majestatis + } + }, + }, + "it": { + "subject pronoun": { + "1st person": { + "neutral": "I", + "plural": "we" # pluralis majestatis + }, + "2nd person": { + "neutral": "you", + "plural": "you" # pluralis majestatis + } + }, + "object pronoun": { + "1st person": { + "neutral": "me", + "plural": "us" # pluralis majestatis + }, + "2nd person": { + "neutral": "you", + "plural": "you" # pluralis majestatis + } + } + }, + "its": { + "possessive adjective": { + "1st person": { + "neutral": "my", + "plural": "our" # pluralis majestatis + }, + "2nd person": { + "neutral": "your", + "plural": "your" # pluralis majestatis + } + } + }, + "theirs": { + "possessive pronoun": { + "1st person": { + "neutral": "mine", + "plural": "ours" # pluralis majestatis + }, + "2nd person": { + "neutral": "yours", + "plural": "yours" # pluralis majestatis + } + } + }, + "itself": { + "reflexive pronoun": { + "1st person": { + "neutral": "myself", + "plural": "ourselves" # pluralis majestatis + }, + "2nd person": { + "neutral": "yourself", + "plural": "yourselves" # pluralis majestatis + } + }, + }, + "they": { + "subject pronoun": { + "1st person": { + "plural": "we", + }, + "2nd person": { + "plural": "you", + } + } + }, + "them": { + "object pronoun": { + "1st person": { + "plural": "us", + }, + "2nd person": { + "plural": "you", + } + } + }, + "their": { + "possessive adjective": { + "1st person": { + "plural": "our", + }, + "2nd person": { + "plural": "your", + } + } + }, + "themselves": { + "reflexive pronoun": { + "1st person": { + "plural": "ourselves", + }, + "2nd person": { + "plural": "yourselves", + } + } + } +} + + +ALIASES = { + "m": "male", + "f": "female", + "n": "neutral", + "p": "plural", + "1st": "1st person", + "2nd": "2nd person", + "3rd": "3rd person", + "1": "1st person", + "2": "2nd person", + "3": "3rd person", + "s": "subject pronoun", + "sp": "subject pronoun", + "subject": "subject pronoun", + "op": "object pronoun", + "object": "object pronoun", + "pa": "possessive adjective", + "pp": "possessive pronoun", +} + +PRONOUN_TYPES = ["subject pronoun", "object pronoun", "possessive adjective", + "possessive pronoun", "reflexive pronoun"] +VIEWPOINTS = ["1st person", "2nd person", "3rd person"] +GENDERS = ["male", "female", "neutral", "plural"] # including plural as a gender for simplicity + + +def pronoun_to_viewpoints(pronoun, + options=None, pronoun_type="object_pronoun", + gender="neutral", viewpoint="2nd person"): + """ + Access function for determining the forms of a pronount from different viewpoints. + + Args: + pronoun (str): A valid English pronoun, such as 'you', 'his', 'themselves' etc. + options (str or list, optional): A list or space-separated string of options to help + the engine when there is no unique mapping to use. This could for example + be "2nd female" (alias 'f') or "possessive adjective" (alias 'pa' or 'a'). + pronoun_type (str, optional): An explicit object pronoun to separate cases where + there is no unique mapping. Pronoun types defined in `options` take precedence. + Values are + + - `subject pronoun`/`subject`/`sp` (I, you, he, they) + - `object pronoun`/`object/`/`op` (me, you, him, them) + - `possessive adjective`/`adjective`/`pa` (my, your, his, their) + - `possessive pronoun`/`pronoun`/`pp` (mine, yours, his, theirs) + + gender (str, optional): Specific gender to use (plural counts a gender for this purpose). + A gender specified in `options` takes precedence. Values and aliases are: + + - `male`/`m` + - `female`/`f` + - `neutral`/`n` + - `plural`/`p` + + viewpoint (str, optional): A specified viewpoint of the one talking, to use + when there is no unique mapping. A viewpoint given in `options` take + precedence. Values and aliases are: + + - `1st person`/`1st`/`1` + - `2nd person`/`2nd`/`2` + - `3rd person`/`3rd`/`3` + + Returns: + tuple: A tuple `(1st/2nd_person_pronoun, 3rd_person_pronoun)` to show to the one sending the + string and others respectively. If pronoun is invalid, the word is returned verbatim. + + Note: + The capitalization of the original word will be retained. + + """ + if not pronoun: + return pronoun + + pronoun_lower = "I" if pronoun == "I" else pronoun.lower() + + if pronoun_lower not in PRONOUN_MAPPING: + return pronoun + + # differentiators + + if pronoun_type not in PRONOUN_TYPES: + pronoun_type = DEFAULT_PRONOUN_TYPE + if viewpoint not in VIEWPOINTS: + viewpoint = DEFAULT_VIEWPOINT + if gender not in GENDERS: + gender = DEFAULT_GENDER + + if options: + # option string/list will override the kwargs differentiators given + if isinstance(options, str): + options = options.split() + options = [str(part).strip().lower() for part in options] + options = [ALIASES.get(opt, opt) for opt in options] + + for opt in options: + if opt in PRONOUN_TYPES: + pronoun_type = opt + elif opt in VIEWPOINTS: + viewpoint = opt + elif opt in GENDERS: + gender = opt + + # step down into the mapping, using differentiators as needed + pronoun_types = PRONOUN_MAPPING[pronoun_lower] + # this has one or more pronoun-types + if len(pronoun_types) == 1: + pronoun_type, viewpoints = next(iter(pronoun_types.items())) + elif pronoun_type in pronoun_types: + viewpoints = pronoun_types[pronoun_type] + elif DEFAULT_PRONOUN_TYPE in pronoun_types: + pronoun_type = DEFAULT_PRONOUN_TYPE + viewpoints = pronoun_types[pronoun_type] + else: + # not enough info - grab the first of the mappings + pronoun_type, viewpoints = next(iter(pronoun_types.items())) + + # we have one or more viewpoints at this point + if len(viewpoints) == 1: + viewpoint, genders = next(iter(viewpoints.items())) + elif viewpoint in viewpoints: + genders = viewpoints[viewpoint] + elif DEFAULT_VIEWPOINT in viewpoints: + viewpoint = DEFAULT_VIEWPOINT + genders = viewpoints[viewpoint] + else: + # not enough info - grab first of mappings + viewpoint, genders = next(iter(viewpoints.items())) + + # we have one or more possible genders (including plural forms) + if len(genders) == 1: + gender, mapped_pronoun = next(iter(genders.items())) + elif gender in genders: + mapped_pronoun = genders[gender] + elif DEFAULT_GENDER in genders: + gender = DEFAULT_GENDER + mapped_pronoun = genders[gender] + else: + # not enough info - grab first mapping + gender, mapped_pronoun = next(iter(genders.items())) + + # keep the same capitalization as the original + if pronoun != "I": + # don't remap I, since this is always capitalized. + mapped_pronoun = copy_word_case(pronoun, mapped_pronoun) + if mapped_pronoun == "i": + mapped_pronoun = mapped_pronoun.upper() + + if viewpoint == "3rd person": + # the remapped viewpoing is in 3rd person, meaning the ingoing viewpoing + # must have been 1st or 2nd person. + return pronoun, mapped_pronoun + else: + # the remapped viewpoint is 1st or 2nd person, so ingoing must have been + # in 3rd person form. + return mapped_pronoun, pronoun diff --git a/evennia/utils/verb_conjugation/tests.py b/evennia/utils/verb_conjugation/tests.py index abc0225e2a..39d10a977c 100644 --- a/evennia/utils/verb_conjugation/tests.py +++ b/evennia/utils/verb_conjugation/tests.py @@ -5,7 +5,7 @@ Unit tests for verb conjugation. from parameterized import parameterized from django.test import TestCase -from . import conjugate +from . import conjugate, pronouns class TestVerbConjugate(TestCase): @@ -239,3 +239,49 @@ class TestVerbConjugate(TestCase): """ self.assertEqual(expected, conjugate.verb_actor_stance_components(verb)) + + +class TestPronounMapping(TestCase): + """ + Test pronoun viewpoint mapping + + """ + + @parameterized.expand([ + ("you", "m", "you", "he"), + ("you", "f op", "you", "her"), + ("I", "", "I", "it"), + ("I", "p", "I", "it"), # plural is invalid + ("I", "m", "I", "he"), + ("Me", "n", "Me", "It"), + ("your", "p", "your", "their"), + ("ours", "", "ours", "theirs"), + ("yourself", "", "yourself", "itself"), + ("yourself", "m", "yourself", "himself"), + ("yourself", "f", "yourself", "herself"), + ("yourself", "p", "yourself", "itself"), # plural is invalid + ("yourselves", "", "yourselves", "themselves"), + ("he", "", "you", "he"), # assume 2nd person + ("he", "1", "I", "he"), + ("he", "1 p", "we", "he"), + ("her", "p", "you", "her"), + ("her", "pa", "your", "her"), + ("their", "pa", "your", "their"), + ("their", "pa", "your", "their"), + ("itself", "", "yourself", "itself"), + ("themselves", "", "yourselves", "themselves"), + ("herself", "", "yourself", "herself"), + ]) + def test_mapping_with_options(self, pronoun, options, + expected_1st_or_2nd_person, + expected_3rd_person): + """ + Test the pronoun mapper. + + """ + received_1st_or_2nd_person, received_3rd_person = ( + pronouns.pronoun_to_viewpoints(pronoun, options) + ) + + self.assertEqual(expected_1st_or_2nd_person, received_1st_or_2nd_person) + self.assertEqual(expected_3rd_person, received_3rd_person)