Skip to content

Instantly share code, notes, and snippets.

@Aerodos12
Created July 29, 2019 22:02
Show Gist options
  • Save Aerodos12/5d0271b2464506d82425de0c1737636e to your computer and use it in GitHub Desktop.
Save Aerodos12/5d0271b2464506d82425de0c1737636e to your computer and use it in GitHub Desktop.
Battleship Stuff
"""
Account
The Account represents the game "account" and each login has only one
Account object. An Account is what chats on default channels but has no
other in-game-world existence. Rather the Account puppets Objects (such
as Characters) in order to actually participate in the game world.
Guest
Guest accounts are simple low-level accounts that are created/deleted
on the fly and allows users to test the game without the commitment
of a full registration. Guest accounts are deactivated by default; to
activate them, add the following line to your settings file:
GUEST_ENABLED = True
You will also need to modify the connection screen to reflect the
possibility to connect with a guest account. The setting file accepts
several more options for customizing the Guest account system.
"""
from evennia import DefaultAccount, DefaultGuest
class Admiralty(DefaultAccount):
"""
This class describes the actual OOC account (i.e. the user connecting
to the MUD). It does NOT have visual appearance in the game world (that
is handled by the character which is connected to this). Comm channels
are attended/joined using this object.
It can be useful e.g. for storing configuration options for your game, but
should generally not hold any character-related info (that's best handled
on the character level).
Can be set using BASE_ACCOUNT_TYPECLASS.
* available properties
key (string) - name of account
name (string)- wrapper for user.username
aliases (list of strings) - aliases to the object. Will be saved to database as AliasDB entries but returned as strings.
dbref (int, read-only) - unique #id-number. Also "id" can be used.
date_created (string) - time stamp of object creation
permissions (list of strings) - list of permission strings
user (User, read-only) - django User authorization object
obj (Object) - game object controlled by account. 'character' can also be used.
sessions (list of Sessions) - sessions connected to this account
is_superuser (bool, read-only) - if the connected user is a superuser
* Handlers
locks - lock-handler: use locks.add() to add new lock strings
db - attribute-handler: store/retrieve database attributes on this self.db.myattr=val, val=self.db.myattr
ndb - non-persistent attribute handler: same as db but does not create a database entry when storing data
scripts - script-handler. Add new scripts to object with scripts.add()
cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object
nicks - nick-handler. New nicks with nicks.add().
* Helper methods
msg(text=None, **kwargs)
execute_cmd(raw_string, session=None)
search(ostring, global_search=False, attribute_name=None, use_nicks=False, location=None, ignore_errors=False, account=False)
is_typeclass(typeclass, exact=False)
swap_typeclass(new_typeclass, clean_attributes=False, no_default=True)
access(accessing_obj, access_type='read', default=False)
check_permstring(permstring)
* Hook methods (when re-implementation, remember methods need to have self as first arg)
basetype_setup()
at_account_creation()
- note that the following hooks are also found on Objects and are
usually handled on the character level:
at_init()
at_cmdset_get(**kwargs)
at_first_login()
at_post_login(session=None)
at_disconnect()
at_message_receive()
at_message_send()
at_server_reload()
at_server_shutdown()
"""
pass
class Guest(DefaultGuest):
"""
This class is used for guest logins. Unlike Accounts, Guests and their
characters are deleted after disconnection.
"""
pass
"""
Characters
Characters are (by default) Objects setup to be puppeted by Accounts.
They are what you "see" in game. The Character class in this module
is setup to be the "default" character type created by the default
creation commands.
"""
from evennia import DefaultCharacter
from evennia.utils import lazy_property
from world.traits import TraitHandler
class Fleet(DefaultCharacter):
"""
Base class for a group of ships in-game (fleets) and for characters.
The Character defaults to reimplementing some of base Object's hook methods with the
following functionality:
at_basetype_setup - always assigns the DefaultCmdSet to this object type
(important!)sets locks so character cannot be picked up
and its commands only be called by itself, not anyone else.
(to change things, use at_object_creation() instead).
at_after_move(source_location) - Launches the "look" command after every move.
at_post_unpuppet(account) - when Account disconnects from the Character, we
store the current location in the pre_logout_location Attribute and
move it to a None-location so the "unpuppeted" character
object does not need to stay on grid. Echoes "Account has disconnected"
to the room.
at_pre_puppet - Just before Account re-connects, retrieves the character's
pre_logout_location Attribute and move it back on the grid.
at_post_puppet - Echoes "AccountName has entered the game" to the room.
"""
def at_object_creation(self):
self.cmdset.add("commands.fleet_creation.FleetCreationCmdSet",permanent=True)
self.db.ship_classes = {
"battleship": "war",
"patrol_boat":"patrol",
}
def is_shipclass_valid(self,ship_class):
"""
Checks if a ship's class is recognized by a fleet (the current fleet).
"""
return ship_class in self.ship_classes
@property
def ship_classes(self):
return self.db.ship_classes
@lazy_property
def traits(self):
return TraitHandler(self,db_attribute="stats")
"""
Exits
Exits are connectors between Rooms. An exit always has a destination property
set and has a single command defined on itself with the same name as its key,
for allowing Characters to traverse the exit to its destination.
"""
from evennia import DefaultExit
class Exit(DefaultExit):
"""
Exits are connectors between rooms. Exits are normal Objects except
they defines the `destination` property. It also does work in the
following methods:
basetype_setup() - sets default exit locks (to change, use `at_object_creation` instead).
at_cmdset_get(**kwargs) - this is called when the cmdset is accessed and should
rebuild the Exit cmdset along with a command matching the name
of the Exit object. Conventionally, a kwarg `force_init`
should force a rebuild of the cmdset, this is triggered
by the `@alias` command when aliases are changed.
at_failed_traverse() - gives a default error message ("You cannot
go there") if exit traversal fails and an
attribute `err_traverse` is not defined.
Relevant hooks to overload (compared to other types of Objects):
at_traverse(traveller, target_loc) - called to do the actual traversal and calling of the other hooks.
If overloading this, consider using super() to use the default
movement implementation (and hook-calling).
at_after_traverse(traveller, source_loc) - called by at_traverse just after traversing.
at_failed_traverse(traveller) - called by at_traverse if traversal failed for some reason. Will
not be called if the attribute `err_traverse` is
defined, in which case that will simply be echoed.
"""
pass
"""
NavalObject
The NavalObject is the "naked" base class for things in the Titanium Armada game world.
"""
from evennia import DefaultObject
from evennia.utils import lazy_property
from world.traits import TraitHandler
class NavalObject(DefaultObject):
"""
This is the root typeclass object for Titanium Armada.
The BaseObject class implements several hooks tying into the game
engine. By re-implementing these hooks you can control the
system. You should never need to re-implement special Python
methods, such as __init__ and especially never __getattribute__ and
__setattr__ since these are used heavily by the typeclass system
of Evennia and messing with them might well break things for you.
* Base properties defined/available on all Objects
key (string) - name of object
name (string)- same as key
aliases (list of strings) - aliases to the object. Will be saved to
database as AliasDB entries but returned as strings.
dbref (int, read-only) - unique #id-number. Also "id" can be used.
back to this class
date_created (string) - time stamp of object creation
permissions (list of strings) - list of permission strings
account (Account) - controlling account (if any, only set together with
sessid below)
sessid (int, read-only) - session id (if any, only set together with
account above). Use `sessions` handler to get the
Sessions directly.
location (Object) - current location. Is None if this is a room
home (Object) - safety start-location
sessions (list of Sessions, read-only) - returns all sessions connected
to this object
has_account (bool, read-only)- will only return *connected* accounts
contents (list of Objects, read-only) - returns all objects inside this
object (including exits)
exits (list of Objects, read-only) - returns all exits from this
object, if any
destination (Object) - only set if this object is an exit.
is_superuser (bool, read-only) - True/False if this user is a superuser
* Handlers available
locks - lock-handler: use locks.add() to add new lock strings
db - attribute-handler: store/retrieve database attributes on this
self.db.myattr=val, val=self.db.myattr
ndb - non-persistent attribute handler: same as db but does not create
a database entry when storing data
scripts - script-handler. Add new scripts to object with scripts.add()
cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object
nicks - nick-handler. New nicks with nicks.add().
sessions - sessions-handler. Get Sessions connected to this
object with sessions.get()
* Helper methods (see src.objects.objects.py for full headers)
search(ostring, global_search=False, attribute_name=None,
use_nicks=False, location=None, ignore_errors=False, account=False)
execute_cmd(raw_string)
msg(text=None, **kwargs)
msg_contents(message, exclude=None, from_obj=None, **kwargs)
move_to(destination, quiet=False, emit_to_obj=None, use_destination=True)
copy(new_key=None)
delete()
is_typeclass(typeclass, exact=False)
swap_typeclass(new_typeclass, clean_attributes=False, no_default=True)
access(accessing_obj, access_type='read', default=False)
check_permstring(permstring)
* Hooks (these are class methods, so args should start with self):
basetype_setup() - only called once, used for behind-the-scenes
setup. Normally not modified.
basetype_posthook_setup() - customization in basetype, after the object
has been created; Normally not modified.
at_object_creation() - only called once, when object is first created.
Object customizations go here.
at_object_delete() - called just before deleting an object. If returning
False, deletion is aborted. Note that all objects
inside a deleted object are automatically moved
to their <home>, they don't need to be removed here.
at_init() - called whenever typeclass is cached from memory,
at least once every server restart/reload
at_cmdset_get(**kwargs) - this is called just before the command handler
requests a cmdset from this object. The kwargs are
not normally used unless the cmdset is created
dynamically (see e.g. Exits).
at_pre_puppet(account)- (account-controlled objects only) called just
before puppeting
at_post_puppet() - (account-controlled objects only) called just
after completing connection account<->object
at_pre_unpuppet() - (account-controlled objects only) called just
before un-puppeting
at_post_unpuppet(account) - (account-controlled objects only) called just
after disconnecting account<->object link
at_server_reload() - called before server is reloaded
at_server_shutdown() - called just before server is fully shut down
at_access(result, accessing_obj, access_type) - called with the result
of a lock access check on this object. Return value
does not affect check result.
at_before_move(destination) - called just before moving object
to the destination. If returns False, move is cancelled.
announce_move_from(destination) - called in old location, just
before move, if obj.move_to() has quiet=False
announce_move_to(source_location) - called in new location, just
after move, if obj.move_to() has quiet=False
at_after_move(source_location) - always called after a move has
been successfully performed.
at_object_leave(obj, target_location) - called when an object leaves
this object in any fashion
at_object_receive(obj, source_location) - called when this object receives
another object
at_traverse(traversing_object, source_loc) - (exit-objects only)
handles all moving across the exit, including
calling the other exit hooks. Use super() to retain
the default functionality.
at_after_traverse(traversing_object, source_location) - (exit-objects only)
called just after a traversal has happened.
at_failed_traverse(traversing_object) - (exit-objects only) called if
traversal fails and property err_traverse is not defined.
at_msg_receive(self, msg, from_obj=None, **kwargs) - called when a message
(via self.msg()) is sent to this obj.
If returns false, aborts send.
at_msg_send(self, msg, to_obj=None, **kwargs) - called when this objects
sends a message to someone via self.msg().
return_appearance(looker) - describes this object. Used by "look"
command by default
at_desc(looker=None) - called by 'look' whenever the
appearance is requested.
at_get(getter) - called after object has been picked up.
Does not stop pickup.
at_drop(dropper) - called when this object has been dropped.
at_say(speaker, message) - by default, called if an object inside this
object speaks
"""
@lazy_property
def traits(self):
return TraitHandler(self,db_attribute="stats")
class Ship(NavalObject):
"""
Ship
NavalObject class for defining ships (the building blocks of fleets)
"""
ship_type = 0
secondary_parts = 1
ship_tag = None
def at_object_creation(self):
self.db.ship_title = "Untitled Ship"
self.db.ship_parts = []
self.create_ship_parts()
def create_ship_parts(self):
self.traits.add("ship_health","Ship Health",type="gauge",base=100,min=0)
def is_part_alive(self, part_index=0):
"""
Indicates whether or not the given part (indicated by index) is operational.
Parameters
----------
part_index : int, optional
the index in which the ShipSegment is stored in the parts list.
Returns
---------
bool
Whether or not the part is alive (operational).
"""
if self.ship_parts[part_index] != None:
return self.ship_parts[part_index].is_alive()
else:
return False
@property
def title(self):
return self.attributes.get("ship_title")
@title.setter
def title(self,title_new):
self.attributes.set("ship_title",title_new)
@property
def num_parts(self):
return self.num_parts + 1
class PatrolBoat(Ship):
"""
Patrol Boat
A two part ship, this boat is one of the smallest ships in the
game.
"""
ship_type = 1
secondary_parts = 1
ship_tag = "patrol_boat"
"""
Bodies of water
Water Bodies are simple containers that has no location of their own (are vast and can host ships).
"""
from evennia import DefaultRoom
class WaterBody(DefaultRoom):
"""
Bodies of water (WaterBody) are Rooms in which act as a
massive naval map or a series of maps.
See examples/object.py for a list of
properties and methods available on all Objects.
"""
combat_size_x = 5
combat_size_y = 5
@property
def size(self):
return (self.combat_size_x + 1, self.combat_size_y + 1)
class Sea(WaterBody):
"""
A 16 by 16 WaterBody that is bigger than a lake, but smaller than an Ocean.
"""
combat_size_x = 16
combat_size_y = 16
"""
Scripts
Scripts are powerful jacks-of-all-trades. They have no in-game
existence and can be used to represent persistent game systems in some
circumstances. Scripts can also have a time component that allows them
to "fire" regularly or a limited number of times.
There is generally no "tree" of Scripts inheriting from each other.
Rather, each script tends to inherit from the base Script class and
just overloads its hooks to have it perform its function.
"""
from evennia import DefaultScript
class Script(DefaultScript):
"""
A script type is customized by redefining some or all of its hook
methods and variables.
* available properties
key (string) - name of object
name (string)- same as key
aliases (list of strings) - aliases to the object. Will be saved
to database as AliasDB entries but returned as strings.
dbref (int, read-only) - unique #id-number. Also "id" can be used.
date_created (string) - time stamp of object creation
permissions (list of strings) - list of permission strings
desc (string) - optional description of script, shown in listings
obj (Object) - optional object that this script is connected to
and acts on (set automatically by obj.scripts.add())
interval (int) - how often script should run, in seconds. <0 turns
off ticker
start_delay (bool) - if the script should start repeating right away or
wait self.interval seconds
repeats (int) - how many times the script should repeat before
stopping. 0 means infinite repeats
persistent (bool) - if script should survive a server shutdown or not
is_active (bool) - if script is currently running
* Handlers
locks - lock-handler: use locks.add() to add new lock strings
db - attribute-handler: store/retrieve database attributes on this
self.db.myattr=val, val=self.db.myattr
ndb - non-persistent attribute handler: same as db but does not
create a database entry when storing data
* Helper methods
start() - start script (this usually happens automatically at creation
and obj.script.add() etc)
stop() - stop script, and delete it
pause() - put the script on hold, until unpause() is called. If script
is persistent, the pause state will survive a shutdown.
unpause() - restart a previously paused script. The script will continue
from the paused timer (but at_start() will be called).
time_until_next_repeat() - if a timed script (interval>0), returns time
until next tick
* Hook methods (should also include self as the first argument):
at_script_creation() - called only once, when an object of this
class is first created.
is_valid() - is called to check if the script is valid to be running
at the current time. If is_valid() returns False, the running
script is stopped and removed from the game. You can use this
to check state changes (i.e. an script tracking some combat
stats at regular intervals is only valid to run while there is
actual combat going on).
at_start() - Called every time the script is started, which for persistent
scripts is at least once every server start. Note that this is
unaffected by self.delay_start, which only delays the first
call to at_repeat().
at_repeat() - Called every self.interval seconds. It will be called
immediately upon launch unless self.delay_start is True, which
will delay the first call of this method by self.interval
seconds. If self.interval==0, this method will never
be called.
at_stop() - Called as the script object is stopped and is about to be
removed from the game, e.g. because is_valid() returned False.
at_server_reload() - Called when server reloads. Can be used to
save temporary variables you want should survive a reload.
at_server_shutdown() - called at a full server shutdown.
"""
pass
# -*- coding: utf-8 -*-
"""
Traits Module
`Trait` classes represent modifiable traits on objects or characters. They
are instantiated by a `TraitHandler` object, which is typically set up
as a property on the object or character's typeclass.
**Setup**
To use traits on an object, add a function that passes the object
itself into the constructor and returns a `TraitHandler`. This function
should be decorated with the `lazy_property` decorator.
If desired, multiple `TraitHandler` properties can be defined on one
object. The optional `db_attribute` argument should be used to specify
a different storage key for each `TraitHandler`. The default is `traits`.
Example:
```python
from evennia.utils import lazy_property
from world.traits import TraitHandler
...
class Object(DefaultObject):
...
@lazy_property
def traits(self):
return TraitHandler(self)
```
**Trait Configuration**
`Trait` objects can be configured as one of three basic types with
increasingly complex behavior.
* Static - A simple trait model with a base value and optional modifier.
* Counter - Trait with a base value and a modifiable current value that
can vary along a range defined by optional min and max values.
* Gauge - Modified counter type modeling a refillable "gauge".
All traits have a read-only `actual` property that will report the trait's
actual value.
Example:
```python
>>> hp = obj.traits.hp
>>> hp.actual
100
```
They also support storing arbitrary data via either dictionary key or
attribute syntax. Storage of arbitrary data in this way has the same
constraints as any nested collection type stored in a persistent Evennia
Attribute, so it is best to avoid attempting to store complex objects.
Static Trait Configuration
A static `Trait` stores a `base` value and a `mod` modifier value.
The trait's actual value is equal to `base`+`mod`.
Static traits can be used to model many different stats, such as
Strength, Character Level, or Defense Rating in many tabletop gaming
systems.
Constructor Args:
name (str): name of the trait
type (str): 'static' for static traits
base (int, float): base value of the trait
mod Optional(int): modifier value
extra Optional(dict): keys of this dict are accessible on the
`Trait` object as attributes or dict keys
Properties:
actual (int, float): returns the value of `mod`+`base` properties
extra (list[str]): list of keys stored in the extra data dict
Methods:
reset_mod(): sets the value of the `mod` property to zero
Examples:
'''python
>>> strength = char.traits.str
>>> strength.actual
5
>>> strength.mod = 2 # add a bonus to strength
>>> str(strength)
'Strength 7 (+2)'
>>> strength.reset_mod() # clear bonuses
>>> str(strength)
'Strength 5 (+0)'
>>> strength.newkey = 'newvalue'
>>> strength.extra
['newkey']
>>> strength
Trait({'name': 'Strength', 'type': 'trait', 'base': 5, 'mod': 0,
'min': None, 'max': None, 'extra': {'newkey': 'newvalue'}})
```
Counter Trait Configuration
Counter type `Trait` objects have a `base` value similar to static
traits, but adds a `current` value and a range along which it may
vary. Modifier values are applied to this `current` value instead
of `base` when determining the `actual` value. The `current` can
also be reset to its `base` value by calling the `reset_counter()`
method.
Counter style traits are best used to represent game traits such as
carrying weight, alignment points, a money system, or bonus/penalty
counters.
Constructor Args:
(all keys listed above for 'static', plus:)
min Optional(int, float, None): default None
minimum allowable value for current; unbounded if None
max Optional(int, float, None): default None
maximum allowable value for current; unbounded if None
Properties:
actual (int, float): returns the value of `mod`+`current` properties
Methods:
reset_counter(): resets `current` equal to the value of `base`
Examples:
```python
>>> carry = caller.traits.carry
>>> str(carry)
'Carry Weight 0 ( +0)'
>>> carry.current -= 3 # try to go negative
>>> carry # enforces zero minimum
'Carry Weight 0 ( +0)'
>>> carry.current += 15
>>> carry
'Carry Weight 15 ( +0)'
>>> carry.mod = -5 # apply a modifier to reduce
>>> carry # apparent weight
'Carry Weight: 10 ( -5)'
>>> carry.current = 10000 # set a semi-large value
>>> carry # still have the modifier
'Carry Weight 9995 ( -5)'
>>> carry.reset() # remove modifier
>>> carry
'Carry Weight 10000 ( +0)'
>>> carry.reset_counter()
>>> +carry
0
```
Gauge Trait Configuration
A gauge type `Trait` is a modified counter trait used to model a
gauge that can be emptied and refilled. The `base` property of a
gauge trait represents its "full" value. The `mod` property increases
or decreases that "full" value, rather than the `current`.
By default gauge type traits have a `min` of zero, and a `max` set
to the `base`+`mod` properties. A gauge will still work if its `max`
property is set to a value above its `base` or to None.
Gauge type traits are best used to represent traits such as health
points, stamina points, or magic points.
Constructor Args:
(all keys listed above for 'static', plus:)
min Optional(int, float, None): default 0
minimum allowable value for current; unbounded if None
max Optional(int, float, None, 'base'): default 'base'
maximum allowable value for current; unbounded if None;
if 'base', returns the value of `base`+`mod`.
Properties:
actual (int, float): returns the value of the `current` property
Methods:
fill_gauge(): adds the value of `base`+`mod` to `current`
percent(): returns the ratio of actual value to max value as
a percentage. if `max` is unbound, return the ratio of
`current` to `base`+`mod` instead.
Examples:
```python
>>> hp = caller.traits.hp
>>> repr(hp)
GaugeTrait({'name': 'HP', 'type': 'gauge', 'base': 10, 'mod': 0,
'min': 0, 'max': 'base', 'current': 10, 'extra': {}})
>>> str(hp)
'HP: 10 / 10 ( +0)'
>>> hp.current -= 6 # take damage
>>> str(hp)
'HP: 4 / 10 ( +0)'
>>> hp.current -= 6 # take damage to below min
>>> str(hp)
'HP: 0 / 10 ( +0)'
>>> hp.fill() # refill trait
>>> str(hp)
'HP: 10 / 10 ( +0)'
>>> hp.current = 15 # try to set above max
>>> str(hp) # disallowed because max=='actual'
'HP: 10 / 10 ( +0)'
>>> hp.mod += 3 # bonus on full trait
>>> str(hp) # buffs flow to current
'HP: 13 / 13 ( +3)'
>>> hp.current -= 5
>>> str(hp)
'HP: 8 / 13 ( +3)'
>>> hp.reset() # remove bonus on reduced trait
>>> str(hp) # debuffs do not affect current
'HP: 8 / 10 ( +0)'
```
"""
from evennia.utils.dbserialize import _SaverDict
from evennia.utils import logger, lazy_property
from functools import total_ordering
TRAIT_TYPES = ('static', 'counter', 'gauge')
RANGE_TRAITS = ('counter', 'gauge')
class TraitException(Exception):
"""Base exception class raised by `Trait` objects.
Args:
msg (str): informative error message
"""
def __init__(self, msg):
self.msg = msg
class TraitHandler(object):
"""Factory class that instantiates Trait objects.
Args:
obj (Object): parent Object typeclass for this TraitHandler
db_attribute (str): name of the DB attribute for trait data storage
"""
def __init__(self, obj, db_attribute='traits'):
if not obj.attributes.has(db_attribute):
obj.attributes.add(db_attribute, {})
self.attr_dict = obj.attributes.get(db_attribute)
self.cache = {}
def __len__(self):
"""Return number of Traits in 'attr_dict'."""
return len(self.attr_dict)
def __setattr__(self, key, value):
"""Returns error message if trait objects are assigned directly."""
if key in ('attr_dict', 'cache'):
super(TraitHandler, self).__setattr__(key, value)
else:
raise TraitException(
"Trait object not settable. Assign one of "
"`{0}.base`, `{0}.mod`, or `{0}.current` ".format(key) +
"properties instead."
)
def __setitem__(self, key, value):
"""Returns error message if trait objects are assigned directly."""
return self.__setattr__(key, value)
def __getattr__(self, trait):
"""Returns Trait instances accessed as attributes."""
return self.get(trait)
def __getitem__(self, trait):
"""Returns `Trait` instances accessed as dict keys."""
return self.get(trait)
def get(self, trait):
"""
Args:
trait (str): key from the traits dict containing config data
for the trait. "all" returns a list of all trait keys.
Returns:
(`Trait` or `None`): named Trait class or None if trait key
is not found in traits collection.
"""
if trait not in self.cache:
if trait not in self.attr_dict:
return None
data = self.attr_dict[trait]
self.cache[trait] = Trait(data)
return self.cache[trait]
def add(self, key, name, type='static',
base=0, mod=0, min=None, max=None, extra={}):
"""Create a new Trait and add it to the handler."""
if key in self.attr_dict:
raise TraitException("Trait '{}' already exists.".format(key))
if type in TRAIT_TYPES:
trait = dict(name=name,
type=type,
base=base,
mod=mod,
extra=extra)
if min:
trait.update(dict(min=min))
if max:
trait.update(dict(max=max))
self.attr_dict[key] = trait
else:
raise TraitException("Invalid trait type specified.")
def remove(self, trait):
"""Remove a Trait from the handler's parent object."""
if trait not in self.attr_dict:
raise TraitException("Trait not found: {}".format(trait))
if trait in self.cache:
del self.cache[trait]
del self.attr_dict[trait]
def clear(self):
"""Remove all Traits from the handler's parent object."""
for trait in self.all:
self.remove(trait)
@property
def all(self):
"""Return a list of all trait keys in this TraitHandler."""
return self.attr_dict.keys()
@total_ordering
class Trait(object):
"""Represents an object or Character trait.
Note:
See module docstring for configuration details.
"""
def __init__(self, data):
if not 'name' in data:
raise TraitException(
"Required key not found in trait data: 'name'")
if not 'type' in data:
raise TraitException(
"Required key not found in trait data: 'type'")
self._type = data['type']
if not 'base' in data:
data['base'] = 0
if not 'mod' in data:
data['mod'] = 0
if not 'extra' in data:
data['extra'] = {}
if 'min' not in data:
data['min'] = 0 if self._type == 'gauge' else None
if 'max' not in data:
data['max'] = 'base' if self._type == 'gauge' else None
self._data = data
self._keys = ('name', 'type', 'base', 'mod',
'current', 'min', 'max', 'extra')
self._locked = True
if not isinstance(data, _SaverDict):
logger.log_warn(
'Non-persistent {} class loaded.'.format(
type(self).__name__
))
def __repr__(self):
"""Debug-friendly representation of this Trait."""
return "{}({{{}}})".format(
type(self).__name__,
', '.join(["'{}': {!r}".format(k, self._data[k])
for k in self._keys if k in self._data]))
def __str__(self):
"""User-friendly string representation of this `Trait`"""
if self._type == 'gauge':
status = "{actual:4} / {base:4}".format(
actual=self.actual,
base=self.base)
else:
status = "{actual:11}".format(actual=self.actual)
return "{name:12} {status} ({mod:+3})".format(
name=self.name,
status=status,
mod=self.mod)
def __unicode__(self):
"""User-friendly unicode representation of this `Trait`"""
return unicode(str(self))
# Extra Properties magic
def __getitem__(self, key):
"""Access extra parameters as dict keys."""
try:
return self.__getattr__(key)
except AttributeError:
raise KeyError(key)
def __setitem__(self, key, value):
"""Set extra parameters as dict keys."""
self.__setattr__(key, value)
def __delitem__(self, key):
"""Delete extra prameters as dict keys."""
self.__delattr__(key)
def __getattr__(self, key):
"""Access extra parameters as attributes."""
if key in self._data['extra']:
return self._data['extra'][key]
else:
raise AttributeError(
"{} '{}' has no attribute {!r}".format(
type(self).__name__, self.name, key
))
def __setattr__(self, key, value):
"""Set extra parameters as attributes.
Arbitrary attributes set on a Trait object will be
stored in the 'extra' key of the `_data` attribute.
This behavior is enabled by setting the instance
variable `_locked` to True.
"""
propobj = getattr(self.__class__, key, None)
if isinstance(propobj, property):
if propobj.fset is None:
raise AttributeError("can't set attribute")
propobj.fset(self, value)
else:
if (self.__dict__.get('_locked', False) and
key not in ('_keys',)):
self._data['extra'][key] = value
else:
super(Trait, self).__setattr__(key, value)
def __delattr__(self, key):
"""Delete extra parameters as attributes."""
if key in self._data['extra']:
del self._data['extra'][key]
# Numeric operations magic
def __eq__(self, other):
"""Support equality comparison between Traits or Trait and numeric.
Note:
This class uses the @functools.total_ordering() decorator to
complete the rich comparison implementation, therefore only
`__eq__` and `__lt__` are implemented.
"""
if type(other) == Trait:
return self.actual == other.actual
elif type(other) in (float, int):
return self.actual == other
else:
return NotImplemented
def __lt__(self, other):
"""Support less than comparison between `Trait`s or `Trait` and numeric."""
if isinstance(other, Trait):
return self.actual < other.actual
elif type(other) in (float, int):
return self.actual < other
else:
return NotImplemented
def __pos__(self):
"""Access `actual` property through unary `+` operator."""
return self.actual
def __add__(self, other):
"""Support addition between `Trait`s or `Trait` and numeric"""
if isinstance(other, Trait):
return self.actual + other.actual
elif type(other) in (float, int):
return self.actual + other
else:
return NotImplemented
def __sub__(self, other):
"""Support subtraction between `Trait`s or `Trait` and numeric"""
if isinstance(other, Trait):
return self.actual - other.actual
elif type(other) in (float, int):
return self.actual - other
else:
return NotImplemented
def __mul__(self, other):
"""Support multiplication between `Trait`s or `Trait` and numeric"""
if isinstance(other, Trait):
return self.actual * other.actual
elif type(other) in (float, int):
return self.actual * other
else:
return NotImplemented
def __floordiv__(self, other):
"""Support floor division between `Trait`s or `Trait` and numeric"""
if isinstance(other, Trait):
return self.actual // other.actual
elif type(other) in (float, int):
return self.actual // other
else:
return NotImplemented
# yay, commutative property!
__radd__ = __add__
__rmul__ = __mul__
def __rsub__(self, other):
"""Support subtraction between `Trait`s or `Trait` and numeric"""
if isinstance(other, Trait):
return other.actual - self.actual
elif type(other) in (float, int):
return other - self.actual
else:
return NotImplemented
def __rfloordiv__(self, other):
"""Support floor division between `Trait`s or `Trait` and numeric"""
if isinstance(other, Trait):
return other.actual // self.actual
elif type(other) in (float, int):
return other // self.actual
else:
return NotImplemented
# Public members
@property
def name(self):
"""Display name for the trait."""
return self._data['name']
@property
def actual(self):
"""The "actual" value of the trait."""
if self._type == 'gauge':
return self.current
elif self._type == 'counter':
return self._mod_current()
else:
return self._mod_base()
@property
def base(self):
"""The trait's base value.
Note:
The setter for this property will enforce any range bounds set
on this `Trait`.
"""
return self._data['base']
@base.setter
def base(self, amount):
if self._data.get('max', None) == 'base':
self._data['base'] = amount
if type(amount) in (int, float):
self._data['base'] = self._enforce_bounds(amount)
@property
def mod(self):
"""The trait's modifier."""
return self._data['mod']
@mod.setter
def mod(self, amount):
if type(amount) in (int, float):
delta = amount - self._data['mod']
self._data['mod'] = amount
if self._type == 'gauge':
if delta >= 0:
# apply increases to current
self.current = self._enforce_bounds(self.current + delta)
else:
# but not decreases, unless current goes out of range
self.current = self._enforce_bounds(self.current)
@property
def min(self):
"""The lower bound of the range."""
if self._type in RANGE_TRAITS:
return self._data['min']
else:
raise AttributeError(
"static 'Trait' object has no attribute 'min'.")
@min.setter
def min(self, amount):
if self._type in RANGE_TRAITS:
if amount is None: self._data['min'] = amount
elif type(amount) in (int, float):
self._data['min'] = amount if amount < self.base else self.base
else:
raise AttributeError(
"static 'Trait' object has no attribute 'min'.")
@property
def max(self):
"""The maximum value of the `Trait`.
Note:
This property may be set to the string literal 'base'.
When set this way, the property returns the value of the
`mod`+`base` properties.
"""
if self._type in RANGE_TRAITS:
if self._data['max'] == 'base':
return self._mod_base()
else:
return self._data['max']
else:
raise AttributeError(
"static 'Trait' object has no attribute 'max'.")
@max.setter
def max(self, value):
if self._type in RANGE_TRAITS:
if value == 'base' or value is None:
self._data['max'] = value
elif type(value) in (int, float):
self._data['max'] = value if value > self.base else self.base
else:
raise AttributeError(
"static 'Trait' object has no attribute 'max'.")
@property
def current(self):
"""The `current` value of the `Trait`."""
if self._type == 'gauge':
return self._data.get('current', self._mod_base())
else:
return self._data.get('current', self.base)
@current.setter
def current(self, value):
if self._type in RANGE_TRAITS:
if type(value) in (int, float):
self._data['current'] = self._enforce_bounds(value)
else:
raise AttributeError(
"'current' property is read-only on static 'Trait'.")
@property
def extra(self):
"""Returns a list containing available extra data keys."""
return self._data['extra'].keys()
def reset_mod(self):
"""Clears any mod value on the `Trait`."""
self.mod = 0
def reset_counter(self):
"""Resets `current` property equal to `base` value."""
self.current = self.base
def fill_gauge(self):
"""Adds the `mod`+`base` to the `current` value.
Note:
Will honor the upper bound if set.
"""
self.current = \
self._enforce_bounds(self.current + self._mod_base())
def percent(self):
"""Returns the value formatted as a percentage."""
if self._type in RANGE_TRAITS:
if self.max:
return "{:3.1f}%".format(self.current * 100.0 / self.max)
elif self._type == 'counter' and self.base != 0:
return "{:3.1f}%".format(self.current * 100.0 / self._mod_base())
elif self._type == 'gauge' and self._mod_base() != 0:
return "{:3.1f}%".format(self.current * 100.0 / self._mod_base())
# if we get to this point, it's either a static trait or
# a divide by zero situation
return "100.0%"
# Private members
def _mod_base(self):
return self._enforce_bounds(self.mod + self.base)
def _mod_current(self):
return self._enforce_bounds(self.mod + self.current)
def _enforce_bounds(self, value):
"""Ensures that incoming value falls within trait's range."""
if self._type in RANGE_TRAITS:
if self.min is not None and value <= self.min:
return self.min
if self._data['max'] == 'base' and value >= self.mod + self.base:
return self.mod + self.base
if self.max is not None and value >= self.max:
return self.max
return value
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment