2
votes

Python comment créer un ensemble de règles pour chaque classe dans un jeu

en C #, nous devons obtenir / définir des règles, mais je ne sais pas comment faire cela en Python.

Exemple: Les orcs ne peuvent équiper que des haches, les autres armes ne sont pas éligibles Les humains ne peuvent équiper que des épées, les autres armes sont éligibles.

Comment puis-je dire à Python qu'un Orc ne peut pas faire quelque chose comme dans l'exemple ci-dessus?

Merci pour les réponses à l'avance, j'espère que cela a du sens pour vous les gars.


3 commentaires

Cherchez-vous des membres privés? qu'entendez-vous par "get / set"? Fonctions Getter / Setter?


Disons que je joue à World of Warcraft en version classique, les humains ne pouvaient équiper que des armes certian au début et certaines armes étaient spécifiques à une classe, comment puis-je rendre certaines armes (objets) spécifiques à une race?


Cela dépend de la manière dont les armes sont mises en œuvre. Le moyen le plus simple serait de vérifier le type, mais ce n'est généralement pas recommandé. Pourquoi ne pas créer une classe Character et la spécialiser avec des sous-classes comme Orc et ajouter l'arme que vous voulez qu'elles aient comme membre de données:


3 Réponses :


0
votes

Le langage Python n'a pas de mécanisme efficace pour restreindre l'accès à une instance ou une méthode. Il existe cependant une convention pour faire précéder le nom d'un champ / d'une méthode d'un trait de soulignement pour simuler un comportement «protégé» ou «privé».

Mais, tous les membres d'une classe Python sont publics par défaut.


0 commentaires

0
votes

Si vous voulez que la classe Orc ait certains membres mais pas la classe Human , et que vous voulez toujours que ces classes soient liées (comme les deux étant des caractères), c'est exactement pour quel héritage. Voici un exemple de hiérarchie d'héritage:

 Hiérarchie d'héritage

Voici un exemple d'implémentation:

class Weapon:
    def __init__(self, damage):
        self.damage = damage

class Gun(Weapon):
    def __init__(self):
        Weapon.__init__(self, 300)

class Grenade(Weapon):
    def __init__(self):
        Weapon.__init__(self, 1000)

class Knife(Weapon):
    def __init__(self):
        Weapon.__init__(self, 50)

class Hammer(Weapon):
    def __init__(self):
        Weapon.__init__(self, 100)


class Character:
    def __init__(self, name, default_weapon, additional):
        self.name = name
        self.default_weapon = default_weapon
        self.additional = additional

    def attack(self):
        pass # some implementation

class Heavily_armed(Character):
    def __init__(self, name):
        Character.__init__(self, name, Gun(), Grenade())


class Lightly_armed(Character):
    def __init__(self, name):
        Character.__init__(self, name, Knife(), Hammer())

class Human(Lightly_armed):
    def __init__(self, name, age, height):
        Lightly_armed.__init__(self, name)
        self.height = height
        self.age = age

class Orc(Heavily_armed):
    def __init__(self, name, danger):
        Heavily_armed.__init__(self, name)
        self.danger = danger

Comme vous peut voir que chaque Personnage possède des armes, mais ce sont des types d'armes différents. Pour développer cela, vous pouvez créer un ensemble «d'armes disponibles» pour chaque type et créer une instance d'une certaine arme uniquement si elle est dans l'ensemble. Cela peut ou non être un choix de conception viable pour votre implémentation du jeu. Les choix sont infinis


8 commentaires

Ne serait-il pas préférable d'avoir dans chaque classe dérivée de Character une propriété qui spécifie quelles armes sont autorisées? Quelque chose comme allowed_weapons et appeler une méthode is_weapon_allowed chaque fois que le personnage tente d'équiper une arme? Cela permettrait également une approche légèrement différente dans laquelle vous pourriez spécifier quelle classe d'armes est autorisée (par exemple, armes à distance, mêlée, etc.)


Le problème avec cette approche est que vous ne pouvez l'utiliser qu'une seule fois. Supposons que nous voulions maintenant «magique» et «banal», et que nous voulions aussi «mal» et «bien», en plus de «lourd» et «léger». Un orc est une créature magique mal armée lourdement armée, un elfe est un bon créateur magique légèrement armé, et un humain est une bonne créature mondaine légèrement armée. À quoi ressemble maintenant la hiérarchie de classes? Les hiérarchies de classes capturent très mal ce type de règles et je le déconseille vivement.


