#python #pygame
#python #pygame
Вопрос:
в C # мы должны получить / установить, чтобы создавать правила, но я не знаю, как это сделать на Python.
Пример: Орки могут вооружаться только топорами, другое оружие запрещено, люди могут вооружаться только мечами, другое оружие разрешено.
Как я могу сказать Python, что Orc не может сделать что-то вроде приведенного выше примера?
Заранее спасибо за ответы, надеюсь, это имело какой-то смысл для вас, ребята.
Комментарии:
1. Вы ищете закрытых участников? что вы подразумеваете под «получить / установить»? Функции получения / установки?
2. Допустим, я играю в world of warcraft еще в классической версии, люди вначале могли вооружаться только определенным оружием, и некоторые виды оружия были классовыми, как я могу придать определенному оружию (предметам) расовый характер?
3. Зависит от того, как реализовано оружие. Самым простым способом была бы проверка типов, но это обычно не рекомендуется. Почему бы вам не создать
Character
класс и не специализировать его на подклассах типаOrc
и добавить оружие, которое вы хотите, чтобы у них было в качестве элемента данных:
Ответ №1:
в C # мы должны получить / установить, чтобы создавать правила, но я не знаю, как это сделать на Python.
Нет. Получатели и установщики здесь вам не помогут. Обратите внимание, что в Python также есть геттеры / сеттеры и дандеры (что-то вроде self.__foo
), но давайте не будем следовать этому пути.
Вместо этого давайте посмотрим, что у вас есть:
- куча вещей (таких как орки, люди, мечи и прочее)
- набор действий (хорошо, в настоящее время это только одно действие, владеть, но, возможно, завтра вы решите, что Вампир может пить кровь, но не человек)
- и кучу правил (топор — это оружие, меч — это оружие, орки могут использовать только топор, Человек может использовать другое оружие, …).
Итак, давайте попробуем смоделировать нашу игру таким образом: с помощью Things
, Actions
и Rules
.
Поскольку мы классные ребята, давайте начнем с записи наших правил в виде текста:
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"
]
Как вы можете видеть, я также говорю о коровах, животных и траве, так что мы можем видеть, что мы собираемся использовать очень общий подход.
Мы знаем, что наши «вещи» имеют разные типы, название и способ вызова «действия», поэтому вот наш Thing
класс:
class Thing:
def __init__(self, name, *type):
self.name = name
self.type = ['Thing', *type]
def action(self, action_class, *args):
action_class(self, *args)()
A Thing
is of type 'Thing'
and anything else we pass to __init__
, and we can call the action
function with an Action
class (we create it shortly) and some arguments we pass to this function.
So far, so simple.
Now, here’s what a generic Action
could look like:
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('----------')
Simply a name and two things ( a
and b
). It can be called (e.g. by Thing.action
), and it’s either allowed to be invoked (and then call invoke
) or not (and then call fobidden
).
Давайте пока проигнорируем Rules.allowed
и создадим несколько действий, которые что-то делают:
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}')
Wield
Действие установит weapon
имя вызывающего, но только если это разрешено. Eat
Действие, ну, пока просто выводит сообщение…
Итак, единственное, что нам осталось сделать сейчас, это фактически реализовать Rules.allowed
, что означает анализ правил, которые мы создали вначале, и действовать в соответствии с этим.
Вот Rules
класс:
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')
Конечно, это всего лишь очень простой и не полноценный движок правил или язык логического программирования, но пока сойдет.
Мы уже поддерживаем 4 функции:
- Псевдонимы. Мы можем сказать, что что-то A является чем-то B, и все правила B применяются к A
- Разрешить что-то
- Запрещать что-либо
- Разрешить A что-либо только для определенного B
parse_rules
Функция просто разбивает строки и добавляет части в разные списки, а в allowed
функции мы перебираем эти списки, чтобы определить, разрешено что-то или нет.
Не стесняйтесь улучшать это или добавлять новые функции.
Итак, теперь мы готовы приступить.
Давайте запустим следующее:
# 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)
мы получаем следующий результат:
-- 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
----------
Всякий раз, когда нам нужно новое «Правило» в нашем игровом мире, мы можем просто добавить его в наш список правил в виде простого текста и позволить нашему движку простых правил решать, разрешено ли что-то (или даже как что-то должно происходить, если мы расширим наш движок).
Так, может быть, у нас есть оружие дальнего боя, и фехтовальщики могут также использовать копья, но не луки, а лучники могут использовать луки и копья, но не оружие ближнего боя?
Нет проблем, просто запишите это в правилах:
"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"
Example:
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)
Результат:
-- 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
----------
Вот полный, доступный для запуска код, чтобы вы могли попробовать сами:
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)
Обратите внимание, что существуют языки программирования именно для этого, такие как Inform7.
Если вы хотите прочитать больше, я предлагаю прочитать серию «Волшебники и воины» Эрика Липперта, в которой рассказывается именно об этой проблеме (и мой ответ вдохновлен этой серией), и даже использует похожие примеры (фэнтезийные классы и оружие), но ИМХО, это обычная ошибка в языках программирования OO — моделировать неправильные вещи с объектами и пытаться внедрить бизнес-логику в систему типов языков.
Комментарии:
1. Что ж, это отвечает примерно на все вопросы, которые я задавал, и даже больше, спасибо! 🙂
2. @JohnnyNiklasson Ну, tl; dr; в основном таков: вы не можете и не должны моделировать бизнес-правила с иерархией классов.
3. Я сам не смог бы сказать это лучше!
4. @EricLippert Я фанат вашего блога, а серия Wizard и warrios — одна из моих любимых. Спасибо вам за это. Я рекомендовал его по крайней мере дюжине pepole и надеюсь, что когда-нибудь люди перестанут с
class Cow extends Animal
-ерундой.5. Отличный ответ. Если бы это отвечало на один из моих вопросов, я бы, вероятно, назначил награду за это — но у вас на 90 тысяч очков больше, чем у меня, так что, возможно, нет. 🙂
Ответ №2:
В языке Python нет эффективного механизма для ограничения доступа к экземпляру или методу. Однако существует соглашение о том, чтобы ставить перед именем поля / метода знак подчеркивания, чтобы имитировать «защищенное» или «частное» поведение.
Но все члены в классе Python по умолчанию являются общедоступными.
Ответ №3:
Если вы хотите, чтобы у класса Orc
были определенные члены, а у class Human
нет, и вы все еще хотите, чтобы эти классы были связаны (поскольку оба являются символами), это именно то, для чего нужно наследование. Вот пример иерархии наследования:
Вот пример реализации:
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
Как вы можете видеть, у каждого Character
есть оружие, но это разные виды оружия. Чтобы расширить это, вы можете создать набор «доступного оружия» для каждого типа и создать экземпляр определенного оружия, только если оно есть в наборе. Это может быть или не быть жизнеспособным выбором дизайна для вашей реализации игры. Выбор бесконечен
Комментарии:
1. Не было бы лучше иметь в каждом классе производное от
Character
свойства, которое определяет, какое оружие разрешено? Что-то вродеallowed_weapons
и вызывать методis_weapon_allowed
каждый раз, когда персонаж пытается экипировать оружие? Это также позволило бы использовать несколько иной подход, при котором вы могли бы указать, какой класс оружия разрешен (скажем, оружие дальнего боя, ближний бой и так далее)2. Проблема с этим подходом заключается в том, что вы можете использовать его только один раз. Предположим, что теперь мы хотим «волшебный» и «приземленный», и мы также хотим «злой» и «добрый», в дополнение к «тяжелому» и «легкому». Орк — это злое, хорошо вооруженное магическое существо, эльф — хороший легковооруженный магический творец, а человек — хорошее, легко вооруженное обычное существо. Теперь, как выглядит иерархия классов? Иерархии классов очень плохо отражают такого рода правила, и я настоятельно рекомендую отказаться от этого.
3. Мы видим эти проблемы и в других доменах. Новичкам в OO часто приводят «транспортное средство» в качестве примера, а затем вы получаете подклассы «наземное транспортное средство» и «водное транспортное средство». Затем вы спрашиваете: «Что, если мы также захотим классифицировать на основе военных и гражданских?» и вы обнаруживаете, что в иерархии типов нет места, которое подходило бы «транспортному средству-амфибии».
4. @EricLippert Почему бы не создать абстрактные классы «зло» и «добро» и добавить их в Orc и Human в качестве базовых классов? Python поддерживает множественное наследование. Как бы вы решили эту проблему?
5. Я должен был сказать, что языки с одним наследованием поддерживают эти концепции особенно плохо. Языки с множественным наследованием, такие как Python, или языки с чертами, могут работать несколько лучше, но все еще есть много подводных камней.