In an RPG game I'm developing, after building a base Weapon
class, instead of having many subclasses of Weapon
, I want the weapons to be built upon dictionary parameters. Many of them will be simple static information (weight, value, etc.), but others will have conditionally defined parameters, with not only numeric/textual values: they may have to call external methods, such as spell-like effect, feat, skill, combat maneuvers, etc.).
In this example I'm using strings, like "unholy", to represent what will become method calls (with a given name) at some point.
The number of items (that will share most of this Weapon example's design, once I'm done with it), eventually, should reach the hundreds (the PRD rules I'm adapting to this game already provide a few hundred "basic items"), and allow the player to build new ones, both at runtime based on existing models (crafting) and pre-runtime (modding).
One weapon, for example, should change its stats and associated functions (spell-like effects) when equipped according to the wielder's class. But many more different conditions should be needed/used as I add fancy magic weapons, armors and items.
By managing to add new weapons without having to mess with the class itself, but instead with a dictionary/JSON (once the core code is built, hopefully with some great advice from this community), the user would benefit from accessible code and even the developer would gain in productivity by handling only dictionaries and relying on an existing core logic.
This currently relies on a few setattr
and eval
calls to handle strings from the dictionary. I would like to hear suggestions on preferable approaches, concerning performance, security, etc.. Can it be significantly improved somehow? Readability (always) matters.
# A design test for dynamic class building.
# > attributes and conditional logic are readed from a dictionary.
# This dictionary will actually reside in another file, maybe a json or
# gzipped pickled json...
WEAPONS = {
"bastard's sting": {
"equipped": False,
"magic": 2,
"on_turn_actions": [],
"on_hit_actions": [],
"on_equip": [
{
"type": "check",
"condition": (
"self.owner.is_of_class",
["antipaladin"]
),
True: [
{
"type": "action",
"action": "self.add_on_hit",
"args": ["unholy"]
},
{
"type": "action",
"action": "self.add_on_turn",
"args": ["unholy aurea"]
},
{
"type": "action",
"action": "self.set_attribute",
"args": ["magic", 5]
}
],
False: [
{
"type": "action",
"action": "self.set_attribute",
"args": ["magic", 2]
}
]
}
],
"on_unequip": [
{
"type": "action",
"action": "self.rem_on_hit",
"args": ["unholy"]
},
{
"type": "action",
"action": "self.rem_on_turn",
"args": ["unholy_aurea"]
},
{
"type": "action",
"action": "self.set_attribute",
"args": ["self.magic", 2]
}
]
}
}
class Player:
inventory = []
def __init__(self, _class):
self._class = _class
def pick_up(self, item):
"""Pick an object, put in inventory, set its owner."""
self.inventory.append(item)
item.owner = self
def is_of_class(self, _class):
"""Checks for the Character _class, not a python `class`"""
return self._class == _class
class Weapon:
"""An type of item that can be equipped/used to attack."""
def __init__(self, template):
"""Set the parameters based on a template."""
self.__dict__.update(WEAPONS[template])
def equip(self):
"""Set item status and call its on equip functions."""
self.equipped = True
for action in self.on_equip:
if action['type'] == "check":
self.check(action)
elif action['type'] == "action":
self.action(action)
def unequip(self):
"""Unset item dynamic status, call its on unequip functions."""
self.equipped = False
for action in self.on_unequip:
if action['type'] == "check":
self.check(action)
elif action['type'] == "action":
self.action(action)
def check(self, dic):
"""Check a condition and call an action according to it."""
check_act = eval(dic['condition'][0])
args = dic['condition'][1]
result = check_act(*args)
self.action(*dic[result])
def action(self, *dicts):
"""Perform action with args, both specified on dicts."""
for dic in dicts:
act = eval(dic['action'])
act(dic['args'])
def set_attribute(self, args):
name, value = args
setattr(self, name, value)
def add_on_hit(self, actions):
for action in actions:
if action not in self.on_hit_actions:
self.on_hit_actions.append(action)
def add_on_turn(self, actions):
for action in actions:
if action not in self.on_turn_actions:
self.on_turn_actions.append(action)
def rem_on_hit(self, actions):
for action in actions:
try:
self.on_hit_actions.remove(action)
except ValueError:
# We never had that but unequip tries to clean it anyway.
pass
def rem_on_turn(self, actions):
for action in actions:
try:
self.on_turn_actions.remove(action)
except ValueError:
pass
if __name__ == '__main__':
"""Let's test it!"""
weapon = Weapon("bastard's sting")
player1 = Player("paladin")
player1.pick_up(weapon)
weapon.equip()
print("Enhancement: {}, Hit effects: {}, Other effects: {}".format(
weapon.magic, weapon.on_hit_actions, weapon.on_turn_actions))
weapon.unequip()
player2 = Player("antipaladin")
player2.pick_up(weapon)
weapon.equip()
print("Enhancement: {}, Hit effects: {}, Other effects: {}".format(
weapon.magic, weapon.on_hit_actions, weapon.on_turn_actions))
Explaining the example:
The "bastard's sting" is a weapon that, in the hands of an "antipaladin", acts like an +5 unholy sword, with some fancy passive and active effects; for other classes, however, it behaves like a plain +2 sword without effects (and, in the future, will probably do some nasty stuffs in the hands of the good ones).
It should print:
For the non-antipaladin
Enhancement: 2, Hit effects: [], Other effects: []
For the antipaladin
Enhancement: 5, Hit effects: ['unholy'], Other effects: ['unholy aurea']
Notes:
As the size of the dictionary grows I intend to keep use a bzipped and pickled version of it, created once and updated in case of changes to the JSON/dictionary.
This code does not reflect the actual main structure of the game but tries to illustrate, through a working example, an approach specifically on how could weapons (and other items) be created based on dictionaries in a way that this data could provide conditional verifications and actions to interact dynamically with a huge combinations of race x attributes x features x classes behavior.
What is a weapon here is actually a component of a game object in the main code, and so is the player and other creatures. With that said, any specific comment on the main structure is still appreciated, but the main focus should be in the interaction between the
Weapon
class and the dictionary.The action "unholy area" of the weapon, in this specific example, would affect every neighbor square of the "owner" (I don't update the item position after someone picks it up, only when the item is dropped); other passive effects would do the same (heal the owner every turn, etc.). I could pass the owner to its equipment at every
on_turn
call but it sounded like such a waste;The
_class
variable holds a character class (such as Cleric, Rogue, etc.), not a Python class.
creature.combat is not None
For (character) class identification, however, I'm going to use at least 32 different classes so a binary identification wouldn't work. \$\endgroup\$