Nous voyons ces problèmes dans d'autres domaines également. Les débutants à l'OO reçoivent souvent "véhicule" comme exemple, puis vous obtenez les sous-classes "véhicule terrestre" et "véhicule nautique". Ensuite, vous demandez "que se passe-t-il si nous voulons classer en fonction des militaires par rapport aux civils également?" et vous découvrez qu'il n'y a pas de place dans la hiérarchie des types qui correspond au "transporteur de troupes amphibies".


@EricLippert Pourquoi ne pas créer des classes abstraites "diaboliques" et "bonnes" et les ajouter à Orc et Human comme classes de base? Python prend en charge l'héritage multiple. Comment aborderiez-vous ce problème?


J'aurais dû dire que les langues à héritage unique supportent particulièrement mal ces concepts. Les langages qui ont plusieurs héritages, comme Python, ou les langages qui ont des traits, peuvent faire un peu mieux, mais il y a encore de nombreux pièges.


@Ayxan: Cela semble étrange d'avoir un humain implémentant à la fois le mal et le bien (et une classe séparée pour EvilHuman et GoodHuman est tout aussi moche). Pour moi, il est plus logique que l'alignement soit un champ ou une propriété mutable, plutôt qu'une classe de base.


@Brian Je pense qu'EvilHuman qui est dérivé à la fois de Human et Evil est un beau design. Ensuite, vous pouvez dériver n'importe quel type de Evil pour le rendre mauvais. Je veux dire bien sûr que ma conception ici est beaucoup plus simple que ce que vous auriez dans un vrai projet, mais quand même


@Ayxan: Cette approche fait un mauvais travail de traitement du cas d'un humain qui change d'alignement. Il double également le nombre de nœuds feuilles sur la hiérarchie d'héritage de classe de personnage de votre joueur.



3
votes

en C #, nous devons obtenir / définir des règles, mais je ne sais pas comment faire cela en Python.

Non. Getters and Setters ne vous aidera pas ici. Notez que Python aussi a des getters / setters et des dunders (quelque chose comme self .__ foo ), mais ne suivons pas ce chemin.


