Реализация правил (действий) в карточной онлайн игре

от автора

Часть вступительная, не обязательна к прочтению, не несёт в себе ценной информации

Немного людей которые никогда не играли в настольные экономические игры, такие как монополия, рынок, миллионер. Мы с друзьями играли в них дни на пролёт. Со временем, после зазубривания всех правил, и десятков сыгранных партий, хотелось чего-то большего. И мы начали рисовать игры сами. Сначала маленькие, и в большей степени копирующие возможности тех игр, что мы выдели раньше, но потом приходили и свои идеи. В конце доходило до того, что игра располагалась на 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *