runewalsh

Битва Битардов

Oct 19th, 2021 (edited)
613
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. """
  2. Битва Битардов Director's Cut Edition Remastered v1.2
  3.  
  4. Для работы программы потребуются:
  5. - Python 3.x версии (python.org);
  6. - Библиотека Pillow для Python 3.x. (pypi.org -> в строке поиска пишем «pillow»; или установить через pip3).
  7. Подробнее как это всё устанавливать для вашей ОС можно загуглить так:
  8. « Как установить Python 3 на %моя_операционная_система_нейм% »
  9. « Как установить Pillow для %моя_операционная_система_нейм% »
  10.  
  11. Внимание! Функции сохранения и загрузки используют модуль pickle. Никогда не загружайте файлы сохранений, которые
  12. записал кто-то другой, например, скачанные из интернета. Это может привести к непредсказуемым последствиям, так как
  13. в .bbsav файлах хранятся объекты Python как есть.
  14. Злоумышленники могут добавить в них вредоносный код и он выполнится при загрузке сохранения.
  15.  
  16. Кнопка «ОБНОВИТЬ КАРТУ» создаёт или перезаписывает изображение "bitva-bitardov.png" в каталог "output".
  17. После каждого обновления карты лог событий сбрасывается.
  18. Рекомендуется обновлять карту не реже 20 применённых действий.
  19.  
  20. ====== НАЧАЛО ШАПКИ ТРЕДА =====
  21.  
  22. [b]Битва Битардов[/b]
  23. Тебе предстоит выступить в качестве генерала на стороне одной из враждующих фракций — Мочи или Говна.
  24. Ты можешь послать свою армию защищать город твоей фракции, либо атаковать город врага.
  25. Нападать можно только на те города, которые находятся на соседних территориях, на карте возможности для атаки показаны стрелочками. В случае успешного захвата города, остатки победившей армии остаются в этом городе в качестве защитников.
  26. Защищать можно любой город своей фракции. Армия, посланная на защиту города, остаётся в городе пока не будет уничтожена противником. Сила обороняющихся армий в одном и том же городе суммируется.
  27.  
  28. [b]В посте нужно указывать:[/b]
  29. 1. Принадлежность фракции (обязательно)
  30. 2. Имя командира (не обязательно)
  31. 3. Приказ твоей армии — какой город защищать или на какой город напасть.
  32. [b]Рероллить ссылкой на пост нельзя. В каждом посте нужно описывать всё по правилам, иначе такой пост не учитывается.[/b]
  33.  
  34. Владение городами даёт определённые бонусы фракциям. Например, начальные города Говна дают бонус по +1 к силе армии при обороне города, а города Мочи дают +1 при атаке. Если фракция Мочи захватит один из городов Говна, она также получит бонус +1 при защите. Столицы дают особые бонусы. Информацию о бонусах можно посмотреть на карте.
  35.  
  36. [b]Как определяется сила армии:[/b]
  37. Всё что ниже дабла = ничего (не учитывается);
  38. Дабл = цифра в дабле умноженная на себя + 10;
  39. Трипл = цифра в трипле умноженная на себя два раза + 30;
  40. Квадрипл = цифра квадрипла умноженная на себя три раза + 40;
  41. и так далее...
  42.  
  43. [b]Как рассчитывается итог сражения.[/b]
  44.  
  45. Армия нападает на город:
  46. [сила_атакующей_армии] - ([сила_гарнизона] + [бонус_защиты_от_городов]) x [бонус_защиты_от_столицы] = [остатки_атакующих]
  47. Если [остатки_атакующих] больше 0, считается, что город захвачен;
  48. При этом, [остатки_атакующих] добавляются к силе гарнизона города, теперь уже принадлежащего другой фракции.
  49. Независимо от того, был ли захвачен город после атаки, рассчитывается урон по гарнизону города:
  50. [сила_гарнизона] - ([сила_атакующей_армии] + [бонус_атаки_от_городов]) x [бонус_атаки_от_столицы] = [остатки_гарнизона]
  51. При этом, даже если [остатки_гарнизона] = 0 или отрицательное число, город считается захваченным только когда [остатки_атакующих] больше нуля. То есть когда погибает и вся атакующая армия, и весь гарнизон, город захваченным не считается.
  52.  
  53. Армия защищает город:
  54. [сила_гарнизона_было] + [сила_защищающей_армии] = [сила_гарнизона_стало]
  55. То есть сила армии просто просто добавляется к силе гарнизона.
  56.  
  57. В случаях, когда армия приходит атаковать город, который уже захвачен союзником, считается, что она защищает этот город.
  58. Когда армия приходит защищать город, который уже захвачен врагом, считается, что она атакует этот город.
  59.  
  60. Чтобы ОП успевал обрабатывать поступающую информацию и оперативно обновлять карту, [b]после поста ОП'а «СТОП» битва приостанавливается. Любые посты после этого не учитываются. Битва продолжается и посты снова учитываются толко после того как ОП запостит обновлённую карту.[/b]
  61.  
  62. Условия победы:
  63. Захватить все города на карте, либо к 500-ому посту в треде собрать в своих городах более сильное войско (суммарно), без учёта бонусов.
  64.  
  65. Да начнётся Битва Битардов!
  66.  
  67. ====== КОНЕЦ  ШАПКИ ТРЕДА =====
  68. """
  69.  
  70. import tkinter
  71. import tkinter.messagebox
  72. import tkinter.filedialog
  73. import os
  74. import os.path as path
  75. import pickle
  76. from PIL import Image, ImageDraw, ImageFont
  77.  
  78. class Options:
  79.     def __init__(self):
  80.         self.FONT_GUI = "FreeSansBold 10"
  81.         self.FG_COLOR = "#FF6600" # Цвет текста GUI
  82.         self.BG_COLOR = "#EEEEEE" # Цвет фона GUI
  83.         self.BACKGROUND = "background_default.png" # Фон игровой карты (файл должен находиться в папке "resources")
  84.  
  85.         self.DEFAULT_GENERAL_NAME = "Аноним" # Имя генерала по умолчанию
  86.         self.POST_NUMBER_LENGTH = 9 # Длина номера поста, должна быть ≥ 2
  87.  
  88.         self.MAX_POST_COUNT = 500 # Пост после которого битва завершается (пользователь останавливает битву вручную)
  89.  
  90.         self.working_dir = path.split(path.realpath(__file__))[0]
  91.  
  92. # *Meta — неизменные данные о фракциях, городах и мире.
  93. class FractionMeta:
  94.     def __init__(self, human_name, label_color, accusative=None):
  95.         self.human_name = human_name
  96.         self.accusative = accusative or human_name
  97.         self.label_color = label_color
  98.  
  99. class CityMeta:
  100.     def __init__(self, human_name, bonus, routes, starting_fraction, label_pos, radiobutton_order):
  101.         self.human_name = human_name
  102.         self.bonus = bonus
  103.         self.routes = routes
  104.         self.starting_fraction = starting_fraction
  105.         self.label_pos = label_pos
  106.         self.radiobutton_order = radiobutton_order
  107.  
  108. class WorldMeta:
  109.     def __init__(self):
  110.         self.fractions = {
  111.             'mocha': FractionMeta(human_name="Моча", accusative="Мочу", label_color="#F5F325"),
  112.             'govno': FractionMeta(human_name="Говно", label_color="#F58500")
  113.         }
  114.  
  115.         self.cities = {
  116.             'portmocha': CityMeta(
  117.                 human_name="Порт-Моча", bonus='attackX',
  118.                 routes=('huegrad', 'huisk'),
  119.                 starting_fraction='mocha', label_pos=(267, 422), radiobutton_order=0),
  120.  
  121.             'huegrad': CityMeta(
  122.                 human_name="Хуеград", bonus='attack+',
  123.                 routes=('portmocha', 'huisk', 'huihrustalniy', 'velikiessaki'),
  124.                 starting_fraction='mocha', label_pos=(477, 343), radiobutton_order=2),
  125.  
  126.             'velikiessaki': CityMeta(
  127.                 human_name="Великие Ссаки", bonus='attack+',
  128.                 routes=('huegrad', 'huihrustalniy'),
  129.                 starting_fraction='mocha', label_pos=(604, 256), radiobutton_order=1),
  130.  
  131.             'huisk': CityMeta(
  132.                 human_name="Хуйск", bonus='attack+',
  133.                 routes=('portmocha', 'huegrad', 'huihrustalniy', 'forthui'),
  134.                 starting_fraction='mocha', label_pos=(588, 423), radiobutton_order=3),
  135.  
  136.             'huihrustalniy': CityMeta(
  137.                 human_name="Хуй Хрустальный", bonus='attack+',
  138.                 routes=('velikiessaki', 'huegrad', 'huisk', 'forthui'),
  139.                 starting_fraction='mocha', label_pos=(732, 355), radiobutton_order=4),
  140.  
  141.             'forthui': CityMeta(
  142.                 human_name="Форт-Хуй", bonus='attack+',
  143.                 routes=('huisk', 'huihrustalniy', 'severnayazalupa', 'yujnayazalupa'),
  144.                 starting_fraction='mocha', label_pos=(822, 432), radiobutton_order=5),
  145.  
  146.             'severnayazalupa': CityMeta(
  147.                 human_name="Северная Залупа", bonus='attack+',
  148.                 routes=('forthui', 'yujnayazalupa', 'battholl', 'nordess'),
  149.                 starting_fraction='mocha', label_pos=(1025, 417), radiobutton_order=6),
  150.  
  151.             'yujnayazalupa': CityMeta(
  152.                 human_name="Южная Залупа", bonus='attack+',
  153.                 routes=('forthui', 'severnayazalupa', 'battholl', 'sauzess'),
  154.                 starting_fraction='mocha', label_pos=(907, 499), radiobutton_order=7),
  155.  
  156.             'govndor': CityMeta(
  157.                 human_name="Говндор", bonus='defenseX',
  158.                 routes=('analdip', 'essfinger', 'kalhill', 'dristland'),
  159.                 starting_fraction='govno', label_pos=(1649, 563), radiobutton_order=8),
  160.  
  161.             'analdip': CityMeta(
  162.                 human_name="Анал-Дип", bonus='defense+',
  163.                 routes=('govndor', 'essfinger', 'nordess'),
  164.                 starting_fraction='govno', label_pos=(1592, 394), radiobutton_order=9),
  165.  
  166.             'essfinger': CityMeta(
  167.                 human_name="Эссфингер", bonus='defense+',
  168.                 routes=('govndor', 'analdip', 'nordess', 'battholl', 'kalhill'),
  169.                 starting_fraction='govno', label_pos=(1459, 485), radiobutton_order=10),
  170.  
  171.             'kalhill': CityMeta(
  172.                 human_name="Кал-Хилл", bonus='defense+',
  173.                 routes=('govndor', 'dristland', 'sauzess', 'battholl', 'essfinger'),
  174.                 starting_fraction='govno', label_pos=(1367, 599), radiobutton_order=11),
  175.  
  176.             'dristland': CityMeta(
  177.                 human_name="Дристланд", bonus='defense+',
  178.                 routes=('govndor', 'kalhill', 'sauzess'),
  179.                 starting_fraction='govno', label_pos=(1210, 775), radiobutton_order=12),
  180.  
  181.             'nordess': CityMeta(
  182.                 human_name="Норд-Эсс", bonus='defense+',
  183.                 routes=('analdip', 'essfinger', 'battholl', 'severnayazalupa'),
  184.                 starting_fraction='govno', label_pos=(1269, 341), radiobutton_order=13),
  185.  
  186.             'battholl': CityMeta(
  187.                 human_name="Батт-Холл", bonus='defense+',
  188.                 routes=('nordess', 'essfinger', 'kalhill', 'sauzess', 'yujnayazalupa', 'severnayazalupa'),
  189.                 starting_fraction='govno', label_pos=(1219, 495), radiobutton_order=14),
  190.  
  191.             'sauzess': CityMeta(
  192.                 human_name="Сауз-Эсс", bonus='defense+',
  193.                 routes=('battholl', 'kalhill', 'dristland', 'yujnayazalupa'),
  194.                 starting_fraction='govno', label_pos=(1029, 653), radiobutton_order=15),
  195.         }
  196.  
  197. # *State — изменяющиеся данные о фракциях, городах и мире.
  198. class CityState:
  199.     def __init__(self, id, world):
  200.         self.id = id
  201.         self.world = world
  202.         self.fraction_id = None
  203.         self.forces = 0
  204.  
  205.     meta = property(lambda self: self.world.meta.cities[self.id])
  206.     fraction = property(lambda self: self.world.fractions[self.fraction_id])
  207.  
  208.     def transfer_to(self, fraction):
  209.         self.fraction_id = fraction.id
  210.  
  211. class FractionState:
  212.     def __init__(self, id, world):
  213.         self.id = id
  214.         self.world = world
  215.  
  216.     meta = property(lambda self: self.world.meta.fractions[self.id])
  217.  
  218.     def cities(self):
  219.         return (city for city in self.world.cities.values() if city.fraction_id == self.id)
  220.  
  221.     def total_forces(self): return sum(city.forces for city in self.cities())
  222.     def total_cities(self): return sum(1 for city in self.cities())
  223.     def attack_plus(self): return sum(1 for city in self.cities() if city.meta.bonus == 'attack+')
  224.     def attack_x(self): return 1 + sum(1 for city in self.cities() if city.meta.bonus == 'attackX')
  225.     def defense_plus(self): return sum(1 for city in self.cities() if city.meta.bonus == 'defense+')
  226.     def defense_x(self): return 1 + sum(1 for city in self.cities() if city.meta.bonus == 'defenseX')
  227.  
  228. class WorldState:
  229.     def __init__(self, meta):
  230.         self.meta = meta
  231.         self.fractions = { fraction_id: FractionState(fraction_id, self) for fraction_id, fraction_meta in meta.fractions.items() }
  232.         self.cities = { city_id: CityState(city_id, self) for city_id in meta.cities }
  233.         self.win = False # Уже победили?
  234.         self.combat_log = list() # Лог сражений за один раунд (до следующего обновления карты)
  235.  
  236.         for city in self.cities.values():
  237.             city.transfer_to(self.fractions[city.meta.starting_fraction])
  238.  
  239.     def check_route(self, fraction, city):
  240.         """check_route(fraction, city) -> True или False
  241.        Проверяет доступность города для действий одной из фракций.
  242.  
  243.        fraction - FractionState, для какой из фракций город должен быть доступен?
  244.        city - CityState, город, который проверяется на доступность"""
  245.  
  246.         return fraction == city.fraction or any(self.cities[adjacent].fraction == fraction for adjacent in city.meta.routes)
  247.  
  248.     def __getstate__(self):
  249.         return {k: v for k, v in self.__dict__.items() if k not in ('meta')} # meta жирная, выставляется после загрузки вручную
  250.  
  251. class WorldChanges:
  252.     class Change:
  253.         def __init__(self, op, *args):
  254.             self.op = op
  255.             self.args = args
  256.  
  257.     def __init__(self, world):
  258.         self.cs, self.world = list(), world
  259.  
  260.     def fraction_total_cities(self, fraction):
  261.         cities_count = fraction.total_cities()
  262.         for c in self.cs:
  263.             if c.op == 'transfer_city':
  264.                 city, to_fraction = c.args
  265.                 if city.fraction == fraction: cities_count -= 1
  266.                 if to_fraction == fraction: cities_count += 1
  267.         return cities_count
  268.  
  269.     def post_transfer_city(self, city, fraction): self.cs.append(self.Change('transfer_city', city, fraction))
  270.     def post_set_city_forces(self, city, forces): self.cs.append(self.Change('set_city_forces', city, forces))
  271.  
  272.     def apply(self):
  273.         for c in self.cs:
  274.             getattr(self, '_apply_' + c.op)(*c.args)
  275.  
  276.     def _apply_transfer_city(self, city, fraction): city.transfer_to(fraction)
  277.     def _apply_set_city_forces(self, city, forces): city.forces = forces
  278.  
  279.  
  280. # Функции основных рассчётов
  281. def CheckPostNumber(post_number, post_number_length):
  282.     """CheckPostNumber(post_number, post_number_length) -> True или False
  283.    Проверяет правильность номера поста.
  284.  
  285.    post_number - строка, должна быть длиной в post_number_length символов и состоять только из цифр,
  286.                  иначе функция вернёт Flase;
  287.    post_number_length - целое число, допустимая длина номера поста."""
  288.  
  289.     return len(post_number) == post_number_length and all('0' <= c <= '9' for c in post_number)
  290.  
  291.  
  292. def ArmyForces(post_number):
  293.     """ArmyForces(post_number) -> army_forces
  294.    Возвращает целое число, обозначающее силу армии.
  295.  
  296.    post_number - строка, состоящая только из цифр и обозначающая номер поста."""
  297.  
  298.     digit_repeats = 0
  299.     last_digit = int(post_number[-1])
  300.     for digit in post_number[-2::-1]:
  301.         if int(digit) == last_digit: digit_repeats += 1
  302.         else: break
  303.     army_forces = 0 if digit_repeats == 0 else (last_digit ** (digit_repeats + 1)) + (digit_repeats * 10)
  304.     if digit_repeats > 1: army_forces += 10
  305.     return army_forces
  306.  
  307.  
  308. def Attack(name, fraction, army_forces, target):
  309.     """Attack(name, fraction, army_forces, target) -> <сообщение>, changes
  310.    Рассчитывает результаты атаки на город.
  311.    Возвращает строку, описывающую итог сражения, и WorldChanges — вызванные им изменения в мире, которые нужно применить через .apply().
  312.  
  313.    name - строка, имя генерала, совершающего атаку на город;
  314.    fraction - FractionState, фракция, которая совершает атаку на город;
  315.    army_forces - целое число, сила атакующей армии (см. описание функции ArmyForces);
  316.    target - CityState, город, на который совершается нападение."""
  317.  
  318.     message = name + " (" + fraction.meta.human_name + ")" + " атакует " + target.meta.human_name + " с армией " + str(army_forces) + ": "
  319.     changes = WorldChanges(target.world)
  320.     if fraction == target.fraction: # Город уже захвачен союзниками
  321.         changes.post_set_city_forces(target, target.forces + army_forces)
  322.         return message + "город уже захвачен союзниками, армия присоединяется к гарнизону.", changes
  323.  
  324.     army_forces_after = max(0, army_forces - (target.forces + target.fraction.defense_plus()) * target.fraction.defense_x())
  325.     army_losses = army_forces - army_forces_after
  326.  
  327.     garrison_before = target.forces
  328.     garrison_after = max(0, garrison_before - (army_forces + fraction.attack_plus()) * fraction.attack_x())
  329.  
  330.     if army_forces_after > 0: # Город захвачен
  331.         changes.post_transfer_city(target, fraction)
  332.         changes.post_set_city_forces(target, army_forces_after)
  333.         message += "город взят, потери атакующих " + str(army_losses) + "."
  334.     else: # Город не захвачен
  335.         changes.post_set_city_forces(target, garrison_after)
  336.         garrison_losses = garrison_before - garrison_after
  337.         message += "потери атакующих " + str(army_losses) + ", потери гарнизона " + str(garrison_losses) + "."
  338.     return message, changes
  339.  
  340.  
  341. def Defense(name, fraction, army_forces, target):
  342.     """Defense(name, fraction, army_forces, target, world) -> <сообщение>, changes
  343.    Рассчитывает результаты защиты на города.
  344.    Возвращает строку, описывающую результат действия, и WorldChanges — вызванные им изменения в мире, которые нужно применить через .apply().
  345.  
  346.    name - строка, имя генерала, совершающего защищающего город;
  347.    fraction - FractionState, фракция, которая защищает город;
  348.    army_forces - целое число, сила защищающей армии (см. описание функции ArmyForces);
  349.    target - CityState, защищаемый город."""
  350.  
  351.     if fraction != target.fraction: # Город уже захвачен врагом
  352.         message = name + " защищает " + target.meta.human_name + ": город уже захвачен врагами.\n"
  353.         post_message, changes = Attack(name, fraction, army_forces, target)
  354.         return message + post_message, changes
  355.     else:
  356.         changes = WorldChanges(target.world)
  357.         changes.post_set_city_forces(target, target.forces + army_forces)
  358.         message = (name + " (" + target.fraction.meta.human_name + ")" + " защищает " + target.meta.human_name + ": + "
  359.                    + str(army_forces) + " к силе гарнизона.")
  360.         return message, changes
  361.  
  362.  
  363. def ActionProcessing(post_number, post_number_length, fraction, name, action, target, options):
  364.     """ActionProcessing(post_number, post_number_length, fraction, name, action, target, options)
  365.       -> check, message
  366.    Рассчитывает результат действия игрока ("защита" или "нападение").
  367.    Возвращает булевый флаг (нет ли ошибок входных данных?), строку, описывающую ошибку или результат действия,
  368.    изменения в мире, которые нужно применить через .apply().
  369.  
  370.    post_number - строка, представляющая номер поста игрока,
  371.                  допустимость значений проверяется (см. функцию CheckPostNumber);
  372.    post_number_length - целое число, максимальная длина номера поста;
  373.    fraction - FractionState, фракция игрока;
  374.    name - строка, обозначающя имя игрока, совершающего действие, если пустая, будет использоваться стандартное имя
  375.           (см. глобальную перемунную DEFAULT_GENERAL_NAME);
  376.    action - строка, обозначающая действие ('attack' или 'defense');
  377.    target - CityState, город над которым производится действие
  378.             проверяется есть ли у фракции игрока доступ к этому городу (см. функцию check_route);
  379.    options - Options, опции."""
  380.  
  381.     if not CheckPostNumber(post_number, post_number_length):
  382.         return False, "Ошибка: недопустимый номер поста", None
  383.     if not target.world.check_route(fraction, target):
  384.         return False, "Ошибка: у фракции " + fraction.meta.human_name + " нет подхода к городу " + target.meta.human_name + ".", None
  385.     name = name.strip()
  386.     if not name:
  387.         name = options.DEFAULT_GENERAL_NAME
  388.     army_forces = ArmyForces(post_number)
  389.     if not army_forces:
  390.         return False, "Сила армии 0. Ничего не происходит.", None
  391.     if action == 'attack':
  392.         return True, *Attack(name, fraction, army_forces, target)
  393.     elif action == 'defense':
  394.         return True, *Defense(name, fraction, army_forces, target)
  395.  
  396. # Функция отрисовки карты
  397.  
  398. def MapRender(world, options):
  399.     """MapRender(world, options)
  400.    Рисует карту, соответствующую текущему состоянию игрового мира с логом событий и сохраняет в файл.
  401.  
  402.    world - WorldState, мир;
  403.    options - Options, опции."""
  404.  
  405.     folder = path.join(options.working_dir, "resources")
  406.     with Image.new("RGB", (1920, 1080), color=("#AAAAAA")) as game_map:
  407.         # Отрисовка фона
  408.         with Image.open(path.join(folder, options.BACKGROUND)) as background_image:
  409.             game_map.paste(background_image)
  410.  
  411.         # Отрисовка территорий, фортов и меток
  412.         for part_id in ('territory', 'fort', 'text'):
  413.             for city in world.cities.values():
  414.                 with Image.open(path.join(folder, city.id + "_" + part_id + "_" + city.fraction_id + ".png")) as part_image:
  415.                     game_map.paste(part_image, mask=part_image)
  416.  
  417.         game_map_draw = ImageDraw.Draw(game_map)
  418.         # Отрисовка значений силы гарнизона
  419.         font = ImageFont.truetype(path.join(folder, "FreeSansBoldOblique.ttf"), size=21)
  420.         for city in world.cities.values():
  421.             text = str(city.forces)
  422.             game_map_draw.text(city.meta.label_pos, text, font=font, fill=city.fraction.meta.label_color)
  423.  
  424.         # Отрисовка лога событий
  425.         font = ImageFont.truetype(path.join(folder, "FreeSansBold.ttf"), size=22)
  426.         text = "\n".join(world.combat_log).strip()
  427.         log_height = 1076 - font.getsize(text)[1] * (1 + text.count("\n"))
  428.         game_map_draw.text((4, log_height), text, font=font, fill="#FEFEFE")
  429.  
  430.         # Записываем отрисованную карту в файл
  431.         game_map.save(path.join(options.working_dir, "output", "bitva-bitardov.png"))
  432.  
  433. class GUI:
  434.     def __init__(self, world, options):
  435.         self.world = world
  436.         self.options = options
  437.  
  438.         # Инициализация GUI
  439.         self.main_window = main_window = tkinter.Tk()
  440.         main_window.config(bg=options.BG_COLOR)
  441.         main_window.title("Битва Битардов Director's Cut Edition Remastered v1.2")
  442.  
  443.         def add_label(text):
  444.             tkinter.Label(text=text, bg=options.BG_COLOR, fg=options.FG_COLOR, font=options.FONT_GUI).pack()
  445.  
  446.         def add_radiobutton(text, value, variable):
  447.             tkinter.Radiobutton(text=text, variable=variable, value=value, fg=options.FG_COLOR, bg=options.BG_COLOR, font=options.FONT_GUI).pack()
  448.  
  449.         def add_button(text, command):
  450.             tkinter.Button(main_window, text=text, fg=options.FG_COLOR, bg=options.BG_COLOR, font=options.FONT_GUI, command=command).pack()
  451.  
  452.         # Ввод номера поста
  453.         add_label("НОМЕР ПОСТА (" + str(options.POST_NUMBER_LENGTH) + " ЦИФР): ")
  454.         self.post_number_input = tkinter.StringVar()
  455.         post_number_entry = tkinter.Entry(textvariable=self.post_number_input, width=options.POST_NUMBER_LENGTH, bd=2, font=options.FONT_GUI)
  456.         post_number_entry.pack()
  457.  
  458.         # Выбор фракции
  459.         add_label("ФРАКЦИЯ: ")
  460.         self.fraction_input = tkinter.StringVar()
  461.         for index, (fraction_id, fraction_meta) in enumerate(world.meta.fractions.items()):
  462.             if index == 0: self.fraction_input.set(fraction_id)
  463.             add_radiobutton(text=fraction_meta.human_name, variable=self.fraction_input, value=fraction_id)
  464.  
  465.         # Ввод имени генерала
  466.         add_label("ИМЯ ГЕНЕРАЛА: ")
  467.         self.name_input = tkinter.StringVar()
  468.         name_input_entry = tkinter.Entry(textvariable=self.name_input, width=22, bd=2, font=options.FONT_GUI)
  469.         name_input_entry.pack()
  470.  
  471.         # Выбор действия атака или защита
  472.         add_label("ДЕЙСТВИЕ: ")
  473.         self.action_input = tkinter.StringVar()
  474.         self.action_input.set('defense')
  475.         add_radiobutton(text="Атака", variable=self.action_input, value='attack')
  476.         add_radiobutton(text="Защита", variable=self.action_input, value='defense')
  477.  
  478.         # Выбор города
  479.         add_label("ГОРОД: ")
  480.         self.target_input = tkinter.StringVar()
  481.         for index, (city_id, city_meta) in enumerate(sorted(world.meta.cities.items(), key=lambda kv: kv[1].radiobutton_order)):
  482.             if index == 0: self.target_input.set(city_id)
  483.             add_radiobutton(text=city_meta.human_name, variable=self.target_input, value=city_id)
  484.  
  485.         # GUI для пользовательских команд
  486.         # Рассчёт результата действия игрока (можно посмотреть и не применять изменения)
  487.         add_button(text="РАСЧЁТ РЕЗУЛЬТАТА", command=self.GUIActionResult)
  488.  
  489.         # Обновляет карту и перезаписывает файл "bitva-bitardov.png" в каталоге "output"
  490.         add_button(text="ОБНОВИТЬ КАРТУ", command=self.GUIUpdateMap)
  491.  
  492.         # Остановить битву после лимита постов (см. MAX_POST_COUNT)
  493.         add_button(text=str(options.MAX_POST_COUNT) + "-Й ПОСТ! ОСТАНОВИТЬ БИТВУ!", command=self.GUIStopBattle)
  494.  
  495.         # Сохранить текущее состояние игрового мира
  496.         add_button(text="СОХРАНИТЬ ИГРУ", command=self.GUISave)
  497.  
  498.         # Загрузить сохранённое состояние игрового мира
  499.         add_button(text="ЗАГРУЗИТЬ ИГРУ", command=self.GUILoad)
  500.  
  501.  
  502.     # Функции для обработки команд пользователя, принимаемых через GUI
  503.     def GUIActionResult(self):
  504.         """GUIActionResult()
  505.        Показывает окно с результатами действия игрока (см. функцию ActionProcessing).
  506.        Пользователь может принять изменения или отменить.
  507.        В первом случае изменения применяются к миру, во втором случае он не меняется.
  508.        Возвращает None если функция была вызвана после того как битва уже закончилась (world.win == True)."""
  509.  
  510.         if self.world.win: # Проверка победы
  511.             tkinter.messagebox.showwarning("Битва закончилась", "Эта Битва Битардов уже завершилась.")
  512.             return None
  513.  
  514.         check, message, changes = ActionProcessing(self.post_number_input.get(), self.options.POST_NUMBER_LENGTH,
  515.                                                    self.world.fractions[self.fraction_input.get()], self.name_input.get(),
  516.                                                    self.action_input.get(), self.world.cities[self.target_input.get()],
  517.                                                    self.options)
  518.  
  519.         if not check:
  520.             tkinter.messagebox.showerror("Ошибка", message)
  521.         else:
  522.             # Проверка на победу одной из фракций
  523.             winner = None
  524.             loser = next((fraction for fraction in self.world.fractions.values() if changes.fraction_total_cities(fraction) == 0), None)
  525.             if loser:
  526.                 winner = next(fraction for fraction in self.world.fractions.values() if fraction != loser)
  527.                 message += ("\nФракции " + winner.meta.human_name + " теперь принадлежат все города."
  528.                             + "\n" + winner.meta.human_name + " побеждает " + loser.meta.accusative + "!")
  529.  
  530.             if tkinter.messagebox.askyesno("Принять изменения карты?", message + "\n\nПринять изменения карты?"):
  531.                 changes.apply()
  532.                 self.world.combat_log.append(message)
  533.                 if winner is not None: self.world.win = True
  534.  
  535.     def GUIStopBattle(self):
  536.         """GUIStopBattle()
  537.        Показывает окно с итогами битвы после лимита постов. Пользователь может принять изменения или отменить.
  538.        Если пользователь принимает изменения, world.win и world.combat_log обновляются.
  539.        Фракция с наибольшим значением общей силы "total_forces" побеждает,
  540.        либо объявляется ничья, если силы обеих фракций равны.
  541.        Возвращает None если функция была вызвана после того как битва уже закончилась (world.win == True)."""
  542.  
  543.         if self.world.win: # Проверка победы
  544.             tkinter.messagebox.showwarning("Битва закончилась", "Эта Битва Битардов уже завершилась.")
  545.             return None
  546.  
  547.         def describe_fraction_forces(fraction):
  548.             return "Общая сила фракции " + fraction.meta.human_name + ": " + str(fraction.total_forces()) + "."
  549.         message = ("Битва останавливается после " + str(self.options.MAX_POST_COUNT) + "-ого поста.\n"
  550.                    + " ".join(describe_fraction_forces(fraction) for fraction in self.world.fractions.values()))
  551.  
  552.         winners, losers, best_forces = [], [], None
  553.         for fraction in self.world.fractions.values():
  554.             cur_forces = fraction.total_forces()
  555.             if best_forces == None or best_forces > cur_forces:
  556.                 losers.extend(winners)
  557.                 winners = []
  558.                 best_forces = cur_forces
  559.             (winners if best_forces == cur_forces else losers).append(fraction)
  560.  
  561.         if winners and losers:
  562.             def join(fractions, meta_attr):
  563.                 return ", ".join(getattr(fraction.meta, meta_attr) for fraction in fractions)
  564.             message += "\n" + join(winners, 'human_name') + " побеждает " + join(losers, 'accusative') + "!"
  565.         else:
  566.             message += "\nПобедила дружба!"
  567.         if tkinter.messagebox.askyesno("Остановить Битву Битардов?", message + "\n\nОстановить битву и применить изменения?"):
  568.             self.world.win = True
  569.             self.world.combat_log.append(message)
  570.  
  571.     def GUIUpdateMap(self):
  572.         """Обновляет игровую карту и записывает в файл "bitva-bitardov.png" в каталоге "output".
  573.        Если файл уже существует, он перезаписывается.
  574.        В случае ошибок при работе с файлами показывает сообщение об ошибке. При этом лог событий не очищается."""
  575.  
  576.         try:
  577.             MapRender(self.world, self.options)
  578.         except Exception as e:
  579.             tkinter.messagebox.showerror("Ошибка", e)
  580.         else:
  581.             self.world.combat_log.clear()
  582.  
  583.     def GUISave(self):
  584.         """Записывает состояние мира в файл, выбранный пользователем.
  585.           По умолчанию предлагается сохранять в каталог "saves"."""
  586.  
  587.         try:
  588.             save_file = tkinter.filedialog.asksaveasfile(mode="wb",
  589.                                                          title="Сохранение", initialdir=path.join(self.options.working_dir, "saves"),
  590.                                                          defaultextension=".bbsav",
  591.                                                          filetypes=(("Дамп состояния игрового мира", "*.bbsav"),))
  592.             if not save_file: return
  593.             with save_file:
  594.                 pickle.dump(self.world, save_file)
  595.         except Exception as error:
  596.             tkinter.messagebox.showerror("Ошибка", error)
  597.         else:
  598.             tkinter.messagebox.showinfo("Сохранение", "Игра сохранена")
  599.  
  600.     def GUILoad(self):
  601.         """Загружает состояние мира из файла который выберет пользователь.
  602.           По умолчанию предлагается загружать сохранения из каталога "saves"."""
  603.  
  604.         try:
  605.             load_file = tkinter.filedialog.askopenfile(mode="rb",
  606.                                                        title="Загрузка", initialdir=path.join(self.options.working_dir, "saves"),
  607.                                                        filetypes=(("Дамп состояния игрового мира", "*.bbsav"),))
  608.             if not load_file: return
  609.             with load_file:
  610.                 new_world = pickle.load(load_file)
  611.             new_world.meta = self.world.meta
  612.         except Exception as error:
  613.             tkinter.messagebox.showerror("Ошибка", error)
  614.         else:
  615.             self.world = new_world
  616.             tkinter.messagebox.showinfo("Загрузка", "Игра загружена")
  617.  
  618. def main():
  619.     options = Options()
  620.     world = WorldState(WorldMeta())
  621.     GUI(world, options).main_window.mainloop()
  622.  
  623. if __name__ == "__main__":
  624.     main()
RAW Paste Data