À la place , regardons ce que vous avez:

  • un tas de choses (comme les orcs et les humains et épées et autres)
  • un tas d'actions (ok, actuellement ce n'est qu'une action, maniez , mais peut-être que demain vous déciderez qu'un vampire peut boire du sang, mais pas un humain)
  • et un tas de règles (une hache est une arme, une épée est une arme, les orcs ne peuvent utiliser que la hache, les humains peuvent utiliser d'autres armes, ...).

Alors, essayons de modéliser notre jeu de cette façon: avec Choses , Actions et Règles . p >

Parce que nous sommes des enfants sympas, commençons par écrire nos règles sous forme de texte:

rules =[
    "Human is Person",
    "Orc is Person",
    "Person may wield Weapon",
    "Human may not wield Axe",
    "Orc may only wield Axe",
    "Sword is Weapon",
    "Bow is Weapon",
    "Axe is Weapon",
    "Cow is Animal",
    "Animal may eat Grass"
]

class Rules:
    alias_list = []
    prohibition_list = []
    permission_list = []
    exclusive_list = []

    def parse_rules(rules):
        for rule in rules:
            if ' is ' in rule:
                type, alias = rule.split(' is ')
                Rules.alias_list.append((type, alias))
            elif ' may only ' in rule:
                obj, rest = rule.split(' may only ')
                action, second = rest.split(' ')
                Rules.exclusive_list.append((obj, action, second))
            elif ' may not ' in rule:
                obj, rest = rule.split(' may not ')
                action, second = rest.split(' ')
                Rules.prohibition_list.append((obj, action, second))
            elif ' may ' in rule:
                obj, rest = rule.split(' may ')
                action, second = rest.split(' ')
                Rules.permission_list.append((obj, action, second))

    def resolve_types_inner(types, aliases):
        for (source_type, alias_type) in aliases[:]:
            if source_type in types:
                types.add(alias_type)
                aliases.remove((source_type, alias_type))
                return Rules.resolve_types_inner(types, aliases)
        return types

    def resolve_types(thing):
        types = set(thing.type)
        return Rules.resolve_types_inner(types, Rules.alias_list[:])

    def allowed(action_to_test):
        a_types = Rules.resolve_types(action_to_test.a)
        b_types = Rules.resolve_types(action_to_test.b)

        for (a, action, b) in Rules.exclusive_list:
            if action == action_to_test.name:
                if a in a_types and b in b_types:
                    print ('-- allowed by exclusive_list')
                    return True

        for (a, action, b) in Rules.prohibition_list:
            if action == action_to_test.name:
                if a in a_types and b in b_types:
                    print ('-- forbidden')
                    return False

        for (a, action, b) in Rules.permission_list:
            if action == action_to_test.name:
                if a in a_types and b in b_types:
                    if not action in (x for (a2,x,b2) in Rules.exclusive_list if x == action and a2 in a_types):
                        print ('-- allowed')
                        return True
                    else:
                        print ('-- forbidden by exclusive_list')
                        return False

        print ('-- no rules match')

class Action:
    def __init__(self, name, a, b):
        self.name = name
        self.a = a
        self.b = b

    def invoke(self):
        print('You feel a strange sensation...')

    def forbidden(self):
        print(f"{self.a.name} tries to {self.name} {self.b.name}, but that is not possible")

    def __call__(self):
        if Rules.allowed(self):
            self.invoke()
        else:
            self.forbidden()
        print('----------')

class Wield(Action):
    def __init__(self, thing, weapon):
        super().__init__('wield', thing, weapon)

    def invoke(self):
        if hasattr(self.a, 'weapon'):
            print(f'{self.a.name} drops {self.a.weapon.name}')
        self.a.weapon = self.b
        print(f'{self.a.name} now wields {self.a.weapon.name}')

class Eat(Action):
    def __init__(self, thing, food):
        super().__init__('eat', thing, food)

    def forbidden(self):
        print(f'{self.a.name} tried to eat {self.b.name}, but did not like it very much...')

    def invoke(self):
        print(f'{self.a.name} eats {self.b.name}')

class Thing:
    def __init__(self, name, *type):
        self.name = name
        self.type = ['Thing', *type]

    def action(self, action_class, *args):
        action_class(self, *args)()

if __name__ == '__main__':

    Rules.parse_rules(rules)

    Carl_the_Human = Thing('Carl', 'Human')
    Grump_the_Orc = Thing('Grump', 'Orc')
    Sandy_the_Cow = Thing('Sandy', 'Cow')
    Carls_sword = Thing("Carl's Sword of Justice", 'Sword')
    Grumps_axe = Thing("Grump's rusty Axe", 'Axe')
    Old_bow = Thing("An old bow", 'Bow')

    Sandy_the_Cow.action(Wield, Grumps_axe)
    Sandy_the_Cow.action(Eat, Grumps_axe)
    Sandy_the_Cow.action(Eat, Thing("a bunch of grass", "Grass"))

    Carl_the_Human.action(Wield, Carls_sword)
    Carl_the_Human.action(Wield, Grumps_axe)
    Carl_the_Human.action(Wield, Old_bow)

    Grump_the_Orc.action(Wield, Grumps_axe)
    Grump_the_Orc.action(Wield, Carls_sword)

Comme vous pouvez le voir, je parle de vaches, d'animaux et d'herbe, aussi, pour que nous puissions voir que nous allons faire une approche très générique.

Nous savons que nos "choses" ont différents types, un nom et une façon d'invoquer une "action", donc voici notre classe Thing :

-- forbidden
the Archer tries to wield Carl's Sword of Justice, but that is not possible
----------
-- allowed
the Archer now wields An old bow
----------
-- allowed
the Archer drops An old bow
the Archer now wields A golden Spear
----------
-- allowed
the old Guy now wields Carl's Sword of Justice
----------
-- forbidden
the old Guy tries to wield An old bow, but that is not possible
----------
-- allowed
the old Guy drops Carl's Sword of Justice
the old Guy now wields A golden Spear
----------

Une Thing est de type 'Thing' et tout ce que nous transmettons à __init__ , et nous pouvons appeler la fonction action avec une classe Action (nous la créons sous peu) et quelques arguments que nous transmettons à cette fonction.

Pour l'instant, tout simplement.


Voici à quoi pourrait ressembler une Action générique:

XXX

Simplement un nom et deux choses ( a et b ). Il peut être appelé (par exemple par Thing.action ), et il est soit autorisé à être appelé (puis appeler invoke ) ou non (puis appeler fobidden ).

Ignorons pour l'instant Rules.allowed et créons des actions qui font quelque chose:

Swordman = Thing('the old Guy', 'Swordman')
Archer = Thing('the Archer', 'Archer')
Carls_sword = Thing("Carl's Sword of Justice", 'Sword')
Old_bow = Thing("An old bow", 'Bow')
Spear = Thing("A golden Spear", 'Spear')

Archer.action(Wield, Carls_sword)
Archer.action(Wield, Old_bow)
Archer.action(Wield, Spear)

Swordman.action(Wield, Carls_sword)
Swordman.action(Wield, Old_bow)
Swordman.action(Wield, Spear)

Le Wield définira l ' arme de l'appelant, mais uniquement si elle est autorisée. L'action Eat , eh bien, affiche juste un message pour le moment ...

Donc, la seule chose qui nous reste à faire maintenant est d'implémenter les Rules.allowed , cela signifie analyser les règles que nous avons créées au début et agir en conséquence.


Voici la classe Rules :

"Ranged is Weapon",
"Melee is Weapon",
"Bow is Ranged",
"Spear is Ranged",
"Sword is Melee",
"Human is Person",
"Archer is Human",
"Swordman is Human",
"Person may wield Weapon",
"Archer may not wield Melee",
"Swordman may not wield Bow"

Bien sûr, c'est juste très basique et pas un Moteur de règles à part entière ou langage de programmation logique, mais cela fera l'affaire pour le moment.

Nous prenons déjà en charge 4 fonctionnalités:

  • Alias. Nous pouvons dire que quelque chose A est quelque chose B, et toutes les règles de B s'appliquent à A
  • Autoriser quelque chose
  • Interdire quelque chose
  • Autoriser A quelque chose uniquement pour un B spécifique

La fonction parse_rules divise simplement les chaînes et ajoute les parties à différentes listes, et dans la fonction allowed nous itérons ces listes pour déterminer si quelque chose est autorisé ou non.

N'hésitez pas à améliorer ceci ou à ajouter de nouvelles fonctionnalités.


Nous sommes maintenant prêts à partir.

Laissez-nous exécutez ce qui suit:

-- no rules match  
Sandy tries to wield Grump's rusty Axe, but that is not possible  
----------  
-- no rules match  
Sandy tried to eat Grump's rusty Axe, but did not like it very much...  
----------  
-- allowed  
Sandy eats a bunch of grass  
----------  
-- allowed  
Carl now wields Carl's Sword of Justice  
----------  
-- forbidden  
Carl tries to wield Grump's rusty Axe, but that is not possible  
----------  
-- allowed  
Carl drops Carl's Sword of Justice  
Carl now wields An old bow  
----------  
-- allowed by exclusive_list  
Grump now wields Grump's rusty Axe  
----------  
-- forbidden by exclusive_list  
Grump tries to wield Carl's Sword of Justice, but that is not possible  
----------

nous obtenons le résultat suivant:

# prepare our simple rule engine
Rules.parse_rules(rules)

# Let some things exist in the world
Carl_the_Human = Thing('Carl', 'Human')
Grump_the_Orc = Thing('Grump', 'Orc')
Sandy_the_Cow = Thing('Sandy', 'Cow')
Carls_sword = Thing("Carl's Sword of Justice", 'Sword')
Grumps_axe = Thing("Grump's rusty Axe", 'Axe')
Old_bow = Thing("An old bow", 'Bow')

# Sandy is hungry
Sandy_the_Cow.action(Wield, Grumps_axe)
Sandy_the_Cow.action(Eat, Grumps_axe)
Sandy_the_Cow.action(Eat, Thing("a bunch of grass", "Grass"))

# Carl wants to try some weapons
Carl_the_Human.action(Wield, Carls_sword)
Carl_the_Human.action(Wield, Grumps_axe)
Carl_the_Human.action(Wield, Old_bow)

# Grump wants to try some weapons    
Grump_the_Orc.action(Wield, Grumps_axe)
Grump_the_Orc.action(Wield, Carls_sword)

Chaque fois que nous avons besoin d'une nouvelle "règle" dans notre monde du jeu, nous pouvons simplement l'ajouter à notre liste de règles sous forme de texte simple et laisser notre moteur de règles simple décider si quelque chose est autorisé (ou même comment quelque chose devrait se passer, si nous étendons notre moteur).

Alors peut-être nous avons des armes à distance et de mêlée, et les épéistes peuvent également utiliser des lances mais pas des arcs, et les archers peuvent utiliser des arcs et des lances mais pas des armes de mêlée?

Pas de problème, écrivez-le simplement dans les règles:

class Rules:
    alias_list = []
    prohibition_list = []
    permission_list = []
    exclusive_list = []

    def parse_rules(rules):
        for rule in rules:
            if ' is ' in rule:
                type, alias = rule.split(' is ')
                Rules.alias_list.append((type, alias))
            elif ' may only ' in rule:
                obj, rest = rule.split(' may only ')
                action, second = rest.split(' ')
                Rules.exclusive_list.append((obj, action, second))
            elif ' may not ' in rule:
                obj, rest = rule.split(' may not ')
                action, second = rest.split(' ')
                Rules.prohibition_list.append((obj, action, second))
            elif ' may ' in rule:
                obj, rest = rule.split(' may ')
                action, second = rest.split(' ')
                Rules.permission_list.append((obj, action, second))

    def resolve_types_inner(types, aliases):
        for (source_type, alias_type) in aliases[:]:
            if source_type in types:
                types.add(alias_type)
                aliases.remove((source_type, alias_type))
                return Rules.resolve_types_inner(types, aliases)
        return types

    def resolve_types(thing):
        types = set(thing.type)
        return Rules.resolve_types_inner(types, Rules.alias_list[:])

    def allowed(action_to_test):
        a_types = Rules.resolve_types(action_to_test.a)
        b_types = Rules.resolve_types(action_to_test.b)

        for (a, action, b) in Rules.exclusive_list:
            if action == action_to_test.name:
                if a in a_types and b in b_types:
                    print ('-- allowed by exclusive_list')
                    return True

        for (a, action, b) in Rules.prohibition_list:
            if action == action_to_test.name:
                if a in a_types and b in b_types:
                    print ('-- forbidden')
                    return False

        for (a, action, b) in Rules.permission_list:
            if action == action_to_test.name:
                if a in a_types and b in b_types:
                    if not action in (x for (a2,x,b2) in Rules.exclusive_list if x == action and a2 in a_types):
                        print ('-- allowed')
                        return True
                    else:
                        print ('-- forbidden by exclusive_list')
                        return False
        print ('-- no rules match')

class Wield(Action):
    def __init__(self, thing, weapon):
        super().__init__('wield', thing, weapon)

    def invoke(self):
        if hasattr(self.a, 'weapon'):
            print(f'{self.a.name} drops {self.a.weapon.name}')
        self.a.weapon = self.b
        print(f'{self.a.name} now wields {self.a.weapon.name}')

class Eat(Action):
    def __init__(self, thing, food):
        super().__init__('eat', thing, food)

    def forbidden(self):
        print(f'{self.a.name} tried to eat {self.b.name}, but did not like it very much...')

    def invoke(self):
        print(f'{self.a.name} eats {self.b.name}')

class Action:
    def __init__(self, name, a, b):
        self.name = name
        self.a = a
        self.b = b

    def invoke(self):
        print('You feel a strange sensation...')

    def forbidden(self):
        print(f"{self.a.name} tries to {self.name} {self.b.name}, but that is not possible")

    def __call__(self):
        if Rules.allowed(self):
            self.invoke()
        else:
            self.forbidden()
        print('----------')

Voici le code complet exécutable que vous pouvez essayer nous-mêmes:

class Thing:
    def __init__(self, name, *type):
        self.name = name
        self.type = ['Thing', *type]

    def action(self, action_class, *args):
        action_class(self, *args)()

Notez qu'il existe des langages de programmation pour exactement cela, comme Inform7 .

Si vous voulez en savoir plus, je vous suggère de lire le Série Wizards and warriors d'Eric Lippert, qui parle exactement de ce problème (et ma réponse est inspirée de cette série), et utilise même des exemples similaires (classes et armes fantastiques), mais à mon humble avis écueil courant dans les langages de programmation OO pour modéliser les mauvaises choses avec des objets et essayer de forcer la logique métier dans le système de type des langages.


4 commentaires

Eh bien, cela répond à peu près à toutes les questions que j'avais posées et plus encore, merci! :)


@JohnnyNiklasson Eh bien, le tl; dr; est essentiellement: vous ne pouvez pas et ne devez pas modéliser les règles métier avec une hiérarchie de classes.


Je n'aurais pas dit mieux moi même!


@EricLippert Je suis fan de votre blog, et la série Wizard and warrios est l'une de mes préférées. Merci pour cela. Je l'ai recommandé à au moins une douzaine de personnes et j'espère qu'un jour, les gens s'arrêteront avec la classe Cow extend Animal -nonsense.