Resolve merge conflicts with master.
This commit is contained in:
commit
7ff783fea1
21 changed files with 900 additions and 122 deletions
|
|
@ -31,8 +31,6 @@ things you want from here into your game folder and change them there.
|
|||
multiple descriptions for time and season as well as details.
|
||||
* GenderSub (Griatch 2015) - Simple example (only) of storing gender
|
||||
on a character and access it in an emote with a custom marker.
|
||||
* In-game Python (Vincent Le Geoff 2017) - Allow trusted builders to script
|
||||
objects and events using Python from in-game.
|
||||
* Mail (grungies1138 2016) - An in-game mail system for communication.
|
||||
* Menu login (Griatch 2011) - A login system using menus asking
|
||||
for name/password rather than giving them as one command
|
||||
|
|
@ -41,6 +39,8 @@ things you want from here into your game folder and change them there.
|
|||
* Menu Login (Vincent-lg 2016) - Alternate login system using EvMenu.
|
||||
* Multidescer (Griatch 2016) - Advanced descriptions combined from
|
||||
many separate description components, inspired by MUSH.
|
||||
* Random String Generator (Vincent Le Goff 2017) - Simple pseudo-random
|
||||
generator of strings with rules, avoiding repetitions.
|
||||
* RPLanguage (Griatch 2015) - Dynamic obfuscation of emotes when
|
||||
speaking unfamiliar languages. Also obfuscates whispers.
|
||||
* RPSystem (Griatch 2015) - Full director-style emoting system
|
||||
|
|
@ -60,6 +60,8 @@ things you want from here into your game folder and change them there.
|
|||
|
||||
* EGI_Client (gtaylor 2016) - Client for reporting game status
|
||||
to the Evennia game index (games.evennia.com)
|
||||
* In-game Python (Vincent Le Goff 2017) - Allow trusted builders to script
|
||||
objects and events using Python from in-game.
|
||||
* Tutorial examples (Griatch 2011, 2015) - A folder of basic
|
||||
example objects, commands and scripts.
|
||||
* Tutorial world (Griatch 2011, 2015) - A folder containing the
|
||||
|
|
|
|||
|
|
@ -428,7 +428,7 @@ class CmdExtendedDesc(default_cmds.CmdDesc):
|
|||
if self.switches and self.switches[0] in ("spring", "summer", "autumn", "winter"):
|
||||
# a seasonal switch was given
|
||||
if self.rhs:
|
||||
caller.msg("Seasonal descs only works with rooms, not objects.")
|
||||
caller.msg("Seasonal descs only work with rooms, not objects.")
|
||||
return
|
||||
switch = self.switches[0]
|
||||
if not location:
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import traceback
|
|||
|
||||
from django.conf import settings
|
||||
from evennia import DefaultObject, DefaultScript, ChannelDB, ScriptDB
|
||||
from evennia import logger
|
||||
from evennia import logger, ObjectDB
|
||||
from evennia.utils.ansi import raw
|
||||
from evennia.utils.create import create_channel
|
||||
from evennia.utils.dbserialize import dbserialize
|
||||
|
|
@ -101,21 +101,29 @@ class EventHandler(DefaultScript):
|
|||
Return a dictionary of events on this object.
|
||||
|
||||
Args:
|
||||
obj (Object): the connected object.
|
||||
obj (Object or typeclass): the connected object or a general typeclass.
|
||||
|
||||
Returns:
|
||||
A dictionary of the object's events.
|
||||
|
||||
Note:
|
||||
Notes:
|
||||
Events would define what the object can have as
|
||||
callbacks. Note, however, that chained callbacks will not
|
||||
appear in events and are handled separately.
|
||||
|
||||
You can also request the events of a typeclass, not a
|
||||
connected object. This is useful to get the global list
|
||||
of events for a typeclass that has no object yet.
|
||||
|
||||
"""
|
||||
events = {}
|
||||
all_events = self.ndb.events
|
||||
classes = Queue()
|
||||
classes.put(type(obj))
|
||||
if isinstance(obj, type):
|
||||
classes.put(obj)
|
||||
else:
|
||||
classes.put(type(obj))
|
||||
|
||||
invalid = []
|
||||
while not classes.empty():
|
||||
typeclass = classes.get()
|
||||
|
|
|
|||
345
evennia/contrib/random_string_generator.py
Normal file
345
evennia/contrib/random_string_generator.py
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
"""
|
||||
Pseudo-random generator and registry
|
||||
|
||||
Evennia contribution - Vincent Le Goff 2017
|
||||
|
||||
This contrib can be used to generate pseudo-random strings of information
|
||||
with specific criteria. You could, for instance, use it to generate
|
||||
phone numbers, license plate numbers, validation codes, non-sensivite
|
||||
passwords and so on. The strings generated by the generator will be
|
||||
stored and won't be available again in order to avoid repetition.
|
||||
Here's a very simple example:
|
||||
|
||||
```python
|
||||
from evennia.contrib.random_string_generator import RandomStringGenerator
|
||||
# Create a generator for phone numbers
|
||||
phone_generator = RandomStringGenerator("phone number", r"555-[0-9]{3}-[0-9]{4}")
|
||||
# Generate a phone number (555-XXX-XXXX with X as numbers)
|
||||
number = phone_generator.get()
|
||||
# `number` will contain something like: "555-981-2207"
|
||||
# If you call `phone_generator.get`, it won't give the same anymore.phone_generator.all()
|
||||
# Will return a list of all currently-used phone numbers
|
||||
phone_generator.remove("555-981-2207")
|
||||
# The number can be generated again
|
||||
```
|
||||
|
||||
To use it, you will need to:
|
||||
|
||||
1. Import the `RandomStringGenerator` class from the contrib.
|
||||
2. Create an instance of this class taking two arguments:
|
||||
- The name of the gemerator (like "phone number", "license plate"...).
|
||||
- The regular expression representing the expected results.
|
||||
3. Use the generator's `all`, `get` and `remove` methods as shown above.
|
||||
|
||||
To understand how to read and create regular expressions, you can refer to
|
||||
[the documentation on the re module](https://docs.python.org/2/library/re.html).
|
||||
Some examples of regular expressions you could use:
|
||||
|
||||
- `r"555-\d{3}-\d{4}"`: 555, a dash, 3 digits, another dash, 4 digits.
|
||||
- `r"[0-9]{3}[A-Z][0-9]{3}"`: 3 digits, a capital letter, 3 digits.
|
||||
- `r"[A-Za-z0-9]{8,15}"`: between 8 and 15 letters and digits.
|
||||
- ...
|
||||
|
||||
Behind the scenes, a script is created to store the generated information
|
||||
for a single generator. The `RandomStringGenerator` object will also
|
||||
read the regular expression you give to it to see what information is
|
||||
required (letters, digits, a more restricted class, simple characters...)...
|
||||
More complex regular expressions (with branches for instance) might not be
|
||||
available.
|
||||
|
||||
"""
|
||||
|
||||
from random import choice, randint, seed
|
||||
import re
|
||||
import string
|
||||
import time
|
||||
|
||||
from evennia import DefaultScript, ScriptDB
|
||||
from evennia.utils.create import create_script
|
||||
|
||||
class RejectedRegex(RuntimeError):
|
||||
|
||||
"""The provided regular expression has been rejected.
|
||||
|
||||
More details regarding why this error occurred will be provided in
|
||||
the message. The usual reason is the provided regular expression is
|
||||
not specific enough and could lead to inconsistent generating.
|
||||
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ExhaustedGenerator(RuntimeError):
|
||||
|
||||
"""The generator hasn't any available strings to generate anymore."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RandomStringGeneratorScript(DefaultScript):
|
||||
|
||||
"""
|
||||
The global script to hold all generators.
|
||||
|
||||
It will be automatically created the first time `generate` is called
|
||||
on a RandomStringGenerator object.
|
||||
|
||||
"""
|
||||
|
||||
def at_script_creation(self):
|
||||
"""Hook called when the script is created."""
|
||||
self.key = "generator_script"
|
||||
self.desc = "Global generator script"
|
||||
self.persistent = True
|
||||
|
||||
# Permanent data to be stored
|
||||
self.db.generated = {}
|
||||
|
||||
|
||||
class RandomStringGenerator(object):
|
||||
|
||||
"""
|
||||
A generator class to generate pseudo-random strings with a rule.
|
||||
|
||||
The "rule" defining what the generator should provide in terms of
|
||||
string is given as a regular expression when creating instances of
|
||||
this class. You can use the `all` method to get all generated strings,
|
||||
the `get` method to generate a new string, the `remove` method
|
||||
to remove a generated string, or the `clear` method to remove all
|
||||
generated strings.
|
||||
|
||||
Bear in mind, however, that while the generated strings will be
|
||||
stored to avoid repetition, the generator will not concern itself
|
||||
with how the string is stored on the object you use. You probably
|
||||
want to create a tag to mark this object. This is outside of the scope
|
||||
of this class.
|
||||
|
||||
"""
|
||||
|
||||
# We keep the script as a class variable to optimize querying
|
||||
# with multiple instandces
|
||||
script = None
|
||||
|
||||
def __init__(self, name, regex):
|
||||
"""
|
||||
Create a new generator.
|
||||
|
||||
Args:
|
||||
name (str): name of the generator to create.
|
||||
regex (str): regular expression describing the generator.
|
||||
|
||||
Notes:
|
||||
`name` should be an explicit name. If you use more than one
|
||||
generator in your game, be sure to give them different names.
|
||||
This name will be used to store the generated information
|
||||
in the global script, and in case of errors.
|
||||
|
||||
The regular expression should describe the generator, what
|
||||
it should generate: a phone number, a license plate, a password
|
||||
or something else. Regular expressions allow you to use
|
||||
pretty advanced criteria, but be aware that some regular
|
||||
expressions will be rejected if not specific enough.
|
||||
|
||||
Raises:
|
||||
RejectedRegex: the provided regular expression couldn't be
|
||||
accepted as a valid generator description.
|
||||
|
||||
"""
|
||||
self.name = name
|
||||
self.elements = []
|
||||
self.total = 1
|
||||
|
||||
# Analyze the regex if any
|
||||
if regex:
|
||||
self._find_elements(regex)
|
||||
|
||||
def __repr__(self):
|
||||
return "<evennia.contrib.random_string_generator.RandomStringGenerator for {}>".format(self.name)
|
||||
|
||||
def _get_script(self):
|
||||
"""Get or create the script."""
|
||||
if type(self).script:
|
||||
return type(self).script
|
||||
|
||||
try:
|
||||
script = ScriptDB.objects.get(db_key="generator_script")
|
||||
except ScriptDB.DoesNotExist:
|
||||
script = create_script("contrib.random_string_generator.RandomStringGeneratorScript")
|
||||
|
||||
type(self).script = script
|
||||
return script
|
||||
|
||||
def _find_elements(self, regex):
|
||||
"""
|
||||
Find the elements described in the regular expression. This will
|
||||
analyze the provided regular expression and try to find elements.
|
||||
|
||||
Args:
|
||||
regex (str): the regular expression.
|
||||
|
||||
"""
|
||||
self.total = 1
|
||||
self.elements = []
|
||||
tree = re.sre_parse.parse(regex).data
|
||||
# `tree` contains a list of elements in the regular expression
|
||||
for element in tree:
|
||||
# `eleemnt` is also a list, the first element is a string
|
||||
name = element[0]
|
||||
desc = {"min": 1, "max": 1}
|
||||
|
||||
# If `.`, break here
|
||||
if name == "any":
|
||||
raise RejectedRegex("the . definition is too broad, specify what you need more precisely")
|
||||
elif name == "at":
|
||||
# Either the beginning or end, we ignore it
|
||||
continue
|
||||
elif name == "min_repeat":
|
||||
raise RejectedRegex("you have to provide a maximum number of this character class")
|
||||
elif name == "max_repeat":
|
||||
desc["min"] = element[1][0]
|
||||
desc["max"] = element[1][1]
|
||||
desc["chars"] = self._find_literal(element[1][2][0])
|
||||
elif name == "in":
|
||||
desc["chars"] = self._find_literal(element)
|
||||
elif name == "literal":
|
||||
desc["chars"] = self._find_literal(element)
|
||||
else:
|
||||
raise RejectedRegex("unhandled regex syntax:: {}".format(repr(name)))
|
||||
|
||||
self.elements.append(desc)
|
||||
self.total *= len(desc["chars"]) ** desc["max"]
|
||||
|
||||
def _find_literal(self, element):
|
||||
"""Find the literal corresponding to a piece of regular expression."""
|
||||
chars = []
|
||||
if element[0] == "literal":
|
||||
chars.append(chr(element[1]))
|
||||
elif element[0] == "in":
|
||||
negate = False
|
||||
if element[1][0][0] == "negate":
|
||||
negate = True
|
||||
chars = list(string.ascii_letters + string.digits)
|
||||
|
||||
for part in element[1]:
|
||||
if part[0] == "negate":
|
||||
continue
|
||||
|
||||
sublist = self._find_literal(part)
|
||||
for char in sublist:
|
||||
if negate:
|
||||
if char in chars:
|
||||
chars.remove(char)
|
||||
else:
|
||||
chars.append(char)
|
||||
elif element[0] == "range":
|
||||
chars = [chr(i) for i in range(element[1][0], element[1][1] + 1)]
|
||||
elif element[0] == "category":
|
||||
category = element[1]
|
||||
if category == "category_digit":
|
||||
chars = list(string.digits)
|
||||
elif category == "category_word":
|
||||
chars = list(string.letters)
|
||||
else:
|
||||
raise RejectedRegex("unknown category: {}".format(category))
|
||||
else:
|
||||
raise RejectedRegex("cannot find the literal: {}".format(element[0]))
|
||||
|
||||
return chars
|
||||
|
||||
def all(self):
|
||||
"""
|
||||
Return all generated strings for this generator.
|
||||
|
||||
Returns:
|
||||
strings (list of strr): the list of strings that are already
|
||||
used. The strings that were generated first come first in the list.
|
||||
|
||||
"""
|
||||
script = self._get_script()
|
||||
generated = list(script.db.generated.get(self.name, []))
|
||||
return generated
|
||||
|
||||
def get(self, store=True, unique=True):
|
||||
"""
|
||||
Generate a pseudo-random string according to the regular expression.
|
||||
|
||||
Args:
|
||||
store (bool, optional): store the generated string in the script.
|
||||
unique (bool, optional): keep on trying if the string is already used.
|
||||
|
||||
Returns:
|
||||
The newly-generated string.
|
||||
|
||||
Raises:
|
||||
ExhaustedGenerator: if there's no available string in this generator.
|
||||
|
||||
Note:
|
||||
Unless asked explicitly, the returned string can't repeat itself.
|
||||
|
||||
"""
|
||||
script = self._get_script()
|
||||
generated = script.db.generated.get(self.name)
|
||||
if generated is None:
|
||||
script.db.generated[self.name] = []
|
||||
generated = script.db.generated[self.name]
|
||||
|
||||
if len(generated) >= self.total:
|
||||
raise ExhaustedGenerator
|
||||
|
||||
# Generate a pseudo-random string that might be used already
|
||||
result = ""
|
||||
for element in self.elements:
|
||||
number = randint(element["min"], element["max"])
|
||||
chars = element["chars"]
|
||||
for index in range(number):
|
||||
char = choice(chars)
|
||||
result += char
|
||||
|
||||
# If the string has already been generated, try again
|
||||
if result in generated and unique:
|
||||
# Change the random seed, incrementing it slowly
|
||||
epoch = time.time()
|
||||
while result in generated:
|
||||
epoch += 1
|
||||
seed(epoch)
|
||||
result = self.get(store=False, unique=False)
|
||||
|
||||
if store:
|
||||
generated.append(result)
|
||||
|
||||
return result
|
||||
|
||||
def remove(self, element):
|
||||
"""
|
||||
Remove a generated string from the list of stored strings.
|
||||
|
||||
Args:
|
||||
element (str): the string to remove from the list of generated strings.
|
||||
|
||||
Raises:
|
||||
ValueError: the specified value hasn't been generated and is not present.
|
||||
|
||||
Note:
|
||||
The specified string has to be present in the script (so
|
||||
has to have been generated). It will remove this entry
|
||||
from the script, so this string could be generated again by
|
||||
calling the `get` method.
|
||||
|
||||
"""
|
||||
script = self._get_script()
|
||||
generated = script.db.generated.get(self.name, [])
|
||||
if element not in generated:
|
||||
raise ValueError("the string {} isn't stored as generated by the generator {}".format(
|
||||
element, self.name))
|
||||
|
||||
generated.remove(element)
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Clear the generator of all generated strings.
|
||||
|
||||
"""
|
||||
script = self._get_script()
|
||||
generated = script.db.generated.get(self.name, [])
|
||||
generated[:] = []
|
||||
|
|
@ -1046,3 +1046,23 @@ class TestColorMarkup(EvenniaTest):
|
|||
bright_map = color_markups.MUX_COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAP
|
||||
self.assertEqual(bright_map[0][1], '%c[500')
|
||||
self.assertEqual(bright_map[-1][1], '%c[222')
|
||||
|
||||
from evennia.contrib import random_string_generator
|
||||
|
||||
SIMPLE_GENERATOR = random_string_generator.RandomStringGenerator("simple", "[01]{2}")
|
||||
|
||||
class TestRandomStringGenerator(EvenniaTest):
|
||||
|
||||
def test_generate(self):
|
||||
"""Generate and fail when exhausted."""
|
||||
generated = []
|
||||
for i in range(4):
|
||||
generated.append(SIMPLE_GENERATOR.get())
|
||||
|
||||
generated.sort()
|
||||
self.assertEqual(generated, ["00", "01", "10", "11"])
|
||||
|
||||
# At this point, we have generated 4 strings.
|
||||
# We can't generate one more
|
||||
with self.assertRaises(random_string_generator.ExhaustedGenerator):
|
||||
SIMPLE_GENERATOR.get()
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ Customisation example:
|
|||
def at_prepare_room(self, coordinates, caller, room):
|
||||
"Any other changes done to the room before showing it"
|
||||
x, y = coordinates
|
||||
desc = "This is a room in the pyramid.
|
||||
desc = "This is a room in the pyramid."
|
||||
if y == 3 :
|
||||
desc = "You can see far and wide from the top of the pyramid."
|
||||
room.db.desc = desc
|
||||
|
|
@ -157,7 +157,7 @@ def enter_wilderness(obj, coordinates=(0, 0), name="default"):
|
|||
default one
|
||||
|
||||
Returns:
|
||||
bool: True if obj succesfully moved into the wilderness.
|
||||
bool: True if obj successfully moved into the wilderness.
|
||||
"""
|
||||
if not WildernessScript.objects.filter(db_key=name).exists():
|
||||
return False
|
||||
|
|
@ -253,6 +253,11 @@ class WildernessScript(DefaultScript):
|
|||
room.ndb.wildernessscript = self
|
||||
room.ndb.active_coordinates = coordinates
|
||||
for item in self.db.itemcoordinates.keys():
|
||||
# Items deleted from the wilderness leave None type 'ghosts'
|
||||
# that must be cleaned out
|
||||
if item is None:
|
||||
del self.db.itemcoordinates[item]
|
||||
continue
|
||||
item.ndb.wilderness = self
|
||||
|
||||
def is_valid_coordinates(self, coordinates):
|
||||
|
|
@ -298,6 +303,11 @@ class WildernessScript(DefaultScript):
|
|||
"""
|
||||
result = []
|
||||
for item, item_coordinates in self.itemcoordinates.items():
|
||||
# Items deleted from the wilderness leave None type 'ghosts'
|
||||
# that must be cleaned out
|
||||
if item is None:
|
||||
del self.db.itemcoordinates[item]
|
||||
continue
|
||||
if coordinates == item_coordinates:
|
||||
result.append(item)
|
||||
return result
|
||||
|
|
@ -503,7 +513,7 @@ class WildernessRoom(DefaultRoom):
|
|||
moved_obj (Object): The object moved into this one.
|
||||
source_location (Object): Where `moved_obj` came from.
|
||||
"""
|
||||
if moved_obj.destination and moved_obj.destination == moved_obj.location:
|
||||
if isinstance(moved_obj, WildernessExit):
|
||||
# Ignore exits looping back to themselves: those are the regular
|
||||
# n, ne, ... exits.
|
||||
return
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue