Progettazione basata sui dati
Ho inviato qualcosa come questa domanda per la revisione del codice di recente.
Dopo alcuni suggerimenti e miglioramenti, il risultato è stato un semplice codice che avrebbe consentito una certa flessibilità relativa alla creazione dell'arma basata su un dizionario (o JSON). I dati vengono interpretati in fase di esecuzione e semplici verifiche vengono eseguite dalla Weapon
classe stessa, senza la necessità di fare affidamento su un intero interprete di script.
Data-Driven Design, nonostante Python sia un linguaggio interpretato (sia i file di origine che quelli di dati possono essere modificati senza la necessità di ricompilarli), suona come la cosa giusta da fare in casi come quello che hai presentato. Questa domanda approfondisce il concetto, i suoi pro e contro. C'è anche una bella presentazione sulla Cornell University a riguardo.
Rispetto ad altri linguaggi, come C ++, che probabilmente utilizzerebbe un linguaggio di scripting (come LUA) per gestire l'interazione dei dati x engine e gli script in generale, e un determinato formato di dati (come XML) per archiviare i dati, Python può effettivamente fare tutto da solo (considerando lo standard dict
ma anche weakref
quest'ultimo, in particolare per il caricamento delle risorse e la memorizzazione nella cache).
Uno sviluppatore indipendente, tuttavia, potrebbe non portare l'approccio basato sui dati all'estremo, come suggerito in questo articolo :
Quanto costa la progettazione basata sui dati? Non credo che un motore di gioco debba contenere una singola riga di codice specifico del gioco. Non uno. Nessun tipo di arma codificata. Nessun layout HUD codificato. Nessuna unità AI codificata. Nada. Cerniera lampo. Zilch.
Forse, con Python, si potrebbe beneficiare del meglio dell'approccio orientato agli oggetti e basato sui dati, mirando sia alla produttività che all'estensibilità.
Semplice elaborazione del campione
Nel caso specifico discusso sulla revisione del codice, un dizionario memorizzerebbe sia gli "attributi statici" che la logica da interpretare - se l'arma avesse un comportamento condizionale.
Nell'esempio seguente una spada dovrebbe avere alcune abilità e statistiche nelle mani dei personaggi della classe "antipaladino" e nessun effetto, con statistiche più basse se usate da altri personaggi):
WEAPONS = {
"bastard's sting": {
# magic enhancement, weight, value, dmg, and other attributes would go here.
"magic": 2,
# Those lists would contain the name of effects the weapon provides by default.
# They are empty because, in this example, the effects are only available in a
# specific condition.
"on_turn_actions": [],
"on_hit_actions": [],
"on_equip": [
{
"type": "check",
"condition": {
'object': 'owner',
'attribute': 'char_class',
'value': "antipaladin"
},
True: [
{
"type": "action",
"action": "add_to",
"args": {
"category": "on_hit",
"actions": ["unholy"]
}
},
{
"type": "action",
"action": "add_to",
"args": {
"category": "on_turn",
"actions": ["unholy aurea"]
}
},
{
"type": "action",
"action": "set_attribute",
"args": {
"field": "magic",
"value": 5
}
}
],
False: [
{
"type": "action",
"action": "set_attribute",
"args": {
"field": "magic",
"value": 2
}
}
]
}
],
"on_unequip": [
{
"type": "action",
"action": "remove_from",
"args": {
"category": "on_hit",
"actions": ["unholy"]
},
},
{
"type": "action",
"action": "remove_from",
"args": {
"category": "on_turn",
"actions": ["unholy aurea"]
},
},
{
"type": "action",
"action": "set_attribute",
"args": ["magic", 2]
}
]
}
}
A scopo di test, ho creato semplici Player
e Weapon
classi: il primo a contenere / equipaggiare l'arma (chiamando così la sua impostazione on_equip condizionale) e il secondo come una singola classe che avrebbe recuperato i dati dal dizionario, in base al nome dell'oggetto passato come argomento durante l' Weapon
inizializzazione. Non riflettono il corretto design delle classi di gioco, ma possono comunque essere utili per testare i dati:
class Player:
"""Represent the player character."""
inventory = []
def __init__(self, char_class):
"""For this example, we just store the class on the instance."""
self.char_class = char_class
def pick_up(self, item):
"""Pick an object, put in inventory, set its owner."""
self.inventory.append(item)
item.owner = self
class Weapon:
"""A type of item that can be equipped/used to attack."""
equipped = False
action_lists = {
"on_hit": "on_hit_actions",
"on_turn": "on_turn_actions",
}
def __init__(self, template):
"""Set the parameters based on a template."""
self.__dict__.update(WEAPONS[template])
def toggle_equip(self):
"""Set item status and call its equip/unequip functions."""
if self.equipped:
self.equipped = False
actions = self.on_unequip
else:
self.equipped = True
actions = self.on_equip
for action in actions:
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."""
obj = getattr(self, dic['condition']['object'])
compared_att = getattr(obj, dic['condition']['attribute'])
value = dic['condition']['value']
result = compared_att == value
self.action(*dic[result])
def action(self, *dicts):
"""Perform action with args, both specified on dicts."""
for dic in dicts:
act = getattr(self, dic['action'])
args = dic['args']
if isinstance(args, list):
act(*args)
elif isinstance(args, dict):
act(**args)
def set_attribute(self, field, value):
"""Set the specified field with the given value."""
setattr(self, field, value)
def add_to(self, category, actions):
"""Add one or more actions to the category's list."""
action_list = getattr(self, self.action_lists[category])
for action in actions:
if action not in action_list:
action_list.append(action)
def remove_from(self, category, actions):
"""Remove one or more actions from the category's list."""
action_list = getattr(self, self.action_lists[category])
for action in actions:
if action in action_list:
action_list.remove(action)
Con qualche miglioramento futuro spero che questo mi permetta persino di avere un sistema di crafting dinamico un giorno, che elabora componenti di armi anziché intere armi ...
Test
- Il personaggio A prende l'arma, la equipaggia (stampiamo le sue statistiche), quindi la rilascia;
- Il personaggio B prende la stessa arma, la equipaggia (e stampiamo di nuovo le sue statistiche per mostrare come sono diverse).
Come questo:
def test():
"""A simple test.
Item features should be printed differently for each player.
"""
weapon = Weapon("bastard's sting")
player1 = Player("bard")
player1.pick_up(weapon)
weapon.toggle_equip()
print("Enhancement: {}, Hit effects: {}, Other effects: {}".format(
weapon.magic, weapon.on_hit_actions, weapon.on_turn_actions))
weapon.toggle_equip()
player2 = Player("antipaladin")
player2.pick_up(weapon)
weapon.toggle_equip()
print("Enhancement: {}, Hit effects: {}, Other effects: {}".format(
weapon.magic, weapon.on_hit_actions, weapon.on_turn_actions))
if __name__ == '__main__':
test()
Dovrebbe stampare:
Per un bardo
Miglioramento: 2, Effetti hit: [], Altri effetti: []
Per un antipaladino
Miglioramento: 5, Effetti hit: ['unholy'], Altri effetti: ['unholy aurea']