Add pronoun parsing for msg_contents inlinefuncs

This commit is contained in:
Griatch 2021-10-30 22:36:40 +02:00
parent 9cb807a73c
commit 9d6cb98349
15 changed files with 1063 additions and 24 deletions

View file

@ -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`.

View file

@ -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_)

View file

@ -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=<obj>, receiver=<obj>, mapping={'key': <obj>, ...})
@ -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

View file

@ -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.

View file

@ -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

View file

@ -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
```

View file

@ -0,0 +1,10 @@
```{eval-rst}
evennia.utils.verb\_conjugation.pronouns
===============================================
.. automodule:: evennia.utils.verb_conjugation.pronouns
:members:
:undoc-members:
:show-inheritance:
```

View file

@ -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

View file

@ -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.

View file

@ -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,
}

View file

@ -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(

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)