
Часть вступительная, не обязательна к прочтению, не несёт в себе ценной информации
Немного людей которые никогда не играли в настольные экономические игры, такие как монополия, рынок, миллионер. Мы с друзьями играли в них дни на пролёт. Со временем, после зазубривания всех правил, и десятков сыгранных партий, хотелось чего-то большего. И мы начали рисовать игры сами. Сначала маленькие, и в большей степени копирующие возможности тех игр, что мы выдели раньше, но потом приходили и свои идеи. В конце доходило до того, что игра располагалась на 9 листах формата А4, а её правила были настолько нетерпимыми к новичкам, что кроме нас никто не мог научиться в неё играть (хотя в монополию со мной играли родители). Там было много всего, строительство, экономика, игровое взаимодействие (например подставы или взаимопомощь). Десятки видов оружия, машин. Чтобы стрелять нужны были патроны. С некоторыми ранениями можно было продолжать играть, с другими путь в больницу, и т.п.
Игра длилась многие часы, и если пора было по домам, мы выходили с комнаты, а я не разрешал никому приближаться к игре, дабы никто не перепутал все наши предметы и фишки.
В данный момент я задал себе вопрос, почему не попробовать воссоздать, хоть малую часть тех возможностей, но не на бумаге, а в виде компьютерной программы.
И в этой статье я хочу поговорить о действиях, то есть о неких способностях игроков, которые изменяют различные свойства игры (правила). От этого и будем отталкиваться.
Часть техническая
Что из себя представляет цикл игры? Игра случайно выстраивает последовательность игроков, а в дальнейшем передаёт ход циклично. Игрок выбирают доступные действия. Заканчивает ход, и т.д.
В чем проблема с действиями
С каждой игрой ассоциирован объект игры (game-object). Действия изменяют его, тем самым открывают или закрывают возможность выполнения других действий. Например: покупка участка позволяет на этом участке заводы возводить (пока не учитываем различные нюансы).

Какие возможности нужно получить? В общем то, только одно: Разрешать или запрещать выполнения действий по мере изменения игровой ситуации.
Кому давать возможность выполнять действия?
В первом варианте, возможность выполнения действий была только у игрока, которому принадлежит право хода. Что несколько упрощает механизм, но исключает вмешательство других игроков на ситуацию и делает невозможным применение очень ценных игровых механик.
Пример:
Игрок вступает на клетку с земельным участком, но не покупает его. В этом случает нужно выставить участок на аукцион, что это значит? А это значит следующее: дать каждому игроку два действия — повысить ставку и отказаться от аукциона. Вышеописанная архитектура не позволяет такого.
Приходим к выводу, что действия должны генерироваться для всех (но для каждого свои).
Когда обновлять список действий?

