Design orientado a dados
Enviei algo como esta pergunta à revisão de código recentemente.
Após algumas sugestões e melhorias, o resultado foi um código simples que permitiria certa flexibilidade na criação de armas com base em um dicionário (ou JSON). Os dados são interpretados em tempo de execução e verificações simples são feitas pela Weapon
própria classe, sem a necessidade de contar com um interpretador de script inteiro.
O Design Orientado a Dados, apesar de o Python ser uma linguagem interpretada (os arquivos de origem e de dados podem ser editados sem a necessidade de recompilá-los), parece a coisa certa a fazer em casos como o que você apresentou. Esta pergunta entra em mais detalhes sobre o conceito, seus prós e contras. Há também uma boa apresentação na Universidade de Cornell sobre isso.
Comparado com outras linguagens, como C ++, que provavelmente usariam uma linguagem de script (como LUA) para lidar com interação e script de dados x mecanismos em geral, e com um determinado formato de dados (como XML) para armazenar os dados, o Python pode realmente fazer tudo por conta própria (considerando o padrão, dict
mas também weakref
, o último especificamente para carregamento e armazenamento de recursos).
Um desenvolvedor independente, no entanto, não pode levar ao extremo a abordagem orientada a dados, conforme sugerido neste artigo :
Quanto de design orientado a dados eu sou? Não acho que um mecanismo de jogo deva conter uma única linha de código específico do jogo. Nenhum. Nenhum tipo de arma codificado. Nenhum layout de HUD codificado. Nenhuma unidade codificada AI. Nada. Fecho eclair. Zilch.
Talvez, com o Python, alguém possa se beneficiar do melhor da abordagem orientada a objetos e orientada a dados, visando produtividade e extensibilidade.
Processamento de amostra simples
No caso específico discutido na revisão de código, um dicionário armazenaria os "atributos estáticos" e a lógica a ser interpretada - caso a arma tenha algum comportamento condicional.
No exemplo abaixo, uma espada deve ter algumas habilidades e estatísticas nas mãos de personagens da classe 'antipaladin', e sem efeitos, com estatísticas mais baixas quando usadas por outros personagens):
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]
}
]
}
}
Para fins de teste, criei classes Player
e simples Weapon
: a primeira a segurar / equipar a arma (chamando sua configuração de on_equip condicional) e a segunda como uma classe única que recuperaria os dados do dicionário, com base no nome do item passado como argumento durante a Weapon
inicialização. Eles não refletem o design adequado das classes de jogos, mas ainda podem ser úteis para testar os dados:
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)
Com algumas melhorias futuras, espero que isso me permita ter um sistema de criação dinâmico algum dia, processando componentes de armas em vez de armas inteiras ...
Teste
- O personagem A pega uma arma, equipa-a (imprimimos suas estatísticas) e depois a solta;
- O personagem B escolhe a mesma arma, equipa-a (e imprimimos suas estatísticas novamente para mostrar como elas são diferentes).
Como isso:
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()
Deve imprimir:
Para um bardo
Aprimoramento: 2, Efeitos de hits: [], Outros efeitos: []
Para um antipaladin
Aprimoramento: 5, Efeitos de hit: ['profano'], Outros efeitos: ['aurea profana']