Один из вариантов, перед ходом игрока. Но тут мы опять ограничиваем себя. Если у игрока не хватало денег купить участок и он применил карту (Добавить деньги. Да, да, так банально), игре нужно дать возможность ему купить.
Приходим к выводу, что создание действий для игроков, должно происходить заново, после каждого выполненного, потому как мы не знаем какие ресурсы изменились, и что будет доступно именно в данный момент.
Программная реализация создания действий
Самый простой вариант — полотно if-ов. Т.е. после каждого действия выполняются проверки и если они проходят, действие добавляется в список. Пример. На клетке мафия происходит следующие:
- Если мафия не подмята ни под кого, её можно подмять
- Если мафия не подмята ни под кого и у вас не получается её подмять, вы отдаёте завод на одном из ваших участков
- Если мафия подмята под вас, ничего не происходит
- Если мафия подмята под другого игрока, вы отдаёте n денег
Как это выглядит с if-ми:
if sector.is_mafia(): # Если мафия подмята под игрока if sector.mafia.is_has_owner(): # Проверка что подмята под другого игрока if sector.mafia.owner != player: # если у игрока есть завод (любой) if player.is_have_factory(): self._actions.append( Action( text='Заплатить мафии', # other args ) ) else: self._actions.append( Action( text='Отдать завод мафии', # other args ) ) self._actions.append( Action( text='Подмять мафию', # other args ) )
Проблемы очевидны. В блоке else сложно понять условия возникновения действия (нужно проследить всю вышестоящую цепочку). Такой код сложно менять, чтобы изменить правило нужно переместить Action() в нужную часть дерева if-ов. Очень длинная функция и итоге.
Неплохо было бы что-нибудь в таком духе:
@some_constraint # Ограничение def some_action(): pass # Изменение игровых данных
Пример с мафией при этом выглядит так:
@when((is_mafia ,), (is_mafia_has_alien_owner, )) def paid_mafia(game): sum = game.calc_paid_mafia(game.active_player) game.active_player.cash -= sum # Выполняем сами действия game.active_sector.land.owner.cash += sum
Условия это обычные функции, но есть два ограничения: они принимают один обязательный аргумент — объект игры и вернуть условие должно значение типа bool. Функции можно вызывать, самостоятельно, чтобы их комбинировать (is_mafia использует другое условие — sector_detect).
def sector_detect(game, sector_type: str) -> bool: return game.active_sector.land.type == sector_type def is_mafia(game) -> bool: return sector_detect(game, MAFIA_SECTOR) def is_mafia_has_alien_owner(game) -> bool: return game.active_sector.mafia.is_has_owner() and not game.active_sector.mafia.is_owner(game.active_player)
На самом деле, хочется чтобы регистрация действий так же была в декораторе.
@am.action('Заплатить мафии', ) # and other option) @am.when((is_mafia ,), (is_mafia_has_alien_owner, )) def paid_mafia(game): pass
Декоратор action регистрирует действие, плюс добавляет различные свойства (например, название для пользовательского интерфейса)
am — объект управляющий действиями (ActionManager)
Посмотрим на реализацию when и action. Я убрал некоторые моменты проверок, не критичные для самой логики. В целом это выглядит так:
class ActionManager: # ... def when(self, *conditions): def decorator(function): @wraps(function) def wrapper(*args, **kwargs): for condition, *arguments in conditions: if not condition(self.game, *arguments): break else: return function, args, kwargs return wrapper return decorator
Единственное что делает when это проверяет все условия, если хоть одно из них не выполнилось, действие игроку недоступно.
function — Это одно из действий, то есть изменение игровых данных (paid_mafia). Оно не должно выполнятся на момент их создания для вывода на интерфейс и предоставление выбора игроку. Поэтому просто возвращаем действие со всеми аргументами.
Регистрация действий:
class ActionManager: # ... def action(self, name): def decorator(function): @wraps(function) def wrapper(*args, **kwargs): pass self.actions.append(Action(name, function)) # Action = namedtuple('Action', ['verbose_name', 'exec']) return wrapper return decorator
Тут стоит заметить, что action на момент выполнения заносит действие в список. На самом деле без action можно обойтись (да и такая реализация приносит кое-какие проблемы), явно регистрируя действие:
game.register_action(some_action)
Чтобы вывести действия пользователю их надо подготовить:
class ActionManager: # ... def prepare(self): for action in self.actions: result = action.exec(self.game) # Вызовем действие (оно обвёрнуто в декоратор when) if result is None: # Это действие не может произойти, when вернуло None continue # Просто сохраним действие с аргументами, на случай если пользователь выберет его self.prepared_actions.append(action._replace(exec=result))
Ну и рабочий мини-пример, убрал все комментарии, добавил вывод отладки, организовал, что-то вроде игрового мини-цикла, ничего выбирать не надо, просто запустить:
from collections import namedtuple from functools import wraps BALANCE_MIN_LIMIT = 1000 MATERIAL_AID = 10000 FACTORY_COST = 900 Action = namedtuple('Action', ['verbose_name', 'exec']) DEBUG = True class ActionManager: def __init__(self): self.actions = [] self.game = { 'active_player': { 'cash': 300, 'factory': False }, } self.prepare_actions = [] def prepare(self): for action in self.actions: result = action.exec(self.game) if result is None: continue self.prepare_actions.append( action._replace(exec=result) ) def execute(self): print("PREPARE ACTION", self.prepare_actions) for action in self.prepare_actions: act, args, kwargs = action.exec act(*args, **kwargs) self.prepare() def action(self, name): def decorator(function): @wraps(function) def wrapper(*args, **kwargs): pass if DEBUG: print('ADD ACTION --', function.__name__) self.actions.append(Action(name, function)) return wrapper return decorator def when(self, *conditions): def decorator(function): @wraps(function) def wrapper(*args, **kwargs): for condition, *arguments in conditions: result = condition(self.game, *arguments) if DEBUG: print("CONDITION: {} ARGS: {} RESULT: {}".format(condition.__name__, arguments, result)) if not result: break else: return function, args, kwargs return wrapper return decorator am = ActionManager() def money_detect(game, limit: float) -> bool: return game.get('active_player').get('cash') < limit def money_more(game, limit: float) -> bool: return game.get('active_player').get('cash') > limit def is_player_havnt_factory(game) -> bool: return not game.get('active_player').get('factory') @am.action('Add money') @am.when((money_detect, BALANCE_MIN_LIMIT)) def add_money_action(game): game['active_player']['cash'] += MATERIAL_AID if DEBUG: print("Action", add_money_action.__name__, "SUCCESS") @am.action('Build factory') @am.when((money_more, FACTORY_COST), (is_player_havnt_factory, )) def build_factory(game): game['active_player']['factory'] = True if DEBUG: print("Action", build_factory.__name__, "SUCCESS") if __name__ == '__main__': print("BEFORE ACTION", am.game['active_player']) am.prepare() print("INTO THE GAME") am.execute() print("AFTER ACTION", am.game['active_player'])
Вывод программы:
ADD ACTION -- add_money_action
ADD ACTION -- build_factory
BEFORE ACTION {'factory': False, 'cash': 300}
CONDITION: money_detect ARGS: [1000] RESULT: True # У игрока критично мало денег
CONDITION: money_more ARGS: [900] RESULT: False # Завод он построить не может
INTO THE GAME
PREPARE ACTION [Action(verbose_name='Add money', exec=(<function add_money_action at 0x7f94728a1730>, ({'active_player': {'factory': False, 'cash': 300}},), {}))]
Action add_money_action SUCCESS
CONDITION: money_detect ARGS: [1000] RESULT: False
CONDITION: money_more ARGS: [900] RESULT: True
CONDITION: is_player_havnt_factory ARGS: [] RESULT: True # Строительство возможно, так как фхщавода еще нет
Action build_factory SUCCESS
CONDITION: money_detect ARGS: [1000] RESULT: False
CONDITION: money_more ARGS: [900] RESULT: True
CONDITION: is_player_havnt_factory ARGS: [] RESULT: False
AFTER ACTION {'factory': True, 'cash': 10300}
Немного страшный, но нам нужно посмотреть лишь то, что покупка участка выполнилась, только когда на счету стало хватать денег. В реальной игре, действия инициализируются игроками, а не происходят сами по себе.
Заключение:
Да, есть еще куча вариантов как это можно было сделать. Например вынести логику в конфигурационные файлы или написать мини-язык. Но это добавляет дополнительных проблем с взаимодействием и манипулированием игровыми данными. Да и цель была не избавиться от программирования Python, а сделать реализацию действий более целостной и, конечно, более лёгкой в изменении.
P.S. Если вам нужен начинающий Python(Django) разработчик, вы можете написать мне)
ссылка на оригинал статьи https://habrahabr.ru/post/318442/
Добавить комментарий