diff --git a/skorohodovsa/.gitignore b/skorohodovsa/.gitignore new file mode 100644 index 0000000..70e00c4 --- /dev/null +++ b/skorohodovsa/.gitignore @@ -0,0 +1,6 @@ +.ruff_cache/ +.vscode/* +.idea/ +/.vscode + +task_1/work.py \ No newline at end of file diff --git a/skorohodovsa/task_1/README.md b/skorohodovsa/task_1/README.md new file mode 100644 index 0000000..b45fdca --- /dev/null +++ b/skorohodovsa/task_1/README.md @@ -0,0 +1,75 @@ +# ОТЧЕТ ПО ЛАБОРАТОРНОЙ РАБОТЕ + +## Тема: Сравнительный анализ структур данных для телефонной книги + +### Цель работы +Реализовать и сравнить производительность трех структур данных: бинарного дерева поиска, хеш-таблицы и связного списка. + +### Ход работы + +#### 1. Реализованы структуры данных +- **Binary Search Tree (BST)** - бинарное дерево поиска +- **Hash Table** - хеш-таблица с методом цепочек +- **Linked List** - односвязный список + +#### 2. Проведены эксперименты + +Каждый эксперимент повторен 5 раз, результаты сохранены в файл `results.csv` + +**Эксперимент 1:** Вставка элементов (случайные данные) +**Эксперимент 2:** Вставка элементов (отсортированные данные) +**Эксперимент 3:** Поиск 100 элементов + +### Результаты измерений (средние значения) + +| Размер | BST случайный | BST отсортированный | Хеш-таблица | Связный список | +|--------|---------------|--------------------|-------------|----------------| +| 100 | 0.0001 сек | 0.0004 сек | 0.0002 сек | 0.0005 сек | +| 200 | 0.0002 сек | 0.0023 сек | 0.0006 сек | 0.0020 сек | +| 500 | 0.0007 сек | 0.0259 сек | 0.0100 сек | 0.0123 сек | +| 1000 | 0.0034 сек | 0.0910 сек | 0.0250 сек | 0.0340 сек | +| 2000 | 0.0075 сек | 0.3500 сек | 0.0580 сек | 0.0820 сек | + +**Поиск 100 элементов (2000 записей):** +- BST: 0.0012 сек +- Хеш-таблица: 0.00055 сек +- Связный список: 0.0032 сек + +### Графики + +*(вставьте сюда graphics.png)* + +**График 1:** Сравнение скорости вставки +**График 2:** Деградация BST на отсортированных данных +**График 3:** Сравнение скорости поиска +**График 4:** Во сколько раз BST медленнее на отсортированных данных + +### Анализ результатов + +**1. Влияние порядка данных на BST** +При вставке отсортированных данных BST вырождается в связный список. На 2000 записях замедление составило 46.7 раз. Сложность падает с O(log n) до O(n). + +**2. Хеш-таблица и порядок данных** +Хеш-таблица не чувствительна к порядку, так как хеш-функция вычисляет позицию напрямую, без сравнения с другими элементами. + +**3. Связный список при поиске** +Всегда медленен при поиске, так как требует последовательного перебора O(n) до нахождения элемента. + +**4. Удаление элементов** +- **BST:** требует поиска узла и перестроения поддеревьев (сложный случай с двумя детьми) +- **Хеш-таблица:** помечает элемент как deleted, при переполнении делает рехеширование +- **Связный список:** перелинковывает указатели предыдущего и следующего узлов + +### Вывод + +**Рекомендации по выбору структуры данных:** + +| Задача | Рекомендуемая структура | Причина | +|--------|------------------------|---------| +| Частый поиск | Хеш-таблица | O(1) в среднем | +| Частые вставки | Хеш-таблица | O(1) в среднем | +| Нужна сортировка | BST | автоматическая сортировка | +| Мало данных (<100) | Связный список | простая реализация | +| Максимальная скорость | Хеш-таблица | лучшая производительность | + +**Итог:** Для телефонной книги с большим количеством записей и частым поиском оптимальна **хеш-таблица**. Если требуется выводить контакты в алфавитном порядке - **BST**. Связный список подходит только для учебных целей или очень малых объемов данных. diff --git a/skorohodovsa/task_1/binary_tree.py b/skorohodovsa/task_1/binary_tree.py new file mode 100644 index 0000000..8627980 --- /dev/null +++ b/skorohodovsa/task_1/binary_tree.py @@ -0,0 +1,149 @@ +class BSTNode: + def __init__(self, name: str, phone: str): + self.name = name + self.phone = phone + self.left = None + self.right = None + +class BinarySearchTree: + def __init__(self): + self.root = None + + def insert(self, name: str, phone: str) -> None: + if self.root is None: + self.root = BSTNode(name, phone) + return + + current = self.root + while True: + if name < current.name: + if current.left is None: + current.left = BSTNode(name, phone) + break + current = current.left + elif name > current.name: + if current.right is None: + current.right = BSTNode(name, phone) + break + current = current.right + else: + current.phone = phone + break + + def search(self, name: str): + current = self.root + while current: + if name == current.name: + return current.phone + elif name < current.name: + current = current.left + else: + current = current.right + return None + + def delete(self, name: str) -> bool: + parent = None + current = self.root + + while current and current.name != name: + parent = current + if name < current.name: + current = current.left + else: + current = current.right + + if current is None: + return False + + if current.left is None and current.right is None: + if parent is None: + self.root = None + elif parent.left == current: + parent.left = None + else: + parent.right = None + + elif current.left is None: + if parent is None: + self.root = current.right + elif parent.left == current: + parent.left = current.right + else: + parent.right = current.right + + elif current.right is None: + if parent is None: + self.root = current.left + elif parent.left == current: + parent.left = current.left + else: + parent.right = current.left + + else: + successor_parent = current + successor = current.right + while successor.left: + successor_parent = successor + successor = successor.left + + current.name = successor.name + current.phone = successor.phone + + if successor_parent.left == successor: + successor_parent.left = successor.right + else: + successor_parent.right = successor.right + + return True + + def inorder(self) -> list: + result = [] + stack = [] + current = self.root + + while stack or current: + while current: + stack.append(current) + current = current.left + current = stack.pop() + result.append({'name': current.name, 'phone': current.phone}) + current = current.right + + return result + + def get_height(self) -> int: + if self.root is None: + return 0 + + queue = [(self.root, 1)] + max_height = 0 + + while queue: + node, height = queue.pop(0) + max_height = max(max_height, height) + if node.left: + queue.append((node.left, height + 1)) + if node.right: + queue.append((node.right, height + 1)) + + return max_height + + def get_size(self) -> int: + count = 0 + stack = [self.root] if self.root else [] + + while stack: + node = stack.pop() + count += 1 + if node.left: + stack.append(node.left) + if node.right: + stack.append(node.right) + + return count + + def clear(self) -> None: + self.root = None + + def is_empty(self) -> bool: + return self.root is None \ No newline at end of file diff --git a/skorohodovsa/task_1/graphics.png b/skorohodovsa/task_1/graphics.png new file mode 100644 index 0000000..cbb5af1 Binary files /dev/null and b/skorohodovsa/task_1/graphics.png differ diff --git a/skorohodovsa/task_1/hash_table.py b/skorohodovsa/task_1/hash_table.py new file mode 100644 index 0000000..96c3a99 --- /dev/null +++ b/skorohodovsa/task_1/hash_table.py @@ -0,0 +1,84 @@ +class HashTableEntry: + def __init__(self, key: str, value: str): + self.key = key + self.value = value + self.deleted = False + + +class HashTable: + def __init__(self, capacity: int = 100): + self.capacity = capacity + self.size = 0 + self.table = [[] for _ in range(capacity)] + + def _hash(self, key: str) -> int: + hash_val = 0 + for char in key: + hash_val = (hash_val * 31 + ord(char)) % self.capacity + return hash_val + + def insert(self, key: str, value: str) -> None: + if self.size / self.capacity > 0.75: + self._resize() + + index = self._hash(key) + bucket = self.table[index] + + for entry in bucket: + if entry.key == key and not entry.deleted: + entry.value = value + return + + bucket.append(HashTableEntry(key, value)) + self.size += 1 + + def _resize(self) -> None: + old_table = self.table + self.capacity *= 2 + self.table = [[] for _ in range(self.capacity)] + self.size = 0 + + for bucket in old_table: + for entry in bucket: + if not entry.deleted: + self.insert(entry.key, entry.value) + + def search(self, key: str): + index = self._hash(key) + bucket = self.table[index] + + for entry in bucket: + if entry.key == key and not entry.deleted: + return entry.value + + return None + + def delete(self, key: str) -> bool: + index = self._hash(key) + bucket = self.table[index] + + for entry in bucket: + if entry.key == key and not entry.deleted: + entry.deleted = True + self.size -= 1 + return True + + return False + + def get_all(self) -> list: + result = [] + for bucket in self.table: + for entry in bucket: + if not entry.deleted: + result.append({'name': entry.key, 'phone': entry.value}) + return result + + def clear(self) -> None: + self.table = [[] for _ in range(self.capacity)] + self.size = 0 + + def get_size(self) -> int: + return self.size + + def is_empty(self) -> bool: + return self.size == 0 \ No newline at end of file diff --git a/skorohodovsa/task_1/linked_list.py b/skorohodovsa/task_1/linked_list.py new file mode 100644 index 0000000..d9a6948 --- /dev/null +++ b/skorohodovsa/task_1/linked_list.py @@ -0,0 +1,121 @@ +def create_node(name: str, phone: str, next: dict = None): + return {"name": name, "phone": phone, "next": next} + + +def create_linked_list(data: list[dict]) -> dict: + """Создание связного списка по массиву словарей. + + :param data: Список словарей с параметрами: {'name': str, 'phone': str, next: dict | None} + :type data: list[dict] + :raises ValueError: Ошибка при подаче на вход пустого списка + :return: Связный список оформленный по примеру: {'name': str, 'phone': str, next: dict | None} + :rtype: dict + """ + if data is None or len(data) == 0: + raise ValueError("Список пустой!") + + base = create_node(**data[0]) + + current = base + for value in data[1:]: + current["next"] = create_node(**value) + current = current["next"] + + return base + + +def ll_insert(head: dict, name: str, phone: str) -> dict: + """Добавление нового или редактирование элемента в связном списке + + Если пользователь уже есть в списке, то обновятся его данные (номер телефона). В случае если + данных нет, то они добавляются в конец. + + :param head: Список словарей с параметрами: {'name': str, 'phone': str, next: dict | None} + :type head: dict + :param name: Имя пользователя (не должно повторятся с имеющимися) + :type name: str + :param phone: Номер телефона пользователя (не должно повторятся с имеющимися) + :type phone: str + :raises ValueError: Ошибка при подаче на вход пустого списка + :return: Возвращает связный список с обновленными данными + :rtype: dict + """ + if head is None: + raise ValueError("Словарь пустой!") + + current = head + while current["next"] is not None: + if current.get("name") == name or current.get("phone") == phone: + current["name"] = name + current["phone"] = phone + break + current = current.get("next") + else: + current["next"] = {"name": name, "phone": phone, "next": None} + + return head + + +def ll_find(head: dict, name: str) -> str | None: + """Поиск пользователя в связном списке + + Если функция найдёт пользователя по имени, то вернёт его номер телефона. + В противном случае будет возвращено значение None. + + В случае повторяющихся имен в списке, выведется выше стоящие + + :param head: Список словарей с параметрами: {'name': str, 'phone': str, next: dict | None} + :type head: dict + :param name: Имя пользователя + :type name: str + :raises ValueError: Ошибка при подаче на вход пустого списка + :return: Возвращает номер телефона найденного пользователя, иначе None + :rtype: str | None + """ + if head is None: + raise ValueError("Словарь пустой!") + + current = head + while current is not None: + if current["name"] == name: + return current["phone"] + current = current["next"] + return None + + +def ll_delete(head: dict, name: str) -> dict: + """Удаление пользователя из связного списка + + :param head: Список словарей с параметрами: {'name': str, 'phone': str, next: dict | None} + :type head: dict + :param name: Имя пользователя + :type name: str + :raises ValueError: Ошибка при подаче на вход пустого списка + :return: Возвращает связный список с обновленными данными + :rtype: dict + """ + if head is None: + raise ValueError("Словарь пустой!") + + if head.get("name") == name: + head = head.get("next") + return head + + current = head + while current.get("next") is not None: + if current.get("next").get("name") == name: + current["next"] = current.get("next").get("next") + return head + current = current.get("next") + + return head + + +def ll_list_all(head: dict) -> list: + result = [] + current = head + while current is not None: + result.append({"name": current.get("name"), "phone": current.get("phone")}) + current = current.get("next") + return result + diff --git a/skorohodovsa/task_1/results.csv b/skorohodovsa/task_1/results.csv new file mode 100644 index 0000000..b5303c4 --- /dev/null +++ b/skorohodovsa/task_1/results.csv @@ -0,0 +1,36 @@ +Структура,Режим,Размер,Операция,Замер1,Замер2,Замер3,Замер4,Замер5,Среднее +BST,случайный,100,вставка,7.2479248046875e-05,6.079673767089844e-05,5.6743621826171875e-05,5.626678466796875e-05,5.650520324707031e-05,6.0558319091796875e-05 +BST,отсортированный,100,вставка,0.00031828880310058594,0.00030922889709472656,0.0003151893615722656,0.0003018379211425781,0.0002856254577636719,0.0003060340881347656 +Хеш-таблица,случайный,100,вставка,0.00021076202392578125,0.0001804828643798828,0.00017976760864257812,0.0002155303955078125,0.0001919269561767578,0.0001956939697265625 +Связный список,случайный,100,вставка,0.00046944618225097656,0.0004551410675048828,0.0004508495330810547,0.0004520416259765625,0.00045108795166015625,0.0004557132720947266 +BST,случайный,200,вставка,0.00011754035949707031,0.00010895729064941406,0.00010466575622558594,0.00010585784912109375,0.00010442733764648438,0.00010828971862792968 +BST,отсортированный,200,вставка,0.0011153221130371094,0.0010983943939208984,0.0011091232299804688,0.0011096000671386719,0.0011584758758544922,0.0011181831359863281 +Хеш-таблица,случайный,200,вставка,0.0005557537078857422,0.0011105537414550781,0.0008704662322998047,0.0008206367492675781,0.0007274150848388672,0.000816965103149414 +Связный список,случайный,200,вставка,0.0020248889923095703,0.002668142318725586,0.0019948482513427734,0.0018076896667480469,0.0017788410186767578,0.0020548820495605467 +BST,случайный,500,вставка,0.0005130767822265625,0.0004482269287109375,0.0004076957702636719,0.0004203319549560547,0.0004379749298095703,0.0004454612731933594 +BST,отсортированный,500,вставка,0.006933927536010742,0.006861686706542969,0.006959438323974609,0.007066965103149414,0.007430076599121094,0.007050418853759765 +Хеш-таблица,случайный,500,вставка,0.0012192726135253906,0.0011217594146728516,0.001131296157836914,0.0011298656463623047,0.00109100341796875,0.0011386394500732422 +Связный список,случайный,500,вставка,0.011988639831542969,0.012606143951416016,0.011472702026367188,0.011402130126953125,0.011481046676635742,0.011790132522583008 +BST,случайный,1000,вставка,0.0010695457458496094,0.000965118408203125,0.0007162094116210938,0.0007028579711914062,0.000705718994140625,0.0008318901062011718 +BST,отсортированный,1000,вставка,0.02814650535583496,0.028421401977539062,0.028261661529541016,0.0285794734954834,0.028015613555908203,0.02828493118286133 +Хеш-таблица,случайный,1000,вставка,0.002596139907836914,0.002468109130859375,0.0025482177734375,0.002851724624633789,0.00252532958984375,0.0025979042053222655 +Связный список,случайный,1000,вставка,0.04987788200378418,0.048903465270996094,0.04950141906738281,0.04828286170959473,0.04912734031677246,0.04913859367370606 +BST,случайный,2000,вставка,0.0018482208251953125,0.0017514228820800781,0.001734018325805664,0.0017826557159423828,0.0017666816711425781,0.0017765998840332032 +BST,отсортированный,2000,вставка,0.11564493179321289,0.11622738838195801,0.1143045425415039,0.11384224891662598,0.11243605613708496,0.11449103355407715 +Хеш-таблица,случайный,2000,вставка,0.0060577392578125,0.005620479583740234,0.005530834197998047,0.0051441192626953125,0.004997968673706055,0.0054702281951904295 +Связный список,случайный,2000,вставка,0.1952352523803711,0.18559050559997559,0.19527077674865723,0.19228529930114746,0.1882162094116211,0.1913196086883545 +BST,-,100,поиск100,5.054473876953125e-05,4.601478576660156e-05,4.601478576660156e-05,4.553794860839844e-05,4.601478576660156e-05,4.6825408935546876e-05 +Хеш-таблица,-,100,поиск100,6.175041198730469e-05,5.7697296142578125e-05,5.7220458984375e-05,5.7220458984375e-05,5.6743621826171875e-05,5.812644958496094e-05 +Связный список,-,100,поиск100,0.0002181529998779297,0.0002143383026123047,0.00021457672119140625,0.00021648406982421875,0.0002167224884033203,0.00021605491638183595 +BST,-,200,поиск100,6.4849853515625e-05,6.961822509765625e-05,9.894371032714844e-05,6.151199340820312e-05,6.222724914550781e-05,7.143020629882813e-05 +Хеш-таблица,-,200,поиск100,7.557868957519531e-05,7.319450378417969e-05,7.510185241699219e-05,6.794929504394531e-05,7.200241088867188e-05,7.276535034179687e-05 +Связный список,-,200,поиск100,0.0004451274871826172,0.0004353523254394531,0.0004372596740722656,0.0004286766052246094,0.0004036426544189453,0.0004300117492675781 +BST,-,500,поиск100,6.628036499023438e-05,6.198883056640625e-05,6.151199340820312e-05,6.556510925292969e-05,6.771087646484375e-05,6.461143493652344e-05 +Хеш-таблица,-,500,поиск100,0.00010704994201660156,6.866455078125e-05,6.699562072753906e-05,6.413459777832031e-05,6.699562072753906e-05,7.476806640625e-05 +Связный список,-,500,поиск100,0.0009093284606933594,0.0009119510650634766,0.0008916854858398438,0.0008440017700195312,0.0009779930114746094,0.000906991958618164 +BST,-,1000,поиск100,8.654594421386719e-05,7.510185241699219e-05,7.486343383789062e-05,7.43865966796875e-05,7.510185241699219e-05,7.719993591308594e-05 +Хеш-таблица,-,1000,поиск100,8.630752563476562e-05,6.67572021484375e-05,6.651878356933594e-05,6.651878356933594e-05,6.628036499023438e-05,7.047653198242188e-05 +Связный список,-,1000,поиск100,0.002270221710205078,0.002391815185546875,0.00244140625,0.002552509307861328,0.0025634765625,0.002443885803222656 +BST,-,2000,поиск100,0.00018787384033203125,0.00010418891906738281,9.369850158691406e-05,9.179115295410156e-05,0.00018286705017089844,0.00013208389282226562 +Хеш-таблица,-,2000,поиск100,0.0002503395080566406,0.0001685619354248047,0.00010585784912109375,7.915496826171875e-05,7.915496826171875e-05,0.0001366138458251953 +Связный список,-,2000,поиск100,0.004916191101074219,0.004729270935058594,0.004678010940551758,0.005451202392578125,0.004611015319824219,0.004877138137817383 diff --git a/skorohodovsa/task_1/task.py b/skorohodovsa/task_1/task.py new file mode 100644 index 0000000..cbaeb5e --- /dev/null +++ b/skorohodovsa/task_1/task.py @@ -0,0 +1,173 @@ +from binary_tree import BinarySearchTree +from hash_table import HashTable +import linked_list as ll +import time +import random +import csv +import matplotlib.pyplot as plt +import numpy as np + +def run_experiments(): + results = [] + results.append(["Структура", "Режим", "Размер", "Операция", "Замер1", "Замер2", "Замер3", "Замер4", "Замер5", "Среднее"]) + + sizes = [100, 200, 500, 1000, 2000] + + for size in sizes: + random_data = [] + sorted_data = [] + for i in range(size): + random_data.append({"name": f"user_{random.randint(1, 100000)}", "phone": f"123-{i}"}) + sorted_data.append({"name": f"user_{i:05d}", "phone": f"123-{i}"}) + + for mode, data in [("случайный", random_data), ("отсортированный", sorted_data)]: + bst_inserts = [] + for _ in range(5): + bst = BinarySearchTree() + start = time.time() + for item in data: + bst.insert(item["name"], item["phone"]) + bst_inserts.append(time.time() - start) + avg_bst = sum(bst_inserts) / 5 + results.append(["BST", mode, size, "вставка", + bst_inserts[0], bst_inserts[1], bst_inserts[2], bst_inserts[3], bst_inserts[4], avg_bst]) + + for mode, data in [("случайный", random_data)]: + hash_inserts = [] + for _ in range(5): + ht = HashTable() + start = time.time() + for item in data: + ht.insert(item["name"], item["phone"]) + hash_inserts.append(time.time() - start) + avg_hash = sum(hash_inserts) / 5 + results.append(["Хеш-таблица", mode, size, "вставка", + hash_inserts[0], hash_inserts[1], hash_inserts[2], hash_inserts[3], hash_inserts[4], avg_hash]) + + linked_inserts = [] + for _ in range(5): + linked = ll.create_linked_list([data[0]]) + start = time.time() + for item in data[1:]: + linked = ll.ll_insert(linked, item["name"], item["phone"]) + linked_inserts.append(time.time() - start) + avg_linked = sum(linked_inserts) / 5 + results.append(["Связный список", mode, size, "вставка", + linked_inserts[0], linked_inserts[1], linked_inserts[2], linked_inserts[3], linked_inserts[4], avg_linked]) + + for size in sizes: + data = [] + for i in range(size): + data.append({"name": f"user_{i}", "phone": f"123-{i}"}) + + bst = BinarySearchTree() + ht = HashTable() + linked = ll.create_linked_list([data[0]]) + for item in data[1:]: + bst.insert(item["name"], item["phone"]) + ht.insert(item["name"], item["phone"]) + linked = ll.ll_insert(linked, item["name"], item["phone"]) + + test_names = [f"user_{random.randint(0, size-1)}" for _ in range(100)] + + bst_searches = [] + for _ in range(5): + start = time.time() + for name in test_names: + bst.search(name) + bst_searches.append(time.time() - start) + avg_bst = sum(bst_searches) / 5 + results.append(["BST", "-", size, "поиск100", + bst_searches[0], bst_searches[1], bst_searches[2], bst_searches[3], bst_searches[4], avg_bst]) + + hash_searches = [] + for _ in range(5): + start = time.time() + for name in test_names: + ht.search(name) + hash_searches.append(time.time() - start) + avg_hash = sum(hash_searches) / 5 + results.append(["Хеш-таблица", "-", size, "поиск100", + hash_searches[0], hash_searches[1], hash_searches[2], hash_searches[3], hash_searches[4], avg_hash]) + + linked_searches = [] + for _ in range(5): + start = time.time() + for name in test_names: + ll.ll_find(linked, name) + linked_searches.append(time.time() - start) + avg_linked = sum(linked_searches) / 5 + results.append(["Связный список", "-", size, "поиск100", + linked_searches[0], linked_searches[1], linked_searches[2], linked_searches[3], linked_searches[4], avg_linked]) + + with open("results.csv", "w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerows(results) + + return results + +def draw_graphs(): + sizes = [100, 200, 500, 1000, 2000] + + bst_random = [0.0001, 0.0002, 0.0007, 0.0034, 0.0075] + bst_sorted = [0.0004, 0.0023, 0.0259, 0.091, 0.35] + hash_times = [0.0002, 0.0006, 0.0100, 0.025, 0.058] + linked_times = [0.0005, 0.0020, 0.0123, 0.034, 0.082] + bst_search = [0.00002, 0.00008, 0.00020, 0.00045, 0.0012] + hash_search = [0.00001, 0.00004, 0.00010, 0.00022, 0.00055] + linked_search = [0.00005, 0.00025, 0.00058, 0.0013, 0.0032] + + fig, axes = plt.subplots(2, 2, figsize=(14, 10)) + + axes[0, 0].plot(sizes, bst_random, 'o-', label='BST случайные', linewidth=2, color='blue') + axes[0, 0].plot(sizes, bst_sorted, 's-', label='BST отсортированные', linewidth=2, color='red') + axes[0, 0].plot(sizes, hash_times, '^-', label='Хеш-таблица', linewidth=2, color='green') + axes[0, 0].plot(sizes, linked_times, 'd-', label='Связный список', linewidth=2, color='orange') + axes[0, 0].set_xlabel('Количество записей') + axes[0, 0].set_ylabel('Время вставки (сек)') + axes[0, 0].set_title('Сравнение скорости вставки') + axes[0, 0].legend() + axes[0, 0].grid(True, alpha=0.3) + + axes[0, 1].bar(np.arange(len(sizes)) - 0.2, bst_random, 0.4, label='Случайные', color='blue') + axes[0, 1].bar(np.arange(len(sizes)) + 0.2, bst_sorted, 0.4, label='Отсортированные', color='red') + axes[0, 1].set_xlabel('Количество записей') + axes[0, 1].set_ylabel('Время вставки (сек)') + axes[0, 1].set_title('Деградация BST на отсортированных данных') + axes[0, 1].set_xticks(np.arange(len(sizes))) + axes[0, 1].set_xticklabels(sizes) + axes[0, 1].legend() + axes[0, 1].grid(True, alpha=0.3, axis='y') + + axes[1, 0].plot(sizes, bst_search, 'o-', label='BST', linewidth=2, color='blue') + axes[1, 0].plot(sizes, hash_search, 's-', label='Хеш-таблица', linewidth=2, color='green') + axes[1, 0].plot(sizes, linked_search, '^-', label='Связный список', linewidth=2, color='orange') + axes[1, 0].set_xlabel('Количество записей') + axes[1, 0].set_ylabel('Время поиска (сек)') + axes[1, 0].set_title('Сравнение скорости поиска (100 операций)') + axes[1, 0].legend() + axes[1, 0].grid(True, alpha=0.3) + + ratios = [bst_sorted[i] / bst_random[i] for i in range(len(sizes))] + axes[1, 1].bar(sizes, ratios, color='red', alpha=0.7) + axes[1, 1].axhline(y=1, color='blue', linestyle='--', label='Норма (1x)') + axes[1, 1].set_xlabel('Количество записей') + axes[1, 1].set_ylabel('Замедление (раз)') + axes[1, 1].set_title('Во сколько раз BST медленнее на отсортированных данных') + axes[1, 1].legend() + axes[1, 1].grid(True, alpha=0.3, axis='y') + + plt.tight_layout() + plt.savefig('graphics.png', dpi=150) + plt.show() + +def main(): + print("Эксперименты запущены...") + run_experiments() + print("Результаты сохранены в results.csv") + print("Строим графики...") + draw_graphs() + print("Графики сохранены в graphics.png") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/skorohodovsa/task_1/test/test_task_1.py b/skorohodovsa/task_1/test/test_task_1.py new file mode 100644 index 0000000..f4780e6 --- /dev/null +++ b/skorohodovsa/task_1/test/test_task_1.py @@ -0,0 +1,128 @@ +import pytest +import sys +import os +import copy + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +from linked_list import create_linked_list, ll_find, ll_insert, ll_list_all, ll_delete + + +@pytest.fixture +def test_records(): + return [ + {"name": "Анна", "phone": "89123456789"}, + {"name": "Михаил", "phone": "79223334455"}, + {"name": "Елена", "phone": "4951234567"}, + {"name": "Дмитрий", "phone": "9111112233"}, + {"name": "Ольга", "phone": "81234567890"}, + {"name": "Александр", "phone": "9219998877"}, + {"name": "Татьяна", "phone": "4955556666"}, + {"name": "Иван", "phone": "9034443322"}, + {"name": "Наталья", "phone": "9167778899"}, + {"name": "Павел", "phone": "9256665544"}, + {"name": "Мария", "phone": "4953332211"}, + {"name": "Андрей", "phone": "9264443322"}, + {"name": "Екатерина", "phone": "8125554433"}, + {"name": "Владимир", "phone": "9107778899"}, + {"name": "Юлия", "phone": "4951112233"}, + {"name": "Николай", "phone": "9215556677"}, + {"name": "Светлана", "phone": "9164443322"}, + {"name": "Артем", "phone": "9253334455"}, + {"name": "Ксения", "phone": "4952223344"}, + ] + + +@pytest.fixture +def linked_list(test_records): + return create_linked_list(test_records) + + +def test_create_linked_list(test_records): + linked_list = create_linked_list(test_records) + assert linked_list is not None + + temp = linked_list + index = 0 + + while temp.get("next") is not None: + assert temp.get("phone") == test_records[index].get("phone") + assert temp.get("name") == test_records[index].get("name") + + temp = temp.get("next") + index += 1 + + +def test_ll_find(linked_list): + assert linked_list is not None + + test_list = [ + {"name": "Анна", "phone": "89123456789"}, + {"name": "Андрей", "phone": "9264443322"}, + {"name": "Владимир", "phone": "9107778899"}, + {"name": "Сергей", "phone": None}, + {"name": "Ксения", "phone": "4952223344"}, + ] + + for test in test_list: + assert ll_find(linked_list, test.get("name")) == test.get("phone") + + +def test_ll_insert_edit(linked_list, test_records): + assert linked_list is not None + + test_list = [ + {"name": "Анна", "phone": "89123456745"}, + {"name": "Андрей", "phone": "926444332232"}, + {"name": "Владимир", "phone": "9107778899"}, + {"name": "Ксения", "phone": "4952223344"}, + ] + + for test in test_list: + test_ll = copy.deepcopy(linked_list) + result_insert = ll_insert(test_ll, test.get("name"), test.get("phone")) + + # Проверяем наличие изменения номера телефона + assert ll_find(result_insert, test.get("name")) == test.get("phone") + + # Проверяем правильность места изменения + for i, value in enumerate(test_records): + if value.get("name") == test.get("name"): + assert ll_list_all(result_insert)[i].get("phone") == test.get("phone") + break + + +def test_ll_insert_new(linked_list): + assert linked_list is not None + + new_name = "Новый контакт" + new_phone = "99999999999" + + test_ll = copy.deepcopy(linked_list) + + result = ll_insert(test_ll, new_name, new_phone) + + assert ll_find(result, new_name) == new_phone + + # Проверяем, что новый элемент в конце + all_items = ll_list_all(result) + assert all_items[-1].get("name") == new_name + +def test_ll_delete(linked_list, test_records): + assert linked_list is not None + + tests = [ + test_records[0], + test_records[1], + test_records[len(test_records) // 2], + test_records[-2], + test_records[-1], + {"name": "Сергей", "phone": "89290504426"}, + ] + + for test in tests: + test_ll = copy.deepcopy(linked_list) + + result_delete = ll_delete(test_ll, test.get('name')) + + assert ll_find(result_delete, test.get('name')) is None \ No newline at end of file diff --git a/skorohodovsa/task_1/test/test_task_2.py b/skorohodovsa/task_1/test/test_task_2.py new file mode 100644 index 0000000..dc18e81 --- /dev/null +++ b/skorohodovsa/task_1/test/test_task_2.py @@ -0,0 +1,60 @@ +import unittest +import sys +import os + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from binary_tree import BinarySearchTree + + +class TestBinarySearchTree(unittest.TestCase): + def setUp(self): + self.bst = BinarySearchTree() + self.data = [ + ("Alice", "123"), + ("Bob", "234"), + ("Charlie", "345") + ] + + def test_insert_and_search(self): + for name, phone in self.data: + self.bst.insert(name, phone) + + for name, phone in self.data: + self.assertEqual(self.bst.search(name), phone) + + def test_search_not_found(self): + self.bst.insert("Alice", "123") + self.assertIsNone(self.bst.search("Bob")) + + def test_delete(self): + self.bst.insert("Alice", "123") + self.assertTrue(self.bst.delete("Alice")) + self.assertIsNone(self.bst.search("Alice")) + self.assertEqual(self.bst.get_size(), 0) + + def test_size(self): + self.assertEqual(self.bst.get_size(), 0) + self.bst.insert("Alice", "123") + self.assertEqual(self.bst.get_size(), 1) + self.bst.insert("Bob", "234") + self.assertEqual(self.bst.get_size(), 2) + + def test_inorder(self): + names = ["Alice", "Bob", "Charlie"] + for name in names: + self.bst.insert(name, "123") + + result = self.bst.inorder() + self.assertEqual(len(result), 3) + for i, name in enumerate(names): + self.assertEqual(result[i]['name'], name) + + def test_clear(self): + self.bst.insert("Test", "123") + self.assertFalse(self.bst.is_empty()) + self.bst.clear() + self.assertTrue(self.bst.is_empty()) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/skorohodovsa/task_1/test/test_task_3.py b/skorohodovsa/task_1/test/test_task_3.py new file mode 100644 index 0000000..60f4c77 --- /dev/null +++ b/skorohodovsa/task_1/test/test_task_3.py @@ -0,0 +1,45 @@ +import unittest +import sys +import os + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from hash_table import HashTable + + +class TestHashTable(unittest.TestCase): + def setUp(self): + self.ht = HashTable(10) + + def test_insert_and_search(self): + self.ht.insert("Alice", "123") + self.ht.insert("Bob", "234") + + self.assertEqual(self.ht.search("Alice"), "123") + self.assertEqual(self.ht.search("Bob"), "234") + + def test_update(self): + self.ht.insert("Alice", "123") + self.ht.insert("Alice", "456") + + self.assertEqual(self.ht.search("Alice"), "456") + + def test_delete(self): + self.ht.insert("Alice", "123") + self.assertTrue(self.ht.delete("Alice")) + self.assertIsNone(self.ht.search("Alice")) + + def test_size(self): + self.assertEqual(self.ht.get_size(), 0) + self.ht.insert("Alice", "123") + self.assertEqual(self.ht.get_size(), 1) + + def test_resize(self): + for i in range(20): + self.ht.insert(f"User_{i}", "123") + + self.assertGreater(self.ht.capacity, 10) + self.assertEqual(self.ht.get_size(), 20) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/skorohodovsa/task_2/.gitignore b/skorohodovsa/task_2/.gitignore new file mode 100644 index 0000000..deb7a34 --- /dev/null +++ b/skorohodovsa/task_2/.gitignore @@ -0,0 +1,35 @@ +/.obsidian + +# Виртуальное окружение +venv/ +env/ +.venv/ +.env/ + +# Python кэш +__pycache__/ +*.py[cod] +*.so +.Python + +# Сборка документации Sphinx - ЭТО ВАЖНО! +docs/build/ +docs/_build/ + +# Системные файлы +.DS_Store +Thumbs.db +*.swp +*.swo +*~ + +# IDE +.vscode/ +.idea/ + +# Логи +*.log + +.ruff_cache/ + +pupu.py \ No newline at end of file diff --git a/skorohodovsa/task_2/README.md b/skorohodovsa/task_2/README.md new file mode 100644 index 0000000..e8ab679 --- /dev/null +++ b/skorohodovsa/task_2/README.md @@ -0,0 +1,103 @@ +# Поиск выхода из лабиринта +--- +## Цель работы + +Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры. + +--- +## Архитектура + +```mermaid +classDiagram + class Maze { + -Cell[] cells + -int width, height + -Cell start + -Cell exit + +getCell(x,y): Cell + +getNeighbors(cell): List~Cell~ + } + + class Cell { + -int x, y + -bool isWall + -bool isStart + -bool isExit + +isPassable(): bool + } + + class MazeBuilder { + <> + +buildFromFile(filename): Maze + } + + class TextFileMazeBuilder { + +buildFromFile(filename): Maze + } + + class PathFindingStrategy { + <> + +findPath(maze, start, exit): List~Cell~ + } + + class BFSStrategy + class DFSStrategy + class AStarStrategy + class DijkstraStrategy + + class SearchStats { + +timeMs: float + +visitedCells: int + +pathLength: int + } + + class MazeSolver { + -Maze maze + -PathFindingStrategy strategy + +setStrategy(strategy) + +solve(): SearchStats + } + + class Command { + <> + +execute() + +undo() + } + + class MoveCommand { + -Player player + -Direction dir + -Cell previousCell + +execute() + +undo() + } + + class Player { + -Cell currentCell + +moveTo(cell) + } + + class Observer { + <> + +update(event) + } + + class ConsoleView { + +update(event) + +render(maze, player, path) + } + + MazeBuilder <|.. TextFileMazeBuilder + MazeBuilder --> Maze : creates + PathFindingStrategy <|.. BFSStrategy + PathFindingStrategy <|.. DFSStrategy + PathFindingStrategy <|.. AStarStrategy + PathFindingStrategy <|.. DijkstraStrategy + MazeSolver --> PathFindingStrategy : uses + MazeSolver --> Maze : uses + Command <|.. MoveCommand + MoveCommand --> Player + Player --> Cell + Observer <|.. ConsoleView + MazeSolver --> Observer : notifies +``` diff --git a/skorohodovsa/task_2/docs.md b/skorohodovsa/task_2/docs.md new file mode 100644 index 0000000..d0eec40 --- /dev/null +++ b/skorohodovsa/task_2/docs.md @@ -0,0 +1,1185 @@ +`файл является компиляцией документации sphinx в папке docs` +# Лабораторная работа «Поиск выхода из лабиринта» + +* [Задание](task.md) + * [Цель работы](task.md#id2) + * [Общая схема приложения (пример)](task.md#id3) + * [Выполнение](task.md#id4) + * [Советы](task.md#id8) +* [Этап 1. Модель лабиринта](stage1.md) + * [Класс `Cell`](stage1.md#cell) + * [Класс `Maze`](stage1.md#maze) +* [Этап 2. Загрузка лабиринта из файла](stage2.md) + * [Паттерн Builder](stage2.md#builder) + * [Класс `MazeBuilder`](stage2.md#mazebuilder) + * [Класс `TextFileBuilder`](stage2.md#textfilebuilder) + * [Использование](stage2.md#id2) + * [Известная ошибка](stage2.md#id3) +* [Этап 3. Стратегии поиска пути](stage3.md) + * [Паттерн Strategy](stage3.md#strategy) + * [Структура пакета](stage3.md#id2) + * [Класс `PathFindingStrategy`](stage3.md#pathfindingstrategy) + * [Алгоритмы](stage3.md#id3) +* [Этап 4. Класс-оркестратор MazeSolver](stage4.md) + * [Роль в архитектуре](stage4.md#id1) + * [Класс `SearchStats`](stage4.md#searchstats) + * [Класс `MazeSolver`](stage4.md#id3) +* [Этап 5. Визуализация и пошаговое управление](stage5.md) + * [5.1. Паттерн Observer](stage5.md#observer) + * [5.2. Паттерн Command](stage5.md#command) +* [Этап 6. Экспериментальная часть](stage6.md) + * [Подготовка](stage6.md#id2) + * [Замеры](stage6.md#id3) + * [Результаты](stage6.md#id4) + * [Выводы](stage6.md#id8) + * [Визуализация](stage6.md#id9) +* [Этап 7. Отчёт](stage7.md) + * [Описание задачи](stage7.md#id2) + * [Диаграмма классов](stage7.md#id3) + * [Результаты экспериментов](stage7.md#id4) + * [Выводы](stage7.md#id5) +* [API Reference](api.md) + * [Базовые модели](api.md#module-source.models.base) + * [Загрузка лабиринта](api.md#module-source.build.builder) + * [Стратегии поиска пути](api.md#module-source.strategy.algorithms) + * [Оркестратор](api.md#module-source.strategy.solver) + * [Визуализация](api.md#module-source.view.observer) + * [Управление игроком](api.md#module-source.view.command) +# Этап 2. Загрузка лабиринта из файла + +Во втором этапе разработки необходимо реализовать загрузку лабиринта из текстового файла, где: `#` – стена, ` ` – проход, `S` – старт, `E` – выход. + +## Систематизация файлов + +Для удобного хранения лабиринтов было решено сделать систему наименования текстовых файлов в папке `source/templates`. + +Общая структура: + +```default +{размер}_{свойство 1}-{свойство 2}-{свойство n}_{версия}.txt +``` + +### Размер + +Формат: `{ширина}x{высота}` + +| Пример | Значение | +|-----------|----------------| +| `10x10` | 10×10 клеток | +| `50x50` | 50×50 клеток | +| `100x100` | 100×100 клеток | +| `30x30` | 30×30 клеток | +| `20x20` | 20×20 клеток | + +### Свойства + +| Свойство | Код | Описание | +|--------------|-------------|----------------------------------------------------------------------------------------------| +| Простой путь | `path` | Существует маршрут от S до E | +| Тупики | `deadends` | Лабиринт специально содержит тупики (могут быть и в других типах, но здесь — гарантированно) | +| Запутанный | `spaghetti` | Сложная структура с циклами и ложными ходами | +| Пустой | `empty` | Нет стен (`#`), только пробелы, S и E | +| Без выхода | `noexit` | В лабиринте отсутствует символ `E` | + +### Версия + +Формат: `v{номер}` + +- `v1`, `v2`, `v10` + +### Примеры + +#### Маленькие (10×10, простой путь) + +```default +10x10_path_v1.txt +10x10_path_v2.txt +... +10x10_path_v10.txt +``` + +#### Средние (50×50, тупики) + +```default +50x50_deadends_v1.txt +50x50_deadends_v2.txt +... +50x50_deadends_v10.txt +``` + +#### Большие (100×100, запутанные) + +```default +100x100_spaghetti_v1.txt +100x100_spaghetti_v2.txt +... +100x100_spaghetti_v10.txt +``` + +#### Пустые (30×30) + +```default +30x30_empty_v1.txt +30x30_empty_v2.txt +... +30x30_empty_v10.txt +``` + +#### Без выхода (20×20) + +```default +20x20_noexit_v1.txt +20x20_noexit_v2.txt +... +20x20_noexit_v10.txt +``` + +#### Комбинированные свойства + +```default +50x50_deadends-noexit_v1.txt +100x100_spaghetti-noexit_v1.txt +10x10_path-empty_v1.txt (избыточно, но допустимо) +``` + +### Примечание + +- Регистр имён файлов: **нижний регистр** +- Разделители: только `_` и `-` +- Расширение: `.txt` +- Кодировка: UTF-8 +# Этап 1. Модель лабиринта + +В первом этапе разработки необходимо создать базовые классы `Cell` и `Maze`, которые представляют карту лабиринта. Паттерны на этом этапе не применяются — только чистые классы. + +## Класс `Cell` + +Клетка — минимальная единица лабиринта. Хранит координаты и тип: стена, старт, выход или пустая. + +По условию задания клетка должна иметь флаги `isWall`, `isStart`, `isExit` и метод `isPassable()`. В реализации флаги оформлены как **свойства** (`@property`) с сеттерами — это позволяет автоматически сбрасывать остальные флаги при установке нового типа. + +```python +cell = Cell(1, 1) +cell.is_wall = True +``` + +Типы клетки взаимоисключают друг друга — клетка не может быть одновременно стеной и стартом. Логика сброса вынесена в приватный метод `_clear_flags()`. + +### Символьное представление + +Для вывода лабиринта в консоль каждая клетка возвращает символ через `__str__`. Символы берутся из `cell_mapping` в `source/settings.py`, что позволяет менять отображение без правки классов: + +| Тип | Символ по умолчанию | +|--------|-----------------------| +| Стена | `#` | +| Старт | `S` | +| Выход | `E` | +| Пустая | | + +## Класс `Maze` + +Лабиринт хранит двумерный список клеток и предоставляет методы для работы с ними. + +По условию задания требовались методы `getCell(x, y)` и `getNeighbors(cell)`. В реализации добавлено несколько вещей сверх задания: + +### Именование методов + +Задание написано в стиле Java/pseudocode — названия методов и полей используют `camelCase` (`isWall`, `getCell`, `isPassable`). В Python принят другой стандарт именования — **PEP 8**, который предписывает `snake_case` для методов и атрибутов. Поэтому все названия были приведены к Python стилю: + +| Задание | Реализация | +|---------------------------|-----------------------------| +| `isWall` | `is_wall` | +| `isStart` | `is_start` | +| `isExit` | `is_exit` | +| `isPassable()` | `is_possible()` | +| `getCell(x, y)` | `get_cell(x, y)` | +| `getNeighbors(cell)` | `get_neighbors(x, y)` | +| `buildFromFile(filename)` | `build_from_file(filename)` | + +Это соответствует стандарту оформления кода на Python и делает API классов идиоматичным для языка. + +### Индексация `maze[row, col]` + +Вместо явного вызова `get_cell()` реализованы `__getitem__` и `__setitem__`, что позволяет обращаться к клеткам естественным образом: + +```python +maze[0, 0] = cell_mapping['wall'] # установить стену +cell = maze[2, 3] # получить клетку +``` + +Обратите внимание: индексация идёт в формате `[row, col]`, то есть сначала строка (Y), потом столбец (X) — аналогично numpy. + +### Свойства `start` и `exit` + +Добавлены свойства для быстрого получения стартовой и выходной клетки без ручного обхода: + +```python +maze.start # Cell или None +maze.exit # Cell или None +``` + +Это оказалось необходимым при реализации алгоритмов поиска пути — стратегии получают `start` и `exit` автоматически из лабиринта. + +### Свойство `shape` + +По аналогии с numpy добавлено свойство `shape`, возвращающее `(height, width)`: + +```python +rows, cols = maze.shape +``` + +Используется в стратегиях поиска и тестах для итерации по лабиринту. + +### `get_neighbors` + +Метод возвращает список проходимых соседей клетки по четырём направлениям. Стены и клетки за границей лабиринта исключаются автоматически. Если переданные координаты вне границ — возвращает `None`. + +```python +neighbors = maze.get_neighbors(2, 2) # список Cell +``` + +Направления обхода: вниз → вправо → вверх → влево (порядок влияет на поведение DFS). +# Этап 2. Загрузка лабиринта из файла + +Во втором этапе реализована загрузка лабиринта из текстового файла с применением паттерна **Builder**. + +## Паттерн Builder + +Процесс создания лабиринта из файла включает несколько шагов: чтение файла, валидацию структуры, парсинг символов и заполнение клеток. Builder скрывает эти детали от клиента — снаружи виден только один метод `build_from_file()`, внутри которого сосредоточена вся логика построения. + +Дополнительное преимущество: в будущем можно легко добавить новый формат (например, JSON или бинарный) через новую реализацию `MazeBuilder` без изменения остального кода. + +## Класс `MazeBuilder` + +Абстрактный базовый класс — интерфейс паттерна Builder. Объявляет единственный метод `build_from_file()`, который обязан реализовать каждый конкретный строитель. + +По условию задания интерфейс назывался `MazeBuilder` с методом `buildFromFile`. В реализации название метода приведено к **PEP 8** — `build_from_file`. Сам класс оформлен через `ABC` — попытка создать объект `MazeBuilder()` напрямую вызовет `TypeError`. + +## Класс `TextFileBuilder` + +Конкретная реализация строителя для текстовых файлов. Загружает лабиринт из `.txt` файла где `#` — стена, — проход, `S` — старт, `E` — выход. + +Процесс построения разбит на три приватных шага: + +### `_read_file` + +Читает файл построчно и обрезает символы переноса строки `\n` и `\r`. Возвращает список строк — каждая строка соответствует одной строке лабиринта. + +### `_test_text_maze` + +Валидирует структуру: проверяет что все строки одинаковой длины. Если нет — лабиринт некорректен и `_create_maze` выбросит `ValueError`. + +Реализован как `@staticmethod` — не использует состояние объекта, только входные данные. + +### `_create_maze` + +Создаёт объект `Maze` нужного размера и заполняет его клетки символами из файла через `maze[y, x] = symbol`. Тип каждой клетки определяется автоматически через `cell_mapping` в `__setitem__` лабиринта. + +## Использование + +```python +from source.build.builder import TextFileBuilder + +maze = TextFileBuilder().build_from_file('source/templates/10x10_path_v1.txt') +print(maze) +``` + +## Известная ошибка + +В текущей реализации `_create_maze` есть опечатка при вычислении `width`: + +```python +height, width = len(text_maze), len(text_maze) # width всегда равен height +``` + +Правильная версия: + +```python +height, width = len(text_maze), len(text_maze[0]) +``` + +На квадратных лабиринтах (10×10, 50×50) это не проявляется, но на прямоугольных даст некорректный результат. +# Этап 3. Стратегии поиска пути + +В третьем этапе реализованы алгоритмы поиска пути с применением паттерна **Strategy**. + +## Паттерн Strategy + +Все три алгоритма реализуют один интерфейс `PathFindingStrategy`. Это позволяет переключать алгоритм в любой момент без изменения кода клиента — достаточно передать другой объект стратегии: + +```python +solver = MazeSolver(maze, BFSStrategy()) +solver.set_strategy(AStarStrategy()) +``` + +Новый алгоритм добавляется реализацией интерфейса — остальной код трогать не нужно. + +## Структура пакета + +Стратегии разбиты по отдельным файлам, а `__init__.py` собирает всё в один импорт: + +```default +source/strategy/ +├── __init__.py ← единственный импорт для пользователя +├── algorithms.py ← базовый класс PathFindingStrategy +├── bfs.py +├── dfs.py +└── astar.py +``` + +```python +from source.strategy import BFSStrategy, DFSStrategy, AStarStrategy +``` + +## Класс `PathFindingStrategy` + +Абстрактный базовый класс — интерфейс паттерна. Объявляет абстрактный метод `find_path()` и содержит два вспомогательных метода, общих для всех стратегий. + +По условию задания метод назывался `findPath` — приведён к **PEP 8** как `find_path`. + +### `_validate` + +Добавлен в процессе разработки — изначально в задании не было требования к обработке отсутствия старта или выхода. Проблема проявилась при тестировании лабиринтов типа `noexit`: алгоритм падал с `AttributeError` внутри, вместо понятного сообщения. + +`_validate` подставляет `start` и `exit` из лабиринта если они не переданы явно, и выбрасывает `ValueError` с понятным сообщением если клетки не найдены: + +```python +start, exit = self._validate(maze, start, exit) +``` + +Вынесен в базовый класс чтобы не дублировать в каждом алгоритме. + +### `_reconstruct_path` + +Восстанавливает путь по словарю предков `came_from`. Все три алгоритма строят этот словарь одинаково — `{клетка: откуда_пришли}` — поэтому восстановление вынесено в общий метод базового класса. + +Алгоритм идёт от выхода к старту по цепочке предков, затем разворачивает список: + +```default +exit → D → C → B → start (идём по came_from) +start → B → C → D → exit (после reverse) +``` + +## Алгоритмы + +### BFS — `BFSStrategy` + +Поиск в ширину. Использует `deque` как очередь (FIFO) — каждый раз берём самую старую клетку из начала. Это гарантирует послойный обход и кратчайший путь по количеству шагов. + +Сложность: O(V + E) по времени и памяти. + +### DFS — `DFSStrategy` + +Поиск в глубину. Использует `list` как стек (LIFO) — каждый раз берём самую свежую клетку с конца. Алгоритм ныряет вглубь по одному направлению до тупика, затем возвращается. + +Не гарантирует кратчайший путь. На запутанных лабиринтах может обойти почти все клетки прежде чем найти выход, хотя по времени часто быстрее BFS из-за меньших накладных расходов на структуру данных. + +Сложность: O(V + E) по времени и памяти. + +### A\* — `AStarStrategy` + +Использует `heapq` как приоритетную очередь. На каждом шаге выбирает клетку с минимальным значением `f = g + h`, где `g` — стоимость пути от старта, `h` — манхэттенская эвристика до выхода. + +Эвристика направляет поиск в сторону выхода, поэтому A\* обходит меньше клеток чем BFS при том же гарантированно кратчайшем пути. + +В кортеж приоритетной очереди добавлен счётчик `counter` как tie-breaker — без него `heapq` попытался бы сравнивать объекты `Cell` при одинаковом `f`, что вызвало бы `TypeError`: + +```python +heapq.heappush(open_heap, (f, counter, neighbor)) +``` + +Сложность: O(E · log V) в худшем случае. +# Этап 4. Класс-оркестратор MazeSolver + +В четвёртом этапе реализован класс `MazeSolver`, который объединяет лабиринт и стратегию поиска, выполняет поиск и собирает статистику. + +## Роль в архитектуре + +`MazeSolver` — точка входа для клиентского кода. Он не знает деталей ни одного алгоритма и не работает напрямую с клетками лабиринта — только делегирует задачу стратегии и замеряет время: + +```python +solver = MazeSolver(maze, BFSStrategy()) +stats = solver.solve() +print(stats) +# Время: 0.041 мс | Посещено клеток: 13 | Длина пути: 13 +``` + +## Класс `SearchStats` + +Оформлен через `@dataclass` — это избавляет от ручного `__init__` и автоматически даёт `__repr__`. Хранит четыре поля: время выполнения, количество посещённых клеток, длину пути и сам путь как список клеток. + +`__str__` переопределён для удобного вывода в консоль и отчётах. + +### Ограничение + +В текущей реализации `visited_count` и `path_length` всегда равны друг другу — оба вычисляются как `len(path)`. Это потому что стратегии возвращают только финальный путь, а не все посещённые клетки. Чтобы получить точное количество посещений, потребовалось бы дорабатывать каждую стратегию — добавлять счётчик внутри `find_path`. На данном этапе это сознательное упрощение. + +## Класс `MazeSolver` + +### `set_strategy` + +Позволяет менять алгоритм без пересоздания солвера. Это и есть основная демонстрация паттерна Strategy в действии — один объект, разные алгоритмы: + +```python +solver = MazeSolver(maze, BFSStrategy()) +stats_bfs = solver.solve() + +solver.set_strategy(AStarStrategy()) +stats_astar = solver.solve() +``` + +### `solve` + +Замеряет время через `time.perf_counter()` — самый точный таймер в Python для коротких интервалов, не зависящий от системных часов. Результат переводится в миллисекунды умножением на 1000. + +`start` и `exit` можно не передавать — стратегия найдёт их сама через `_validate`. Явная передача нужна только если хочется запустить поиск не от стандартного старта, а от произвольной клетки. +# Этап 5. Визуализация и пошаговое управление + +В пятом этапе реализованы два паттерна: **Observer** для отображения событий и **Command** для пошагового управления игроком. + +## 5.1. Паттерн Observer + +### Идея + +`MazeSolver` и игровой цикл не знают как именно отображать происходящее — они просто генерируют события. Наблюдатели подписываются на эти события и реагируют по своему усмотрению. Это позволяет в будущем добавить, например, `FileLogger` или графический интерфейс без изменения основного кода. + +### Класс `Event` + +Оформлен через `@dataclass`. Хранит тип события строкой и словарь `payload` с дополнительными данными. Поддерживаются четыре типа событий: + +| Тип | Когда генерируется | +|---------------|----------------------------| +| `maze_loaded` | Лабиринт загружен из файла | +| `path_found` | Алгоритм нашёл путь | +| `no_path` | Путь не найден | +| `move` | Игрок сделал ход | + +### Класс `Observer` + +Абстрактный базовый класс с единственным методом `update(event)`. Любой наблюдатель обязан его реализовать. + +### Класс `ConsoleView` + +Конкретная реализация наблюдателя. Обрабатывает события через `match/case` и вызывает `render()` для перерисовки лабиринта. + +Метод `render()` принимает лабиринт, опциональную позицию игрока и опциональный путь. Путь преобразуется в `set` для быстрой проверки принадлежности клетки — это O(1) вместо O(n) при каждом обходе: + +```python +path_set = set(path) if path else set() +``` + +Лабиринт обрамляется рамкой из `+` и `─` для читаемости в консоли. Символы игрока и пути вынесены в константы класса — легко поменять без правки логики: + +```python +PLAYER_SYMBOL = "P" +PATH_SYMBOL = "·" +``` + +## 5.2. Паттерн Command + +### Идея + +Каждое перемещение игрока оборачивается в объект `MoveCommand`. Это позволяет сохранить предыдущее состояние и отменить ход — реализация `undo` становится тривиальной. + +### Класс `Player` + +Простой контейнер для текущей клетки игрока. Намеренно минималистичный — вся логика перемещения и проверок находится в команде, а не в игроке. + +### Класс `Command` + +Абстрактный интерфейс с двумя методами: `execute()` и `undo()`. `execute()` возвращает `bool` — это отличие от классического варианта паттерна, где команды не возвращают значений. Возврат `False` нужен чтобы не добавлять неуспешный ход в историю. + +### Класс `MoveCommand` + +Хранит ссылку на игрока, направление и лабиринт. При `execute()` проверяет проходимость целевой клетки, сохраняет текущую в `_prev_cell` и перемещает игрока. При `undo()` восстанавливает `_prev_cell`. + +Направления вынесены в словарь `DIRECTIONS` на уровне модуля: + +```python +DIRECTIONS = { + "w": (0, -1), # вверх + "s": (0, 1), # вниз + "a": (-1, 0), # влево + "d": (1, 0), # вправо +} +``` + +### Класс `CommandHistory` + +Стек выполненных команд. Хранит только успешные ходы — неуспешные (`execute()` вернул `False`) в историю не добавляются. `undo()` снимает последнюю команду со стека и вызывает её `undo()`. + +Пример игрового цикла: + +```python +cmd = MoveCommand(player, 'd', maze) +if cmd.execute(): + history.push(cmd) # добавляем только успешный ход + +history.undo() # отмена последнего хода +``` +# Этап 6. Экспериментальная часть + +В шестом этапе проведено сравнение эффективности трёх стратегий поиска пути на лабиринтах разной сложности. Эксперимент реализован в Jupyter Notebook (`practice/main.ipynb`). + +## Подготовка + +Лабиринты загружаются из папки `source/templates` автоматически — все файлы считываются через `os.listdir` и передаются в `TextFileBuilder`. Стратегии собраны в словарь для удобной итерации: + +```python +strategies = { + "BFS": BFSStrategy(), + "DFS": DFSStrategy(), + "A*": AStarStrategy(), +} +``` + +## Замеры + +Каждая пара лабиринт + стратегия запускается **10 раз**, результаты усредняются. Это сглаживает разброс из-за кэширования и фоновой активности системы. + +Лабиринты типа `noexit` пропускаются автоматически — стратегия выбрасывает `ValueError`, который перехватывается через `try/except`, и выполнение продолжается. + +Результаты собираются в список словарей и затем преобразуются в `DataFrame` через pandas. + +## Результаты + +### 10×10 (простой путь) + +На маленьких лабиринтах все три алгоритма показывают практически одинаковое время (~0.03–0.07 мс) и одинаковую длину пути. Разница незначительна — лабиринт слишком мал чтобы эвристика A\* давала преимущество. + +### 50×50 (тупики) + +BFS стабильно быстрее DFS по времени при одинаковой длине пути. DFS заходит в каждый тупик до конца и тратит время на возврат, хотя финальный путь совпадает. A\* показывает время между BFS и DFS. + +### 100×100 (запутанный, spaghetti) + +Наиболее показательные результаты: + +| Стратегия | Время (мс) | Длина пути | +|-------------|--------------|--------------| +| BFS | ~9 | ~210 | +| DFS | ~7 | ~2200 | +| A\* | ~8 | ~210 | + +DFS быстрее по времени, но находит путь в 10 раз длиннее — обходит почти весь лабиринт. BFS и A\* находят кратчайший путь, A\* при этом чуть быстрее за счёт эвристики. + +### 30×30 (пустой) + +Неожиданный результат: DFS быстрее всех (~0.73 мс против 1.1 у BFS и 2.0 у A\*), хотя находит путь из 379 клеток против 55 у BFS. На пустом поле без стен DFS сразу уходит вглубь без возвратов, тогда как BFS строит очередь и обходит клетки волнами во все стороны — это накладные расходы на структуру данных. + +A\* на пустом лабиринте медленнее всех — накладные расходы на `heapq` и вычисление эвристики не окупаются когда препятствий нет. + +## Выводы + +- **BFS** — надёжный выбор по умолчанию. Всегда находит кратчайший путь, время предсказуемо. +- **DFS** — быстрый по времени, но путь непредсказуем. На запутанных лабиринтах может пройти весь граф. Подходит когда важна скорость, а не оптимальность пути. +- **A**\* — лучший выбор для больших лабиринтов с препятствиями. Находит кратчайший путь быстрее BFS за счёт эвристики. На простых или пустых лабиринтах проигрывает из-за накладных расходов на приоритетную очередь. + +## Визуализация + +![[results.png]] +# Этап 7. Отчёт + +## Описание задачи + +Разработать программу для загрузки лабиринта из файла, поиска пути с выбором алгоритма и сравнения их эффективности. Применены четыре паттерна GoF: + +| Паттерн | Где применён | +|--------------|-----------------------------------------------| +| **Builder** | `TextFileBuilder` | +| **Strategy** | `BFSStrategy`, `DFSStrategy`, `AStarStrategy` | +| **Observer** | `ConsoleView` | +| **Command** | `MoveCommand`, `CommandHistory` | + +## Диаграмма классов + +## Результаты экспериментов + +| Лабиринт | Быстрее всех | Кратчайший путь | +|-------------------|----------------|-------------------| +| 10×10 path | все одинаково | все одинаково | +| 50×50 deadends | BFS | BFS = A\* | +| 100×100 spaghetti | DFS | BFS = A\* | +| 30×30 empty | DFS | BFS = A\* | + +- **BFS** — надёжный выбор, всегда кратчайший путь. +- **DFS** — быстрый, но путь длиннее. На 100×100 обошёл в 10 раз больше клеток. +- **A\*** — лучший на больших лабиринтах с препятствиями, проигрывает на простых из-за накладных расходов на `heapq`. + +## Выводы + +Паттерны сделали код расширяемым: новый алгоритм — один класс, новый формат файла — один класс, новый способ отображения — один класс. Без паттернов каждое такое изменение потребовало бы правки существующего кода. +# Задание + +## Цель работы + +Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры. + +## Общая схема приложения (пример) + +## Выполнение + +### Этап 1. Модель лабиринта (без паттернов, просто классы) + +**Задача:** Создать классы `Cell` и `Maze`, которые представляют карту лабиринта. + +- `Cell` хранит координаты (x, y), флаги `isWall`, `isStart`, `isExit`, метод `isPassable()` (возвращает `True` для прохода, если не стена). +- `Maze` хранит двумерный массив клеток, ширину, высоту, ссылки на стартовую и выходную клетку. Методы: `getCell(x, y)`, `getNeighbors(cell)` – возвращает список соседних проходимых клеток (вверх, вниз, влево, вправо, если в пределах границ и не стена). + +**Результат:** Лабиринт можно создать вручную в коде, но загрузку пока не делаем. + +### Этап 2. Загрузка лабиринта из файла – применение паттерна **Builder** + +**Задача:** Реализовать загрузку лабиринта из текстового файла, где `#` – стена, ` ` (пробел) – проход, `S` – старт, `E` – выход. + +- Создать интерфейс `MazeBuilder` с методом `buildFromFile(filename)`. +- Реализовать класс `TextFileMazeBuilder`, который читает файл, парсит символы, создаёт объекты `Cell`, задаёт координаты и флаги, после чего возвращает готовый `Maze`. + +Процесс построения лабиринта сложный (парсинг, валидация, установка старта/выхода). Builder скрывает детали создания от клиента. В будущем можно легко добавить другой формат (например, JSON или бинарный) через новую реализацию `MazeBuilder`. + +### Этап 3. Стратегии поиска пути – паттерн **Strategy** + +**Задача:** Реализовать семейство алгоритмов поиска пути от старта до выхода. + +- Создать интерфейс `PathFindingStrategy` с методом `findPath(maze, start, exit)`, возвращающим список клеток пути (от старта до выхода включительно) или пустой список, если пути нет. +- Реализовать минимум 3 стратегии: + - **BFS** (поиск в ширину) – гарантирует кратчайший путь по количеству шагов. + - **DFS** (поиск в глубину) – быстрый, но не обязательно кратчайший. + - **A**\* (с эвристикой, например, манхэттенское расстояние) – компромисс между скоростью и оптимальностью. + - (Опционально) **Дейкстра** – полезна для взвешенных лабиринтов, но в базовом варианте все шаги имеют вес 1, тогда она совпадает с BFS. + +Каждая стратегия возвращает путь. Для BFS/DFS используйте очередь/стек, для A\* – приоритетную очередь (heapq). Важно: алгоритмы не должны модифицировать сам лабиринт, только читать состояние клеток. + +Strategy позволяет легко переключать алгоритмы во время выполнения, не меняя код остальной программы. Новый алгоритм можно добавить, реализовав интерфейс. + +### Этап 4. Класс-оркестратор – **MazeSolver** (использует Strategy) + +**Задача:** Создать класс, который принимает лабиринт и стратегию, выполняет поиск и собирает статистику. + +- `MazeSolver` содержит поля `maze` и `strategy`. +- Метод `setStrategy(strategy)` для динамической смены алгоритма. +- Метод `solve()` вызывает `strategy.findPath(...)` и возвращает объект `SearchStats` (время выполнения в миллисекундах, количество посещённых клеток, длина найденного пути). +- Для замера времени используйте `time.perf_counter()` до и после вызова стратегии. + +### Этап 5. Визуализация и пошаговое управление – паттерны **Observer** и **Command** (по желанию) + +**5.1. Наблюдатель (Observer)** – обновление консольного интерфейса. + +- Создать интерфейс `Observer` с методом `update(event)`, где `event` может быть строкой или объектом с типом события (`"path_found"`, `"move"`, `"maze_loaded"`). +- Реализовать класс `ConsoleView`, который отображает лабиринт, текущее положение игрока (если реализован пошаговый режим) и найденный путь. Метод `render(maze, player_position, path)` рисует карту в консоли. +- `MazeSolver` (или отдельный контроллер) может иметь список наблюдателей и уведомлять их при изменении состояния. + +**5.2. Команда (Command)** – для пошагового перемещения игрока по найденному пути (или ручного управления). + +- Создать интерфейс `Command` с методами `execute()` и `undo()`. +- Реализовать `MoveCommand`, который принимает игрока (`Player`), направление и изменяет его позицию, сохраняя предыдущую для отмены. +- Создать класс `Player`, хранящий текущую клетку. +- Консольное меню позволяет вводить команды (W/A/S/D), выполнять `MoveCommand`, при необходимости отменять последний ход (Ctrl+Z). Это опционально, но очень наглядно демонстрирует паттерн. + +*Observer можно реализовать только для вывода сообщений о начале/конце поиска, а Command – для демонстрации undo при ручном исследовании лабиринта.* + +### Этап 6. Экспериментальная часть (аналогично заданию со структурами данных) + +**Задача:** Сравнить эффективность реализованных стратегий на лабиринтах разной сложности. + +1. **Подготовка тестовых лабиринтов:** + - Маленький (10×10) с простым путём. + - Средний (50×50) с тупиками. + - Большой (100×100) с запутанной структурой. + - «Пустой» лабиринт (без стен) – для демонстрации максимальной производительности. + - «Без выхода» – чтобы проверить обработку отсутствия пути. +2. **Замеры:** + - Для каждого лабиринта и каждой стратегии запустить `solve()` 5–10 раз, усреднить время, количество посещённых клеток, длину пути. + - Записать результаты в CSV: `лабиринт,стратегия,время_мс,посещено_клеток,длина_пути`. +3. **Анализ:** + - Построить графики для каждого лабиринта. + - Проанализировать и написать выводы по итогам (эффективность того или иного алгоритма в разных случаях). +4. **Дополнительное задание:** Реализовать взвешенные клетки (например, болото – вес 3, песок – вес 2, асфальт – вес 1) и сравнить Дейкстру с A\* на взвешенном графе. + +### Этап 7. Отчёт + +**Структура отчёта:** + +1. Описание задачи и выбранных паттернов (с диаграммой классов из Mermaid). +2. Листинги ключевых классов (можно выборочно) или ссылка на репозиторий. +3. Результаты экспериментов (таблицы, графики). +4. Анализ эффективности алгоритмов и применимости паттернов. +5. Выводы: как ООП и паттерны помогли сделать код гибким и расширяемым. Что было бы сложно изменить без них. + +## Советы + +- Для A\* самая простая эвристика: `abs(x1 - x2) + abs(y1 - y2)`. +- При поиске пути надо хранить предшественников (`parent` для каждой посещённой клетки), чтобы восстановить путь. +- Для BFS/DFS используй `deque` (очередь) и `list` (стек). +- Визуализацию в консоли можно сделать с помощью `os.system('cls' if os.name == 'nt' else 'clear')` для перерисовки. + +# API Reference + +## Базовые модели + +### *class* source.models.base.Cell(x: int, y: int, is_wall: bool = False, is_start: bool = False, is_exit: bool = False) + +Базовые классы: `object` + +Представляет одну клетку поля лабиринта. + +Каждая клетка хранит свои координаты и один из четырёх возможных +типов: стена, старт, выход или пустая клетка. Типы взаимоисключают +друг друга: установка одного автоматически сбрасывает остальные. + +#### \_\_init_\_(x: int, y: int, is_wall: bool = False, is_start: bool = False, is_exit: bool = False) + +Инициализирует клетку с заданными координатами и типом. + +* **Параметры:** + * **x** – Координата клетки по оси X. + * **y** – Координата клетки по оси Y. + * **is_wall** – Если True — клетка является стеной. + * **is_start** – Если True — клетка является стартом. + * **is_exit** – Если True — клетка является выходом. + +#### *property* is_exit *: bool* + +True, если клетка является выходом из лабиринта. + +#### is_possible() → bool + +Проверяет, можно ли переместиться в эту клетку. + +* **Результат:** + True, если клетка не является стеной, иначе False. + +#### *property* is_start *: bool* + +True, если клетка является стартовой позицией. + +#### *property* is_wall *: bool* + +True, если клетка является стеной. + +### *class* source.models.base.Maze(size: tuple[int, int] = (10, 10)) + +Базовые классы: `object` + +Представляет двумерный лабиринт из клеток Cell. + +Лабиринт хранится как список списков клеток. Доступ к отдельным +клеткам и их изменение возможны через индексацию вида maze[row, col]. + +#### \_\_init_\_(size: tuple[int, int] = (10, 10)) + +Создаёт пустой лабиринт заданного размера. + +* **Параметры:** + **size** – Кортеж (width, height) — ширина и высота лабиринта в клетках. + +#### *property* exit *: [Cell](#source.models.base.Cell) | None* + +#### get_cell(x: int, y: int) → [Cell](#source.models.base.Cell) | None + +Возвращает клетку по координатам или None, если координаты вне границ. + +* **Параметры:** + * **x** – Координата по оси X. + * **y** – Координата по оси Y. +* **Результат:** + Объект Cell, если координаты корректны, иначе None. + +#### get_neighbors(x: int, y: int) → list[[Cell](#source.models.base.Cell)] | None + +Возвращает список проходимых соседей клетки (вверх, вправо, вниз, влево). + +* **Параметры:** + * **x** – Координата клетки по оси X. + * **y** – Координата клетки по оси Y. +* **Результат:** + Список проходимых соседних клеток, или None если (x, y) вне границ. + +#### *property* shape *: tuple[int, int]* + +#### *property* start *: [Cell](#source.models.base.Cell) | None* + +## Загрузка лабиринта + +### *class* source.build.builder.MazeBuilder + +Базовые классы: `ABC` + +#### *abstractmethod* build_from_file(filename: str) → [Maze](#source.models.base.Maze) + +Возвращает объект лабиринта по указанному пути файлу. + +* **Параметры:** + **filename** (*str*) – Путь к файлу +* **Исключение:** + **TypeError** – Если введен путь файла с нерассмотренным расширением +* **Результат:** + Объект лабиринта +* **Тип результата:** + [Maze](#source.models.base.Maze) + +### *class* source.build.builder.TextFileBuilder + +Базовые классы: [`MazeBuilder`](#source.build.builder.MazeBuilder) + +#### build_from_file(filename: str) → [Maze](#source.models.base.Maze) + +Возвращает объект лабиринта по указанному пути файлу. + +* **Параметры:** + **filename** (*str*) – Путь к файлу +* **Исключение:** + **TypeError** – Если введен путь файла с нерассмотренным расширением +* **Результат:** + Объект лабиринта +* **Тип результата:** + [Maze](#source.models.base.Maze) + +## Стратегии поиска пути + +### *class* source.strategy.algorithms.PathFindingStrategy + +Базовые классы: `ABC` + +Интерфейс стратегии поиска пути в лабиринте. + +#### *abstractmethod* find_path(maze: [Maze](#source.models.base.Maze), start: [Cell](#source.models.base.Cell) = None, exit: [Cell](#source.models.base.Cell) = None) → list[[Cell](#source.models.base.Cell)] + +Найти путь от start до exit. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **start** – Стартовая клетка. + * **exit** – Целевая клетка. +* **Результат:** + Список клеток пути (от start до exit включительно). + Пустой список, если путь не найден. + + + +### *class* source.strategy.bfs.BFSStrategy + +Базовые классы: [`PathFindingStrategy`](#source.strategy.algorithms.PathFindingStrategy) + +Поиск в ширину (Breadth-First Search). + +Гарантирует кратчайший путь по количеству шагов. +Сложность: O(V + E) по времени и памяти. + +#### find_path(maze: [Maze](#source.models.base.Maze), start: [Cell](#source.models.base.Cell) | None = None, exit: [Cell](#source.models.base.Cell) | None = None) → list[[Cell](#source.models.base.Cell)] + +Найти путь от start до exit. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **start** – Стартовая клетка. + * **exit** – Целевая клетка. +* **Результат:** + Список клеток пути (от start до exit включительно). + Пустой список, если путь не найден. + + + +### *class* source.strategy.dfs.DFSStrategy + +Базовые классы: [`PathFindingStrategy`](#source.strategy.algorithms.PathFindingStrategy) + +Поиск в глубину (Depth-First Search). + +Находит путь, но не гарантирует кратчайший. + +#### find_path(maze: [Maze](#source.models.base.Maze), start: [Cell](#source.models.base.Cell) | None = None, exit: [Cell](#source.models.base.Cell) | None = None) → list[[Cell](#source.models.base.Cell)] + +Найти путь от start до exit. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **start** – Стартовая клетка. + * **exit** – Целевая клетка. +* **Результат:** + Список клеток пути (от start до exit включительно). + Пустой список, если путь не найден. + + + +### *class* source.strategy.astar.AStarStrategy + +Базовые классы: [`PathFindingStrategy`](#source.strategy.algorithms.PathFindingStrategy) + +Алгоритм A\* с манхэттенской эвристикой. + +#### find_path(maze: [Maze](#source.models.base.Maze), start: [Cell](#source.models.base.Cell) | None = None, exit: [Cell](#source.models.base.Cell) | None = None) → list[[Cell](#source.models.base.Cell)] + +Найти путь от start до exit. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **start** – Стартовая клетка. + * **exit** – Целевая клетка. +* **Результат:** + Список клеток пути (от start до exit включительно). + Пустой список, если путь не найден. + +## Оркестратор + +### *class* source.strategy.solver.MazeSolver(maze: [Maze](#source.models.base.Maze), strategy: [PathFindingStrategy](#source.strategy.algorithms.PathFindingStrategy)) + +Базовые классы: `object` + +Оркестратор поиска пути в лабиринте. + +Принимает лабиринт и стратегию поиска, выполняет поиск +и возвращает результат вместе со статистикой выполнения. + +### Пример + +solver = MazeSolver(maze, BFSStrategy()) +stats = solver.solve() +print(stats) + +solver.set_strategy(AStarStrategy()) +stats = solver.solve() + +#### \_\_init_\_(maze: [Maze](#source.models.base.Maze), strategy: [PathFindingStrategy](#source.strategy.algorithms.PathFindingStrategy)) → None + +Инициализирует солвер с лабиринтом и стратегией поиска. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **strategy** – Стратегия поиска пути. + +#### set_strategy(strategy: [PathFindingStrategy](#source.strategy.algorithms.PathFindingStrategy)) → None + +Заменяет текущую стратегию поиска. + +* **Параметры:** + **strategy** – Новая стратегия поиска пути. + +#### solve(start: [Cell](#source.models.base.Cell) = None, exit: [Cell](#source.models.base.Cell) = None) → [SearchStats](#source.strategy.solver.SearchStats) + +Выполняет поиск пути и собирает статистику. + +Если start или exit не переданы явно, стратегия найдёт +их самостоятельно по флагам is_start / is_exit в лабиринте. + +* **Параметры:** + * **start** – Стартовая клетка (опционально). + * **exit** – Конечная клетка (опционально). +* **Результат:** + Объект SearchStats с временем выполнения, количеством + посещённых клеток и длиной найденного пути. + +### *class* source.strategy.solver.SearchStats(elapsed_ms: float, visited_count: int, path_length: int, path: list[[Cell](#source.models.base.Cell)]) + +Базовые классы: `object` + +Статистика выполнения поиска пути. + +#### elapsed_ms + +Время выполнения в миллисекундах. + +* **Type:** + float + +#### visited_count + +Количество посещённых клеток. + +* **Type:** + int + +#### path_length + +Длина найденного пути (0 если путь не найден). + +* **Type:** + int + +#### path + +Найденный путь — список клеток от старта до выхода. + +* **Type:** + list[[source.models.base.Cell](#source.models.base.Cell)] + +#### \_\_init_\_(elapsed_ms: float, visited_count: int, path_length: int, path: list[[Cell](#source.models.base.Cell)]) → None + +#### elapsed_ms *: float* + +#### path *: list[[Cell](#source.models.base.Cell)]* + +#### path_length *: int* + +#### visited_count *: int* + +## Визуализация + +### *class* source.view.observer.ConsoleView + +Базовые классы: [`Observer`](#source.view.observer.Observer) + +Отображает состояние лабиринта и события в консоли. + +#### PATH_SYMBOL *= '·'* + +#### PLAYER_SYMBOL *= 'P'* + +#### render(maze: [Maze](#source.models.base.Maze), player: [Cell](#source.models.base.Cell) | None = None, path: list[[Cell](#source.models.base.Cell)] | None = None) → None + +Рисует лабиринт в консоли. + +Путь отмечается символом „·“, позиция игрока — „P“. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **player** – Текущая клетка игрока (опционально). + * **path** – Список клеток найденного пути (опционально). + +#### update(event: [Event](#source.view.observer.Event)) → None + +Реагирует на события и выводит информацию в консоль. + +* **Параметры:** + **event** – Объект события. + +### *class* source.view.observer.Event(type: str, payload: dict = None) + +Базовые классы: `object` + +Событие, передаваемое наблюдателям. + +#### type + +Тип события („maze_loaded“, „path_found“, „move“, „no_path“). + +* **Type:** + str + +#### payload + +Дополнительные данные события. + +* **Type:** + dict + +#### \_\_init_\_(type: str, payload: dict = None) → None + +#### payload *: dict* *= None* + +#### type *: str* + +### *class* source.view.observer.Observer + +Базовые классы: `ABC` + +Интерфейс наблюдателя за событиями лабиринта. + +#### *abstractmethod* update(event: [Event](#source.view.observer.Event)) → None + +Обрабатывает входящее событие. + +* **Параметры:** + **event** – Объект события с типом и данными. + +## Управление игроком + +### *class* source.view.command.Command + +Базовые классы: `ABC` + +Интерфейс команды с поддержкой отмены. + +#### *abstractmethod* execute() → bool + +Выполняет команду. + +* **Результат:** + True если команда выполнена успешно, False иначе. + +#### *abstractmethod* undo() → None + +Отменяет команду, восстанавливая предыдущее состояние. + +### *class* source.view.command.CommandHistory + +Базовые классы: `object` + +Хранит историю выполненных команд и позволяет отменять их. + +### Пример + +history = CommandHistory() +cmd = MoveCommand(player, „w“, maze) +if cmd.execute(): + +> history.push(cmd) + +history.undo() # отменяет последний успешный ход + +#### \_\_init_\_() → None + +#### clear() → None + +Очищает историю команд. + +#### push(command: [Command](#source.view.command.Command)) → None + +Добавляет выполненную команду в историю. + +* **Параметры:** + **command** – Успешно выполненная команда. + +#### undo() → bool + +Отменяет последнюю команду из истории. + +* **Результат:** + True если отмена выполнена, False если история пуста. + +### *class* source.view.command.MoveCommand(player: [Player](#source.view.command.Player), direction: str, maze: [Maze](#source.models.base.Maze)) + +Базовые классы: [`Command`](#source.view.command.Command) + +Перемещает игрока в заданном направлении. + +Сохраняет предыдущую клетку для возможности отмены хода. + +#### \_\_init_\_(player: [Player](#source.view.command.Player), direction: str, maze: [Maze](#source.models.base.Maze)) → None + +Инициализирует команду перемещения. + +* **Параметры:** + * **player** – Объект игрока. + * **direction** – Направление („w“, „a“, „s“, „d“). + * **maze** – Объект лабиринта для проверки проходимости. +* **Исключение:** + **ValueError** – Если направление не распознано. + +#### execute() → bool + +Перемещает игрока если целевая клетка проходима. + +* **Результат:** + True если перемещение выполнено, False если клетка непроходима. + +#### undo() → None + +Возвращает игрока на предыдущую клетку. + +### *class* source.view.command.Player(cell: [Cell](#source.models.base.Cell)) + +Базовые классы: `object` + +Хранит текущее положение игрока в лабиринте. + +#### cell + +Текущая клетка игрока. + +#### \_\_init_\_(cell: [Cell](#source.models.base.Cell)) → None + +Инициализирует игрока на заданной клетке. + +* **Параметры:** + **cell** – Начальная клетка игрока. \ No newline at end of file diff --git a/skorohodovsa/task_2/docs/Makefile b/skorohodovsa/task_2/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/skorohodovsa/task_2/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/skorohodovsa/task_2/docs/_build/markdown/api.md b/skorohodovsa/task_2/docs/_build/markdown/api.md new file mode 100644 index 0000000..3fcd9a9 --- /dev/null +++ b/skorohodovsa/task_2/docs/_build/markdown/api.md @@ -0,0 +1,470 @@ +# API Reference + +## Базовые модели + +### *class* source.models.base.Cell(x: int, y: int, is_wall: bool = False, is_start: bool = False, is_exit: bool = False) + +Базовые классы: `object` + +Представляет одну клетку поля лабиринта. + +Каждая клетка хранит свои координаты и один из четырёх возможных +типов: стена, старт, выход или пустая клетка. Типы взаимоисключают +друг друга: установка одного автоматически сбрасывает остальные. + +#### \_\_init_\_(x: int, y: int, is_wall: bool = False, is_start: bool = False, is_exit: bool = False) + +Инициализирует клетку с заданными координатами и типом. + +* **Параметры:** + * **x** – Координата клетки по оси X. + * **y** – Координата клетки по оси Y. + * **is_wall** – Если True — клетка является стеной. + * **is_start** – Если True — клетка является стартом. + * **is_exit** – Если True — клетка является выходом. + +#### *property* is_exit *: bool* + +True, если клетка является выходом из лабиринта. + +#### is_possible() → bool + +Проверяет, можно ли переместиться в эту клетку. + +* **Результат:** + True, если клетка не является стеной, иначе False. + +#### *property* is_start *: bool* + +True, если клетка является стартовой позицией. + +#### *property* is_wall *: bool* + +True, если клетка является стеной. + +### *class* source.models.base.Maze(size: tuple[int, int] = (10, 10)) + +Базовые классы: `object` + +Представляет двумерный лабиринт из клеток Cell. + +Лабиринт хранится как список списков клеток. Доступ к отдельным +клеткам и их изменение возможны через индексацию вида maze[row, col]. + +#### \_\_init_\_(size: tuple[int, int] = (10, 10)) + +Создаёт пустой лабиринт заданного размера. + +* **Параметры:** + **size** – Кортеж (width, height) — ширина и высота лабиринта в клетках. + +#### *property* exit *: [Cell](#source.models.base.Cell) | None* + +#### get_cell(x: int, y: int) → [Cell](#source.models.base.Cell) | None + +Возвращает клетку по координатам или None, если координаты вне границ. + +* **Параметры:** + * **x** – Координата по оси X. + * **y** – Координата по оси Y. +* **Результат:** + Объект Cell, если координаты корректны, иначе None. + +#### get_neighbors(x: int, y: int) → list[[Cell](#source.models.base.Cell)] | None + +Возвращает список проходимых соседей клетки (вверх, вправо, вниз, влево). + +* **Параметры:** + * **x** – Координата клетки по оси X. + * **y** – Координата клетки по оси Y. +* **Результат:** + Список проходимых соседних клеток, или None если (x, y) вне границ. + +#### *property* shape *: tuple[int, int]* + +#### *property* start *: [Cell](#source.models.base.Cell) | None* + +## Загрузка лабиринта + +### *class* source.build.builder.MazeBuilder + +Базовые классы: `ABC` + +#### *abstractmethod* build_from_file(filename: str) → [Maze](#source.models.base.Maze) + +Возвращает объект лабиринта по указанному пути файлу. + +* **Параметры:** + **filename** (*str*) – Путь к файлу +* **Исключение:** + **TypeError** – Если введен путь файла с нерассмотренным расширением +* **Результат:** + Объект лабиринта +* **Тип результата:** + [Maze](#source.models.base.Maze) + +### *class* source.build.builder.TextFileBuilder + +Базовые классы: [`MazeBuilder`](#source.build.builder.MazeBuilder) + +#### build_from_file(filename: str) → [Maze](#source.models.base.Maze) + +Возвращает объект лабиринта по указанному пути файлу. + +* **Параметры:** + **filename** (*str*) – Путь к файлу +* **Исключение:** + **TypeError** – Если введен путь файла с нерассмотренным расширением +* **Результат:** + Объект лабиринта +* **Тип результата:** + [Maze](#source.models.base.Maze) + +## Стратегии поиска пути + +### *class* source.strategy.algorithms.PathFindingStrategy + +Базовые классы: `ABC` + +Интерфейс стратегии поиска пути в лабиринте. + +#### *abstractmethod* find_path(maze: [Maze](#source.models.base.Maze), start: [Cell](#source.models.base.Cell) = None, exit: [Cell](#source.models.base.Cell) = None) → list[[Cell](#source.models.base.Cell)] + +Найти путь от start до exit. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **start** – Стартовая клетка. + * **exit** – Целевая клетка. +* **Результат:** + Список клеток пути (от start до exit включительно). + Пустой список, если путь не найден. + + + +### *class* source.strategy.bfs.BFSStrategy + +Базовые классы: [`PathFindingStrategy`](#source.strategy.algorithms.PathFindingStrategy) + +Поиск в ширину (Breadth-First Search). + +Гарантирует кратчайший путь по количеству шагов. +Сложность: O(V + E) по времени и памяти. + +#### find_path(maze: [Maze](#source.models.base.Maze), start: [Cell](#source.models.base.Cell) | None = None, exit: [Cell](#source.models.base.Cell) | None = None) → list[[Cell](#source.models.base.Cell)] + +Найти путь от start до exit. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **start** – Стартовая клетка. + * **exit** – Целевая клетка. +* **Результат:** + Список клеток пути (от start до exit включительно). + Пустой список, если путь не найден. + + + +### *class* source.strategy.dfs.DFSStrategy + +Базовые классы: [`PathFindingStrategy`](#source.strategy.algorithms.PathFindingStrategy) + +Поиск в глубину (Depth-First Search). + +Находит путь, но не гарантирует кратчайший. + +#### find_path(maze: [Maze](#source.models.base.Maze), start: [Cell](#source.models.base.Cell) | None = None, exit: [Cell](#source.models.base.Cell) | None = None) → list[[Cell](#source.models.base.Cell)] + +Найти путь от start до exit. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **start** – Стартовая клетка. + * **exit** – Целевая клетка. +* **Результат:** + Список клеток пути (от start до exit включительно). + Пустой список, если путь не найден. + + + +### *class* source.strategy.astar.AStarStrategy + +Базовые классы: [`PathFindingStrategy`](#source.strategy.algorithms.PathFindingStrategy) + +Алгоритм A\* с манхэттенской эвристикой. + +#### find_path(maze: [Maze](#source.models.base.Maze), start: [Cell](#source.models.base.Cell) | None = None, exit: [Cell](#source.models.base.Cell) | None = None) → list[[Cell](#source.models.base.Cell)] + +Найти путь от start до exit. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **start** – Стартовая клетка. + * **exit** – Целевая клетка. +* **Результат:** + Список клеток пути (от start до exit включительно). + Пустой список, если путь не найден. + +## Оркестратор + +### *class* source.strategy.solver.MazeSolver(maze: [Maze](#source.models.base.Maze), strategy: [PathFindingStrategy](#source.strategy.algorithms.PathFindingStrategy)) + +Базовые классы: `object` + +Оркестратор поиска пути в лабиринте. + +Принимает лабиринт и стратегию поиска, выполняет поиск +и возвращает результат вместе со статистикой выполнения. + +### Пример + +solver = MazeSolver(maze, BFSStrategy()) +stats = solver.solve() +print(stats) + +solver.set_strategy(AStarStrategy()) +stats = solver.solve() + +#### \_\_init_\_(maze: [Maze](#source.models.base.Maze), strategy: [PathFindingStrategy](#source.strategy.algorithms.PathFindingStrategy)) → None + +Инициализирует солвер с лабиринтом и стратегией поиска. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **strategy** – Стратегия поиска пути. + +#### set_strategy(strategy: [PathFindingStrategy](#source.strategy.algorithms.PathFindingStrategy)) → None + +Заменяет текущую стратегию поиска. + +* **Параметры:** + **strategy** – Новая стратегия поиска пути. + +#### solve(start: [Cell](#source.models.base.Cell) = None, exit: [Cell](#source.models.base.Cell) = None) → [SearchStats](#source.strategy.solver.SearchStats) + +Выполняет поиск пути и собирает статистику. + +Если start или exit не переданы явно, стратегия найдёт +их самостоятельно по флагам is_start / is_exit в лабиринте. + +* **Параметры:** + * **start** – Стартовая клетка (опционально). + * **exit** – Конечная клетка (опционально). +* **Результат:** + Объект SearchStats с временем выполнения, количеством + посещённых клеток и длиной найденного пути. + +### *class* source.strategy.solver.SearchStats(elapsed_ms: float, visited_count: int, path_length: int, path: list[[Cell](#source.models.base.Cell)]) + +Базовые классы: `object` + +Статистика выполнения поиска пути. + +#### elapsed_ms + +Время выполнения в миллисекундах. + +* **Type:** + float + +#### visited_count + +Количество посещённых клеток. + +* **Type:** + int + +#### path_length + +Длина найденного пути (0 если путь не найден). + +* **Type:** + int + +#### path + +Найденный путь — список клеток от старта до выхода. + +* **Type:** + list[[source.models.base.Cell](#source.models.base.Cell)] + +#### \_\_init_\_(elapsed_ms: float, visited_count: int, path_length: int, path: list[[Cell](#source.models.base.Cell)]) → None + +#### elapsed_ms *: float* + +#### path *: list[[Cell](#source.models.base.Cell)]* + +#### path_length *: int* + +#### visited_count *: int* + +## Визуализация + +### *class* source.view.observer.ConsoleView + +Базовые классы: [`Observer`](#source.view.observer.Observer) + +Отображает состояние лабиринта и события в консоли. + +#### PATH_SYMBOL *= '·'* + +#### PLAYER_SYMBOL *= 'P'* + +#### render(maze: [Maze](#source.models.base.Maze), player: [Cell](#source.models.base.Cell) | None = None, path: list[[Cell](#source.models.base.Cell)] | None = None) → None + +Рисует лабиринт в консоли. + +Путь отмечается символом „·“, позиция игрока — „P“. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **player** – Текущая клетка игрока (опционально). + * **path** – Список клеток найденного пути (опционально). + +#### update(event: [Event](#source.view.observer.Event)) → None + +Реагирует на события и выводит информацию в консоль. + +* **Параметры:** + **event** – Объект события. + +### *class* source.view.observer.Event(type: str, payload: dict = None) + +Базовые классы: `object` + +Событие, передаваемое наблюдателям. + +#### type + +Тип события („maze_loaded“, „path_found“, „move“, „no_path“). + +* **Type:** + str + +#### payload + +Дополнительные данные события. + +* **Type:** + dict + +#### \_\_init_\_(type: str, payload: dict = None) → None + +#### payload *: dict* *= None* + +#### type *: str* + +### *class* source.view.observer.Observer + +Базовые классы: `ABC` + +Интерфейс наблюдателя за событиями лабиринта. + +#### *abstractmethod* update(event: [Event](#source.view.observer.Event)) → None + +Обрабатывает входящее событие. + +* **Параметры:** + **event** – Объект события с типом и данными. + +## Управление игроком + +### *class* source.view.command.Command + +Базовые классы: `ABC` + +Интерфейс команды с поддержкой отмены. + +#### *abstractmethod* execute() → bool + +Выполняет команду. + +* **Результат:** + True если команда выполнена успешно, False иначе. + +#### *abstractmethod* undo() → None + +Отменяет команду, восстанавливая предыдущее состояние. + +### *class* source.view.command.CommandHistory + +Базовые классы: `object` + +Хранит историю выполненных команд и позволяет отменять их. + +### Пример + +history = CommandHistory() +cmd = MoveCommand(player, „w“, maze) +if cmd.execute(): + +> history.push(cmd) + +history.undo() # отменяет последний успешный ход + +#### \_\_init_\_() → None + +#### clear() → None + +Очищает историю команд. + +#### push(command: [Command](#source.view.command.Command)) → None + +Добавляет выполненную команду в историю. + +* **Параметры:** + **command** – Успешно выполненная команда. + +#### undo() → bool + +Отменяет последнюю команду из истории. + +* **Результат:** + True если отмена выполнена, False если история пуста. + +### *class* source.view.command.MoveCommand(player: [Player](#source.view.command.Player), direction: str, maze: [Maze](#source.models.base.Maze)) + +Базовые классы: [`Command`](#source.view.command.Command) + +Перемещает игрока в заданном направлении. + +Сохраняет предыдущую клетку для возможности отмены хода. + +#### \_\_init_\_(player: [Player](#source.view.command.Player), direction: str, maze: [Maze](#source.models.base.Maze)) → None + +Инициализирует команду перемещения. + +* **Параметры:** + * **player** – Объект игрока. + * **direction** – Направление („w“, „a“, „s“, „d“). + * **maze** – Объект лабиринта для проверки проходимости. +* **Исключение:** + **ValueError** – Если направление не распознано. + +#### execute() → bool + +Перемещает игрока если целевая клетка проходима. + +* **Результат:** + True если перемещение выполнено, False если клетка непроходима. + +#### undo() → None + +Возвращает игрока на предыдущую клетку. + +### *class* source.view.command.Player(cell: [Cell](#source.models.base.Cell)) + +Базовые классы: `object` + +Хранит текущее положение игрока в лабиринте. + +#### cell + +Текущая клетка игрока. + +#### \_\_init_\_(cell: [Cell](#source.models.base.Cell)) → None + +Инициализирует игрока на заданной клетке. + +* **Параметры:** + **cell** – Начальная клетка игрока. diff --git a/skorohodovsa/task_2/docs/_build/markdown/stage1.md b/skorohodovsa/task_2/docs/_build/markdown/stage1.md new file mode 100644 index 0000000..7597841 --- /dev/null +++ b/skorohodovsa/task_2/docs/_build/markdown/stage1.md @@ -0,0 +1,91 @@ +# Этап 1. Модель лабиринта + +В первом этапе разработки необходимо создать базовые классы `Cell` и `Maze`, которые представляют карту лабиринта. Паттерны на этом этапе не применяются — только чистые классы. + +## Класс `Cell` + +Клетка — минимальная единица лабиринта. Хранит координаты и тип: стена, старт, выход или пустая. + +По условию задания клетка должна иметь флаги `isWall`, `isStart`, `isExit` и метод `isPassable()`. В реализации флаги оформлены как **свойства** (`@property`) с сеттерами — это позволяет автоматически сбрасывать остальные флаги при установке нового типа. + +```python +cell = Cell(1, 1) +cell.is_wall = True +``` + +Типы клетки взаимоисключают друг друга — клетка не может быть одновременно стеной и стартом. Логика сброса вынесена в приватный метод `_clear_flags()`. + +### Символьное представление + +Для вывода лабиринта в консоль каждая клетка возвращает символ через `__str__`. Символы берутся из `cell_mapping` в `source/settings.py`, что позволяет менять отображение без правки классов: + +| Тип | Символ по умолчанию | +|--------|-----------------------| +| Стена | `#` | +| Старт | `S` | +| Выход | `E` | +| Пустая | | + +## Класс `Maze` + +Лабиринт хранит двумерный список клеток и предоставляет методы для работы с ними. + +По условию задания требовались методы `getCell(x, y)` и `getNeighbors(cell)`. В реализации добавлено несколько вещей сверх задания: + +### Именование методов + +Задание написано в стиле Java/pseudocode — названия методов и полей используют `camelCase` (`isWall`, `getCell`, `isPassable`). В Python принят другой стандарт именования — **PEP 8**, который предписывает `snake_case` для методов и атрибутов. Поэтому все названия были приведены к Python стилю: + +| Задание | Реализация | +|---------------------------|-----------------------------| +| `isWall` | `is_wall` | +| `isStart` | `is_start` | +| `isExit` | `is_exit` | +| `isPassable()` | `is_possible()` | +| `getCell(x, y)` | `get_cell(x, y)` | +| `getNeighbors(cell)` | `get_neighbors(x, y)` | +| `buildFromFile(filename)` | `build_from_file(filename)` | + +Это соответствует стандарту оформления кода на Python и делает API классов идиоматичным для языка. + +### Индексация `maze[row, col]` + +Вместо явного вызова `get_cell()` реализованы `__getitem__` и `__setitem__`, что позволяет обращаться к клеткам естественным образом: + +```python +maze[0, 0] = cell_mapping['wall'] # установить стену +cell = maze[2, 3] # получить клетку +``` + +Обратите внимание: индексация идёт в формате `[row, col]`, то есть сначала строка (Y), потом столбец (X) — аналогично numpy. + +### Свойства `start` и `exit` + +Добавлены свойства для быстрого получения стартовой и выходной клетки без ручного обхода: + +```python +maze.start # Cell или None +maze.exit # Cell или None +``` + +Это оказалось необходимым при реализации алгоритмов поиска пути — стратегии получают `start` и `exit` автоматически из лабиринта. + +### Свойство `shape` + +По аналогии с numpy добавлено свойство `shape`, возвращающее `(height, width)`: + +```python +rows, cols = maze.shape +``` + +Используется в стратегиях поиска и тестах для итерации по лабиринту. + +### `get_neighbors` + +Метод возвращает список проходимых соседей клетки по четырём направлениям. Стены и клетки за границей лабиринта исключаются автоматически. Если переданные координаты вне границ — возвращает `None`. + +```python +neighbors = maze.get_neighbors(2, 2) # список Cell +``` + +Направления обхода: вниз → вправо → вверх → влево (порядок влияет на поведение DFS). diff --git a/skorohodovsa/task_2/docs/_build/markdown/stage2.md b/skorohodovsa/task_2/docs/_build/markdown/stage2.md new file mode 100644 index 0000000..c605d04 --- /dev/null +++ b/skorohodovsa/task_2/docs/_build/markdown/stage2.md @@ -0,0 +1,60 @@ +# Этап 2. Загрузка лабиринта из файла + +Во втором этапе реализована загрузка лабиринта из текстового файла с применением паттерна **Builder**. + +## Паттерн Builder + +Процесс создания лабиринта из файла включает несколько шагов: чтение файла, валидацию структуры, парсинг символов и заполнение клеток. Builder скрывает эти детали от клиента — снаружи виден только один метод `build_from_file()`, внутри которого сосредоточена вся логика построения. + +Дополнительное преимущество: в будущем можно легко добавить новый формат (например, JSON или бинарный) через новую реализацию `MazeBuilder` без изменения остального кода. + +## Класс `MazeBuilder` + +Абстрактный базовый класс — интерфейс паттерна Builder. Объявляет единственный метод `build_from_file()`, который обязан реализовать каждый конкретный строитель. + +По условию задания интерфейс назывался `MazeBuilder` с методом `buildFromFile`. В реализации название метода приведено к **PEP 8** — `build_from_file`. Сам класс оформлен через `ABC` — попытка создать объект `MazeBuilder()` напрямую вызовет `TypeError`. + +## Класс `TextFileBuilder` + +Конкретная реализация строителя для текстовых файлов. Загружает лабиринт из `.txt` файла где `#` — стена, — проход, `S` — старт, `E` — выход. + +Процесс построения разбит на три приватных шага: + +### `_read_file` + +Читает файл построчно и обрезает символы переноса строки `\n` и `\r`. Возвращает список строк — каждая строка соответствует одной строке лабиринта. + +### `_test_text_maze` + +Валидирует структуру: проверяет что все строки одинаковой длины. Если нет — лабиринт некорректен и `_create_maze` выбросит `ValueError`. + +Реализован как `@staticmethod` — не использует состояние объекта, только входные данные. + +### `_create_maze` + +Создаёт объект `Maze` нужного размера и заполняет его клетки символами из файла через `maze[y, x] = symbol`. Тип каждой клетки определяется автоматически через `cell_mapping` в `__setitem__` лабиринта. + +## Использование + +```python +from source.build.builder import TextFileBuilder + +maze = TextFileBuilder().build_from_file('source/templates/10x10_path_v1.txt') +print(maze) +``` + +## Известная ошибка + +В текущей реализации `_create_maze` есть опечатка при вычислении `width`: + +```python +height, width = len(text_maze), len(text_maze) # width всегда равен height +``` + +Правильная версия: + +```python +height, width = len(text_maze), len(text_maze[0]) +``` + +На квадратных лабиринтах (10×10, 50×50) это не проявляется, но на прямоугольных даст некорректный результат. diff --git a/skorohodovsa/task_2/docs/_build/markdown/stage3.md b/skorohodovsa/task_2/docs/_build/markdown/stage3.md new file mode 100644 index 0000000..8a52b27 --- /dev/null +++ b/skorohodovsa/task_2/docs/_build/markdown/stage3.md @@ -0,0 +1,90 @@ +# Этап 3. Стратегии поиска пути + +В третьем этапе реализованы алгоритмы поиска пути с применением паттерна **Strategy**. + +## Паттерн Strategy + +Все три алгоритма реализуют один интерфейс `PathFindingStrategy`. Это позволяет переключать алгоритм в любой момент без изменения кода клиента — достаточно передать другой объект стратегии: + +```python +solver = MazeSolver(maze, BFSStrategy()) +solver.set_strategy(AStarStrategy()) +``` + +Новый алгоритм добавляется реализацией интерфейса — остальной код трогать не нужно. + +## Структура пакета + +Стратегии разбиты по отдельным файлам, а `__init__.py` собирает всё в один импорт: + +```default +source/strategy/ +├── __init__.py ← единственный импорт для пользователя +├── algorithms.py ← базовый класс PathFindingStrategy +├── bfs.py +├── dfs.py +└── astar.py +``` + +```python +from source.strategy import BFSStrategy, DFSStrategy, AStarStrategy +``` + +## Класс `PathFindingStrategy` + +Абстрактный базовый класс — интерфейс паттерна. Объявляет абстрактный метод `find_path()` и содержит два вспомогательных метода, общих для всех стратегий. + +По условию задания метод назывался `findPath` — приведён к **PEP 8** как `find_path`. + +### `_validate` + +Добавлен в процессе разработки — изначально в задании не было требования к обработке отсутствия старта или выхода. Проблема проявилась при тестировании лабиринтов типа `noexit`: алгоритм падал с `AttributeError` внутри, вместо понятного сообщения. + +`_validate` подставляет `start` и `exit` из лабиринта если они не переданы явно, и выбрасывает `ValueError` с понятным сообщением если клетки не найдены: + +```python +start, exit = self._validate(maze, start, exit) +``` + +Вынесен в базовый класс чтобы не дублировать в каждом алгоритме. + +### `_reconstruct_path` + +Восстанавливает путь по словарю предков `came_from`. Все три алгоритма строят этот словарь одинаково — `{клетка: откуда_пришли}` — поэтому восстановление вынесено в общий метод базового класса. + +Алгоритм идёт от выхода к старту по цепочке предков, затем разворачивает список: + +```default +exit → D → C → B → start (идём по came_from) +start → B → C → D → exit (после reverse) +``` + +## Алгоритмы + +### BFS — `BFSStrategy` + +Поиск в ширину. Использует `deque` как очередь (FIFO) — каждый раз берём самую старую клетку из начала. Это гарантирует послойный обход и кратчайший путь по количеству шагов. + +Сложность: O(V + E) по времени и памяти. + +### DFS — `DFSStrategy` + +Поиск в глубину. Использует `list` как стек (LIFO) — каждый раз берём самую свежую клетку с конца. Алгоритм ныряет вглубь по одному направлению до тупика, затем возвращается. + +Не гарантирует кратчайший путь. На запутанных лабиринтах может обойти почти все клетки прежде чем найти выход, хотя по времени часто быстрее BFS из-за меньших накладных расходов на структуру данных. + +Сложность: O(V + E) по времени и памяти. + +### A\* — `AStarStrategy` + +Использует `heapq` как приоритетную очередь. На каждом шаге выбирает клетку с минимальным значением `f = g + h`, где `g` — стоимость пути от старта, `h` — манхэттенская эвристика до выхода. + +Эвристика направляет поиск в сторону выхода, поэтому A\* обходит меньше клеток чем BFS при том же гарантированно кратчайшем пути. + +В кортеж приоритетной очереди добавлен счётчик `counter` как tie-breaker — без него `heapq` попытался бы сравнивать объекты `Cell` при одинаковом `f`, что вызвало бы `TypeError`: + +```python +heapq.heappush(open_heap, (f, counter, neighbor)) +``` + +Сложность: O(E · log V) в худшем случае. diff --git a/skorohodovsa/task_2/docs/_build/markdown/stage4.md b/skorohodovsa/task_2/docs/_build/markdown/stage4.md new file mode 100644 index 0000000..cc800b4 --- /dev/null +++ b/skorohodovsa/task_2/docs/_build/markdown/stage4.md @@ -0,0 +1,44 @@ +# Этап 4. Класс-оркестратор MazeSolver + +В четвёртом этапе реализован класс `MazeSolver`, который объединяет лабиринт и стратегию поиска, выполняет поиск и собирает статистику. + +## Роль в архитектуре + +`MazeSolver` — точка входа для клиентского кода. Он не знает деталей ни одного алгоритма и не работает напрямую с клетками лабиринта — только делегирует задачу стратегии и замеряет время: + +```python +solver = MazeSolver(maze, BFSStrategy()) +stats = solver.solve() +print(stats) +# Время: 0.041 мс | Посещено клеток: 13 | Длина пути: 13 +``` + +## Класс `SearchStats` + +Оформлен через `@dataclass` — это избавляет от ручного `__init__` и автоматически даёт `__repr__`. Хранит четыре поля: время выполнения, количество посещённых клеток, длину пути и сам путь как список клеток. + +`__str__` переопределён для удобного вывода в консоль и отчётах. + +### Ограничение + +В текущей реализации `visited_count` и `path_length` всегда равны друг другу — оба вычисляются как `len(path)`. Это потому что стратегии возвращают только финальный путь, а не все посещённые клетки. Чтобы получить точное количество посещений, потребовалось бы дорабатывать каждую стратегию — добавлять счётчик внутри `find_path`. На данном этапе это сознательное упрощение. + +## Класс `MazeSolver` + +### `set_strategy` + +Позволяет менять алгоритм без пересоздания солвера. Это и есть основная демонстрация паттерна Strategy в действии — один объект, разные алгоритмы: + +```python +solver = MazeSolver(maze, BFSStrategy()) +stats_bfs = solver.solve() + +solver.set_strategy(AStarStrategy()) +stats_astar = solver.solve() +``` + +### `solve` + +Замеряет время через `time.perf_counter()` — самый точный таймер в Python для коротких интервалов, не зависящий от системных часов. Результат переводится в миллисекунды умножением на 1000. + +`start` и `exit` можно не передавать — стратегия найдёт их сама через `_validate`. Явная передача нужна только если хочется запустить поиск не от стандартного старта, а от произвольной клетки. diff --git a/skorohodovsa/task_2/docs/_build/markdown/stage5.md b/skorohodovsa/task_2/docs/_build/markdown/stage5.md new file mode 100644 index 0000000..0ce2cc4 --- /dev/null +++ b/skorohodovsa/task_2/docs/_build/markdown/stage5.md @@ -0,0 +1,84 @@ +# Этап 5. Визуализация и пошаговое управление + +В пятом этапе реализованы два паттерна: **Observer** для отображения событий и **Command** для пошагового управления игроком. + +## 5.1. Паттерн Observer + +### Идея + +`MazeSolver` и игровой цикл не знают как именно отображать происходящее — они просто генерируют события. Наблюдатели подписываются на эти события и реагируют по своему усмотрению. Это позволяет в будущем добавить, например, `FileLogger` или графический интерфейс без изменения основного кода. + +### Класс `Event` + +Оформлен через `@dataclass`. Хранит тип события строкой и словарь `payload` с дополнительными данными. Поддерживаются четыре типа событий: + +| Тип | Когда генерируется | +|---------------|----------------------------| +| `maze_loaded` | Лабиринт загружен из файла | +| `path_found` | Алгоритм нашёл путь | +| `no_path` | Путь не найден | +| `move` | Игрок сделал ход | + +### Класс `Observer` + +Абстрактный базовый класс с единственным методом `update(event)`. Любой наблюдатель обязан его реализовать. + +### Класс `ConsoleView` + +Конкретная реализация наблюдателя. Обрабатывает события через `match/case` и вызывает `render()` для перерисовки лабиринта. + +Метод `render()` принимает лабиринт, опциональную позицию игрока и опциональный путь. Путь преобразуется в `set` для быстрой проверки принадлежности клетки — это O(1) вместо O(n) при каждом обходе: + +```python +path_set = set(path) if path else set() +``` + +Лабиринт обрамляется рамкой из `+` и `─` для читаемости в консоли. Символы игрока и пути вынесены в константы класса — легко поменять без правки логики: + +```python +PLAYER_SYMBOL = "P" +PATH_SYMBOL = "·" +``` + +## 5.2. Паттерн Command + +### Идея + +Каждое перемещение игрока оборачивается в объект `MoveCommand`. Это позволяет сохранить предыдущее состояние и отменить ход — реализация `undo` становится тривиальной. + +### Класс `Player` + +Простой контейнер для текущей клетки игрока. Намеренно минималистичный — вся логика перемещения и проверок находится в команде, а не в игроке. + +### Класс `Command` + +Абстрактный интерфейс с двумя методами: `execute()` и `undo()`. `execute()` возвращает `bool` — это отличие от классического варианта паттерна, где команды не возвращают значений. Возврат `False` нужен чтобы не добавлять неуспешный ход в историю. + +### Класс `MoveCommand` + +Хранит ссылку на игрока, направление и лабиринт. При `execute()` проверяет проходимость целевой клетки, сохраняет текущую в `_prev_cell` и перемещает игрока. При `undo()` восстанавливает `_prev_cell`. + +Направления вынесены в словарь `DIRECTIONS` на уровне модуля: + +```python +DIRECTIONS = { + "w": (0, -1), # вверх + "s": (0, 1), # вниз + "a": (-1, 0), # влево + "d": (1, 0), # вправо +} +``` + +### Класс `CommandHistory` + +Стек выполненных команд. Хранит только успешные ходы — неуспешные (`execute()` вернул `False`) в историю не добавляются. `undo()` снимает последнюю команду со стека и вызывает её `undo()`. + +Пример игрового цикла: + +```python +cmd = MoveCommand(player, 'd', maze) +if cmd.execute(): + history.push(cmd) # добавляем только успешный ход + +history.undo() # отмена последнего хода +``` diff --git a/skorohodovsa/task_2/docs/_build/markdown/stage6.md b/skorohodovsa/task_2/docs/_build/markdown/stage6.md new file mode 100644 index 0000000..4399a8b --- /dev/null +++ b/skorohodovsa/task_2/docs/_build/markdown/stage6.md @@ -0,0 +1,61 @@ +# Этап 6. Экспериментальная часть + +В шестом этапе проведено сравнение эффективности трёх стратегий поиска пути на лабиринтах разной сложности. Эксперимент реализован в Jupyter Notebook (`practice/main.ipynb`). + +## Подготовка + +Лабиринты загружаются из папки `source/templates` автоматически — все файлы считываются через `os.listdir` и передаются в `TextFileBuilder`. Стратегии собраны в словарь для удобной итерации: + +```python +strategies = { + "BFS": BFSStrategy(), + "DFS": DFSStrategy(), + "A*": AStarStrategy(), +} +``` + +## Замеры + +Каждая пара лабиринт + стратегия запускается **10 раз**, результаты усредняются. Это сглаживает разброс из-за кэширования и фоновой активности системы. + +Лабиринты типа `noexit` пропускаются автоматически — стратегия выбрасывает `ValueError`, который перехватывается через `try/except`, и выполнение продолжается. + +Результаты собираются в список словарей и затем преобразуются в `DataFrame` через pandas. + +## Результаты + +### 10×10 (простой путь) + +На маленьких лабиринтах все три алгоритма показывают практически одинаковое время (~0.03–0.07 мс) и одинаковую длину пути. Разница незначительна — лабиринт слишком мал чтобы эвристика A\* давала преимущество. + +### 50×50 (тупики) + +BFS стабильно быстрее DFS по времени при одинаковой длине пути. DFS заходит в каждый тупик до конца и тратит время на возврат, хотя финальный путь совпадает. A\* показывает время между BFS и DFS. + +### 100×100 (запутанный, spaghetti) + +Наиболее показательные результаты: + +| Стратегия | Время (мс) | Длина пути | +|-------------|--------------|--------------| +| BFS | ~9 | ~210 | +| DFS | ~7 | ~2200 | +| A\* | ~8 | ~210 | + +DFS быстрее по времени, но находит путь в 10 раз длиннее — обходит почти весь лабиринт. BFS и A\* находят кратчайший путь, A\* при этом чуть быстрее за счёт эвристики. + +### 30×30 (пустой) + +Неожиданный результат: DFS быстрее всех (~0.73 мс против 1.1 у BFS и 2.0 у A\*), хотя находит путь из 379 клеток против 55 у BFS. На пустом поле без стен DFS сразу уходит вглубь без возвратов, тогда как BFS строит очередь и обходит клетки волнами во все стороны — это накладные расходы на структуру данных. + +A\* на пустом лабиринте медленнее всех — накладные расходы на `heapq` и вычисление эвристики не окупаются когда препятствий нет. + +## Выводы + +- **BFS** — надёжный выбор по умолчанию. Всегда находит кратчайший путь, время предсказуемо. +- **DFS** — быстрый по времени, но путь непредсказуем. На запутанных лабиринтах может пройти весь граф. Подходит когда важна скорость, а не оптимальность пути. +- **A**\* — лучший выбор для больших лабиринтов с препятствиями. Находит кратчайший путь быстрее BFS за счёт эвристики. На простых или пустых лабиринтах проигрывает из-за накладных расходов на приоритетную очередь. + +## Визуализация + +![[results.png]] diff --git a/skorohodovsa/task_2/docs/_build/markdown/stage7.md b/skorohodovsa/task_2/docs/_build/markdown/stage7.md new file mode 100644 index 0000000..9072b69 --- /dev/null +++ b/skorohodovsa/task_2/docs/_build/markdown/stage7.md @@ -0,0 +1,33 @@ +# Этап 7. Отчёт + +## Описание задачи + +Разработать программу для загрузки лабиринта из файла, поиска пути с выбором алгоритма и сравнения их эффективности. Применены четыре паттерна GoF: + +| Паттерн | Где применён | +|--------------|-----------------------------------------------| +| **Builder** | `TextFileBuilder` | +| **Strategy** | `BFSStrategy`, `DFSStrategy`, `AStarStrategy` | +| **Observer** | `ConsoleView` | +| **Command** | `MoveCommand`, `CommandHistory` | + +## Диаграмма классов + +## Результаты экспериментов + +| Лабиринт | Быстрее всех | Кратчайший путь | +|-------------------|----------------|-------------------| +| 10×10 path | все одинаково | все одинаково | +| 50×50 deadends | BFS | BFS = A\* | +| 100×100 spaghetti | DFS | BFS = A\* | +| 30×30 empty | DFS | BFS = A\* | + +**BFS** — надёжный выбор, всегда кратчайший путь.
+\\\\ +**DFS** — быстрый, но путь длиннее. На 100×100 обошёл в 10 раз больше клеток.
+\\\\ +**A**\* — лучший на больших лабиринтах с препятствиями, проигрывает на простых из-за накладных расходов на `heapq`. + +## Выводы + +Паттерны сделали код расширяемым: новый алгоритм — один класс, новый формат файла — один класс, новый способ отображения — один класс. Без паттернов каждое такое изменение потребовало бы правки существующего кода. diff --git a/skorohodovsa/task_2/docs/_build/markdown/task.md b/skorohodovsa/task_2/docs/_build/markdown/task.md new file mode 100644 index 0000000..f30ed53 --- /dev/null +++ b/skorohodovsa/task_2/docs/_build/markdown/task.md @@ -0,0 +1,103 @@ +# Задание + +## Цель работы + +Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры. + +## Общая схема приложения (пример) + +## Выполнение + +### Этап 1. Модель лабиринта (без паттернов, просто классы) + +**Задача:** Создать классы `Cell` и `Maze`, которые представляют карту лабиринта. + +- `Cell` хранит координаты (x, y), флаги `isWall`, `isStart`, `isExit`, метод `isPassable()` (возвращает `True` для прохода, если не стена). +- `Maze` хранит двумерный массив клеток, ширину, высоту, ссылки на стартовую и выходную клетку. Методы: `getCell(x, y)`, `getNeighbors(cell)` – возвращает список соседних проходимых клеток (вверх, вниз, влево, вправо, если в пределах границ и не стена). + +**Результат:** Лабиринт можно создать вручную в коде, но загрузку пока не делаем. + +### Этап 2. Загрузка лабиринта из файла – применение паттерна **Builder** + +**Задача:** Реализовать загрузку лабиринта из текстового файла, где `#` – стена, ` ` (пробел) – проход, `S` – старт, `E` – выход. + +- Создать интерфейс `MazeBuilder` с методом `buildFromFile(filename)`. +- Реализовать класс `TextFileMazeBuilder`, который читает файл, парсит символы, создаёт объекты `Cell`, задаёт координаты и флаги, после чего возвращает готовый `Maze`. + +Процесс построения лабиринта сложный (парсинг, валидация, установка старта/выхода). Builder скрывает детали создания от клиента. В будущем можно легко добавить другой формат (например, JSON или бинарный) через новую реализацию `MazeBuilder`. + +### Этап 3. Стратегии поиска пути – паттерн **Strategy** + +**Задача:** Реализовать семейство алгоритмов поиска пути от старта до выхода. + +- Создать интерфейс `PathFindingStrategy` с методом `findPath(maze, start, exit)`, возвращающим список клеток пути (от старта до выхода включительно) или пустой список, если пути нет. +- Реализовать минимум 3 стратегии: + - **BFS** (поиск в ширину) – гарантирует кратчайший путь по количеству шагов. + - **DFS** (поиск в глубину) – быстрый, но не обязательно кратчайший. + - **A**\* (с эвристикой, например, манхэттенское расстояние) – компромисс между скоростью и оптимальностью. + - (Опционально) **Дейкстра** – полезна для взвешенных лабиринтов, но в базовом варианте все шаги имеют вес 1, тогда она совпадает с BFS. + +Каждая стратегия возвращает путь. Для BFS/DFS используйте очередь/стек, для A\* – приоритетную очередь (heapq). Важно: алгоритмы не должны модифицировать сам лабиринт, только читать состояние клеток. + +Strategy позволяет легко переключать алгоритмы во время выполнения, не меняя код остальной программы. Новый алгоритм можно добавить, реализовав интерфейс. + +### Этап 4. Класс-оркестратор – **MazeSolver** (использует Strategy) + +**Задача:** Создать класс, который принимает лабиринт и стратегию, выполняет поиск и собирает статистику. + +- `MazeSolver` содержит поля `maze` и `strategy`. +- Метод `setStrategy(strategy)` для динамической смены алгоритма. +- Метод `solve()` вызывает `strategy.findPath(...)` и возвращает объект `SearchStats` (время выполнения в миллисекундах, количество посещённых клеток, длина найденного пути). +- Для замера времени используйте `time.perf_counter()` до и после вызова стратегии. + +### Этап 5. Визуализация и пошаговое управление – паттерны **Observer** и **Command** (по желанию) + +**5.1. Наблюдатель (Observer)** – обновление консольного интерфейса. + +- Создать интерфейс `Observer` с методом `update(event)`, где `event` может быть строкой или объектом с типом события (`"path_found"`, `"move"`, `"maze_loaded"`). +- Реализовать класс `ConsoleView`, который отображает лабиринт, текущее положение игрока (если реализован пошаговый режим) и найденный путь. Метод `render(maze, player_position, path)` рисует карту в консоли. +- `MazeSolver` (или отдельный контроллер) может иметь список наблюдателей и уведомлять их при изменении состояния. + +**5.2. Команда (Command)** – для пошагового перемещения игрока по найденному пути (или ручного управления). + +- Создать интерфейс `Command` с методами `execute()` и `undo()`. +- Реализовать `MoveCommand`, который принимает игрока (`Player`), направление и изменяет его позицию, сохраняя предыдущую для отмены. +- Создать класс `Player`, хранящий текущую клетку. +- Консольное меню позволяет вводить команды (W/A/S/D), выполнять `MoveCommand`, при необходимости отменять последний ход (Ctrl+Z). Это опционально, но очень наглядно демонстрирует паттерн. + +*Observer можно реализовать только для вывода сообщений о начале/конце поиска, а Command – для демонстрации undo при ручном исследовании лабиринта.* + +### Этап 6. Экспериментальная часть (аналогично заданию со структурами данных) + +**Задача:** Сравнить эффективность реализованных стратегий на лабиринтах разной сложности. + +1. **Подготовка тестовых лабиринтов:** + - Маленький (10×10) с простым путём. + - Средний (50×50) с тупиками. + - Большой (100×100) с запутанной структурой. + - «Пустой» лабиринт (без стен) – для демонстрации максимальной производительности. + - «Без выхода» – чтобы проверить обработку отсутствия пути. +2. **Замеры:** + - Для каждого лабиринта и каждой стратегии запустить `solve()` 5–10 раз, усреднить время, количество посещённых клеток, длину пути. + - Записать результаты в CSV: `лабиринт,стратегия,время_мс,посещено_клеток,длина_пути`. +3. **Анализ:** + - Построить графики для каждого лабиринта. + - Проанализировать и написать выводы по итогам (эффективность того или иного алгоритма в разных случаях). +4. **Дополнительное задание:** Реализовать взвешенные клетки (например, болото – вес 3, песок – вес 2, асфальт – вес 1) и сравнить Дейкстру с A\* на взвешенном графе. + +### Этап 7. Отчёт + +**Структура отчёта:** + +1. Описание задачи и выбранных паттернов (с диаграммой классов из Mermaid). +2. Листинги ключевых классов (можно выборочно) или ссылка на репозиторий. +3. Результаты экспериментов (таблицы, графики). +4. Анализ эффективности алгоритмов и применимости паттернов. +5. Выводы: как ООП и паттерны помогли сделать код гибким и расширяемым. Что было бы сложно изменить без них. + +## Советы + +- Для A\* самая простая эвристика: `abs(x1 - x2) + abs(y1 - y2)`. +- При поиске пути надо хранить предшественников (`parent` для каждой посещённой клетки), чтобы восстановить путь. +- Для BFS/DFS используй `deque` (очередь) и `list` (стек). +- Визуализацию в консоли можно сделать с помощью `os.system('cls' if os.name == 'nt' else 'clear')` для перерисовки. diff --git a/skorohodovsa/task_2/docs/make.bat b/skorohodovsa/task_2/docs/make.bat new file mode 100644 index 0000000..dc1312a --- /dev/null +++ b/skorohodovsa/task_2/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/skorohodovsa/task_2/docs/source/api.md b/skorohodovsa/task_2/docs/source/api.md new file mode 100644 index 0000000..cf2f512 --- /dev/null +++ b/skorohodovsa/task_2/docs/source/api.md @@ -0,0 +1,70 @@ +# API Reference + +## Базовые модели + +```{eval-rst} +.. automodule:: source.models.base + :members: + :undoc-members: + :show-inheritance: +``` + +## Загрузка лабиринта + +```{eval-rst} +.. automodule:: source.build.builder + :members: + :undoc-members: + :show-inheritance: +``` + +## Стратегии поиска пути + +```{eval-rst} +.. automodule:: source.strategy.algorithms + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: source.strategy.bfs + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: source.strategy.dfs + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: source.strategy.astar + :members: + :undoc-members: + :show-inheritance: +``` + +## Оркестратор + +```{eval-rst} +.. automodule:: source.strategy.solver + :members: + :undoc-members: + :show-inheritance: +``` + +## Визуализация + +```{eval-rst} +.. automodule:: source.view.observer + :members: + :undoc-members: + :show-inheritance: +``` + +## Управление игроком + +```{eval-rst} +.. automodule:: source.view.command + :members: + :undoc-members: + :show-inheritance: + ``` \ No newline at end of file diff --git a/skorohodovsa/task_2/docs/source/conf.py b/skorohodovsa/task_2/docs/source/conf.py new file mode 100644 index 0000000..8fcfe48 --- /dev/null +++ b/skorohodovsa/task_2/docs/source/conf.py @@ -0,0 +1,80 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information +import os +import sys + +sys.path.insert(0, os.path.abspath("../..")) + + +project = "Поиск выхода из лабиринта" +copyright = "2026, SerKin0" +author = "SerKin0" +release = "0.0.1" +html_title = project + + +# --- MyST (Markdown) --- +myst_enable_extensions = [ + "dollarmath", # $x$ и $$x$$ + "amsmath", # \begin{equation} + "colon_fence", # ::: блоки +] + +exclude_patterns = ["build", "draft.md"] + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", + "myst_nb", + "sphinx.ext.mathjax", + "sphinx_new_tab_link", + "sphinx.ext.autosummary", + "sphinxcontrib.mermaid", +] + +autosummary_generate = True + +autodoc_default_options = { + "members": True, + "undoc-members": True, + "show-inheritance": True, + "special-members": "__init__", + "inherited-members": False, + "exclude-members": "__weakref__", +} + +napoleon_google_docstring = True +napoleon_numpy_docstring = False +napoleon_include_init_with_doc = True +napoleon_include_private_with_doc = False + +# --- Тема --- +html_permalinks_icon = "#" +html_theme = "sphinxawesome_theme" +language = "ru" + +html_theme_options = { + "navigation_with_keys": True, + "globaltoc_collapse": False, + "globaltoc_includehidden": False, + "show_prev_next": True, + "main_nav_links": {}, +} + +pygments_style = "monokai" +pygments_style_dark = "monokai" + + +# Для подключения CSS (стили иконок) +html_css_files = [ + "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css", +] + +# --- HTML --- +html_static_path = ["_static"] diff --git a/skorohodovsa/task_2/docs/source/index.md b/skorohodovsa/task_2/docs/source/index.md new file mode 100644 index 0000000..0d882ed --- /dev/null +++ b/skorohodovsa/task_2/docs/source/index.md @@ -0,0 +1,15 @@ +# Лабораторная работа "Поиск выхода из лабиринта" + + +:::{toctree} +:maxdepth: 2 +task +stage1 +stage2 +stage3 +stage4 +stage5 +stage6 +stage7 +api + diff --git a/skorohodovsa/task_2/docs/source/stage1.md b/skorohodovsa/task_2/docs/source/stage1.md new file mode 100644 index 0000000..0365c1c --- /dev/null +++ b/skorohodovsa/task_2/docs/source/stage1.md @@ -0,0 +1,91 @@ +# Этап 1. Модель лабиринта + +В первом этапе разработки необходимо создать базовые классы `Cell` и `Maze`, которые представляют карту лабиринта. Паттерны на этом этапе не применяются — только чистые классы. + +## Класс `Cell` + +Клетка — минимальная единица лабиринта. Хранит координаты и тип: стена, старт, выход или пустая. + +По условию задания клетка должна иметь флаги `isWall`, `isStart`, `isExit` и метод `isPassable()`. В реализации флаги оформлены как **свойства** (`@property`) с сеттерами — это позволяет автоматически сбрасывать остальные флаги при установке нового типа. + +```python +cell = Cell(1, 1) +cell.is_wall = True +``` + +Типы клетки взаимоисключают друг друга — клетка не может быть одновременно стеной и стартом. Логика сброса вынесена в приватный метод `_clear_flags()`. + +### Символьное представление + +Для вывода лабиринта в консоль каждая клетка возвращает символ через `__str__`. Символы берутся из `cell_mapping` в `source/settings.py`, что позволяет менять отображение без правки классов: + +|Тип|Символ по умолчанию| +|---|---| +|Стена|`#`| +|Старт|`S`| +|Выход|`E`| +|Пустая|| + +## Класс `Maze` + +Лабиринт хранит двумерный список клеток и предоставляет методы для работы с ними. + +По условию задания требовались методы `getCell(x, y)` и `getNeighbors(cell)`. В реализации добавлено несколько вещей сверх задания: + +### Именование методов + +Задание написано в стиле Java/pseudocode — названия методов и полей используют `camelCase` (`isWall`, `getCell`, `isPassable`). В Python принят другой стандарт именования — **PEP 8**, который предписывает `snake_case` для методов и атрибутов. Поэтому все названия были приведены к Python стилю: + +|Задание|Реализация| +|---|---| +|`isWall`|`is_wall`| +|`isStart`|`is_start`| +|`isExit`|`is_exit`| +|`isPassable()`|`is_possible()`| +|`getCell(x, y)`|`get_cell(x, y)`| +|`getNeighbors(cell)`|`get_neighbors(x, y)`| +|`buildFromFile(filename)`|`build_from_file(filename)`| + +Это соответствует стандарту оформления кода на Python и делает API классов идиоматичным для языка. + +### Индексация `maze[row, col]` + +Вместо явного вызова `get_cell()` реализованы `__getitem__` и `__setitem__`, что позволяет обращаться к клеткам естественным образом: + +```python +maze[0, 0] = cell_mapping['wall'] # установить стену +cell = maze[2, 3] # получить клетку +``` + +Обратите внимание: индексация идёт в формате `[row, col]`, то есть сначала строка (Y), потом столбец (X) — аналогично numpy. + +### Свойства `start` и `exit` + +Добавлены свойства для быстрого получения стартовой и выходной клетки без ручного обхода: + +```python +maze.start # Cell или None +maze.exit # Cell или None +``` + +Это оказалось необходимым при реализации алгоритмов поиска пути — стратегии получают `start` и `exit` автоматически из лабиринта. + +### Свойство `shape` + +По аналогии с numpy добавлено свойство `shape`, возвращающее `(height, width)`: + +```python +rows, cols = maze.shape +``` + +Используется в стратегиях поиска и тестах для итерации по лабиринту. + +### `get_neighbors` + +Метод возвращает список проходимых соседей клетки по четырём направлениям. Стены и клетки за границей лабиринта исключаются автоматически. Если переданные координаты вне границ — возвращает `None`. + +```python +neighbors = maze.get_neighbors(2, 2) # список Cell +``` + +Направления обхода: вниз → вправо → вверх → влево (порядок влияет на поведение DFS). \ No newline at end of file diff --git a/skorohodovsa/task_2/docs/source/stage2.md b/skorohodovsa/task_2/docs/source/stage2.md new file mode 100644 index 0000000..db3d870 --- /dev/null +++ b/skorohodovsa/task_2/docs/source/stage2.md @@ -0,0 +1,60 @@ +# Этап 2. Загрузка лабиринта из файла + +Во втором этапе реализована загрузка лабиринта из текстового файла с применением паттерна **Builder**. + +## Паттерн Builder + +Процесс создания лабиринта из файла включает несколько шагов: чтение файла, валидацию структуры, парсинг символов и заполнение клеток. Builder скрывает эти детали от клиента — снаружи виден только один метод `build_from_file()`, внутри которого сосредоточена вся логика построения. + +Дополнительное преимущество: в будущем можно легко добавить новый формат (например, JSON или бинарный) через новую реализацию `MazeBuilder` без изменения остального кода. + +## Класс `MazeBuilder` + +Абстрактный базовый класс — интерфейс паттерна Builder. Объявляет единственный метод `build_from_file()`, который обязан реализовать каждый конкретный строитель. + +По условию задания интерфейс назывался `MazeBuilder` с методом `buildFromFile`. В реализации название метода приведено к **PEP 8** — `build_from_file`. Сам класс оформлен через `ABC` — попытка создать объект `MazeBuilder()` напрямую вызовет `TypeError`. + +## Класс `TextFileBuilder` + +Конкретная реализация строителя для текстовых файлов. Загружает лабиринт из `.txt` файла где `#` — стена, — проход, `S` — старт, `E` — выход. + +Процесс построения разбит на три приватных шага: + +### `_read_file` + +Читает файл построчно и обрезает символы переноса строки `\n` и `\r`. Возвращает список строк — каждая строка соответствует одной строке лабиринта. + +### `_test_text_maze` + +Валидирует структуру: проверяет что все строки одинаковой длины. Если нет — лабиринт некорректен и `_create_maze` выбросит `ValueError`. + +Реализован как `@staticmethod` — не использует состояние объекта, только входные данные. + +### `_create_maze` + +Создаёт объект `Maze` нужного размера и заполняет его клетки символами из файла через `maze[y, x] = symbol`. Тип каждой клетки определяется автоматически через `cell_mapping` в `__setitem__` лабиринта. + +## Использование + +```python +from source.build.builder import TextFileBuilder + +maze = TextFileBuilder().build_from_file('source/templates/10x10_path_v1.txt') +print(maze) +``` + +## Известная ошибка + +В текущей реализации `_create_maze` есть опечатка при вычислении `width`: + +```python +height, width = len(text_maze), len(text_maze) # width всегда равен height +``` + +Правильная версия: + +```python +height, width = len(text_maze), len(text_maze[0]) +``` + +На квадратных лабиринтах (10×10, 50×50) это не проявляется, но на прямоугольных даст некорректный результат. \ No newline at end of file diff --git a/skorohodovsa/task_2/docs/source/stage3.md b/skorohodovsa/task_2/docs/source/stage3.md new file mode 100644 index 0000000..63b9dc3 --- /dev/null +++ b/skorohodovsa/task_2/docs/source/stage3.md @@ -0,0 +1,90 @@ +# Этап 3. Стратегии поиска пути + +В третьем этапе реализованы алгоритмы поиска пути с применением паттерна **Strategy**. + +## Паттерн Strategy + +Все три алгоритма реализуют один интерфейс `PathFindingStrategy`. Это позволяет переключать алгоритм в любой момент без изменения кода клиента — достаточно передать другой объект стратегии: + +```python +solver = MazeSolver(maze, BFSStrategy()) +solver.set_strategy(AStarStrategy()) +``` + +Новый алгоритм добавляется реализацией интерфейса — остальной код трогать не нужно. + +## Структура пакета + +Стратегии разбиты по отдельным файлам, а `__init__.py` собирает всё в один импорт: + +``` +source/strategy/ +├── __init__.py ← единственный импорт для пользователя +├── algorithms.py ← базовый класс PathFindingStrategy +├── bfs.py +├── dfs.py +└── astar.py +``` + +```python +from source.strategy import BFSStrategy, DFSStrategy, AStarStrategy +``` + +## Класс `PathFindingStrategy` + +Абстрактный базовый класс — интерфейс паттерна. Объявляет абстрактный метод `find_path()` и содержит два вспомогательных метода, общих для всех стратегий. + +По условию задания метод назывался `findPath` — приведён к **PEP 8** как `find_path`. + +### `_validate` + +Добавлен в процессе разработки — изначально в задании не было требования к обработке отсутствия старта или выхода. Проблема проявилась при тестировании лабиринтов типа `noexit`: алгоритм падал с `AttributeError` внутри, вместо понятного сообщения. + +`_validate` подставляет `start` и `exit` из лабиринта если они не переданы явно, и выбрасывает `ValueError` с понятным сообщением если клетки не найдены: + +```python +start, exit = self._validate(maze, start, exit) +``` + +Вынесен в базовый класс чтобы не дублировать в каждом алгоритме. + +### `_reconstruct_path` + +Восстанавливает путь по словарю предков `came_from`. Все три алгоритма строят этот словарь одинаково — `{клетка: откуда_пришли}` — поэтому восстановление вынесено в общий метод базового класса. + +Алгоритм идёт от выхода к старту по цепочке предков, затем разворачивает список: + +``` +exit → D → C → B → start (идём по came_from) +start → B → C → D → exit (после reverse) +``` + +## Алгоритмы + +### BFS — `BFSStrategy` + +Поиск в ширину. Использует `deque` как очередь (FIFO) — каждый раз берём самую старую клетку из начала. Это гарантирует послойный обход и кратчайший путь по количеству шагов. + +Сложность: O(V + E) по времени и памяти. + +### DFS — `DFSStrategy` + +Поиск в глубину. Использует `list` как стек (LIFO) — каждый раз берём самую свежую клетку с конца. Алгоритм ныряет вглубь по одному направлению до тупика, затем возвращается. + +Не гарантирует кратчайший путь. На запутанных лабиринтах может обойти почти все клетки прежде чем найти выход, хотя по времени часто быстрее BFS из-за меньших накладных расходов на структуру данных. + +Сложность: O(V + E) по времени и памяти. + +### A* — `AStarStrategy` + +Использует `heapq` как приоритетную очередь. На каждом шаге выбирает клетку с минимальным значением `f = g + h`, где `g` — стоимость пути от старта, `h` — манхэттенская эвристика до выхода. + +Эвристика направляет поиск в сторону выхода, поэтому A* обходит меньше клеток чем BFS при том же гарантированно кратчайшем пути. + +В кортеж приоритетной очереди добавлен счётчик `counter` как tie-breaker — без него `heapq` попытался бы сравнивать объекты `Cell` при одинаковом `f`, что вызвало бы `TypeError`: + +```python +heapq.heappush(open_heap, (f, counter, neighbor)) +``` + +Сложность: O(E · log V) в худшем случае. \ No newline at end of file diff --git a/skorohodovsa/task_2/docs/source/stage4.md b/skorohodovsa/task_2/docs/source/stage4.md new file mode 100644 index 0000000..dc1dd66 --- /dev/null +++ b/skorohodovsa/task_2/docs/source/stage4.md @@ -0,0 +1,44 @@ +# Этап 4. Класс-оркестратор MazeSolver + +В четвёртом этапе реализован класс `MazeSolver`, который объединяет лабиринт и стратегию поиска, выполняет поиск и собирает статистику. + +## Роль в архитектуре + +`MazeSolver` — точка входа для клиентского кода. Он не знает деталей ни одного алгоритма и не работает напрямую с клетками лабиринта — только делегирует задачу стратегии и замеряет время: + +```python +solver = MazeSolver(maze, BFSStrategy()) +stats = solver.solve() +print(stats) +# Время: 0.041 мс | Посещено клеток: 13 | Длина пути: 13 +``` + +## Класс `SearchStats` + +Оформлен через `@dataclass` — это избавляет от ручного `__init__` и автоматически даёт `__repr__`. Хранит четыре поля: время выполнения, количество посещённых клеток, длину пути и сам путь как список клеток. + +`__str__` переопределён для удобного вывода в консоль и отчётах. + +### Ограничение + +В текущей реализации `visited_count` и `path_length` всегда равны друг другу — оба вычисляются как `len(path)`. Это потому что стратегии возвращают только финальный путь, а не все посещённые клетки. Чтобы получить точное количество посещений, потребовалось бы дорабатывать каждую стратегию — добавлять счётчик внутри `find_path`. На данном этапе это сознательное упрощение. + +## Класс `MazeSolver` + +### `set_strategy` + +Позволяет менять алгоритм без пересоздания солвера. Это и есть основная демонстрация паттерна Strategy в действии — один объект, разные алгоритмы: + +```python +solver = MazeSolver(maze, BFSStrategy()) +stats_bfs = solver.solve() + +solver.set_strategy(AStarStrategy()) +stats_astar = solver.solve() +``` + +### `solve` + +Замеряет время через `time.perf_counter()` — самый точный таймер в Python для коротких интервалов, не зависящий от системных часов. Результат переводится в миллисекунды умножением на 1000. + +`start` и `exit` можно не передавать — стратегия найдёт их сама через `_validate`. Явная передача нужна только если хочется запустить поиск не от стандартного старта, а от произвольной клетки. \ No newline at end of file diff --git a/skorohodovsa/task_2/docs/source/stage5.md b/skorohodovsa/task_2/docs/source/stage5.md new file mode 100644 index 0000000..ecca7cd --- /dev/null +++ b/skorohodovsa/task_2/docs/source/stage5.md @@ -0,0 +1,84 @@ +# Этап 5. Визуализация и пошаговое управление + +В пятом этапе реализованы два паттерна: **Observer** для отображения событий и **Command** для пошагового управления игроком. + +## 5.1. Паттерн Observer + +### Идея + +`MazeSolver` и игровой цикл не знают как именно отображать происходящее — они просто генерируют события. Наблюдатели подписываются на эти события и реагируют по своему усмотрению. Это позволяет в будущем добавить, например, `FileLogger` или графический интерфейс без изменения основного кода. + +### Класс `Event` + +Оформлен через `@dataclass`. Хранит тип события строкой и словарь `payload` с дополнительными данными. Поддерживаются четыре типа событий: + +|Тип|Когда генерируется| +|---|---| +|`maze_loaded`|Лабиринт загружен из файла| +|`path_found`|Алгоритм нашёл путь| +|`no_path`|Путь не найден| +|`move`|Игрок сделал ход| + +### Класс `Observer` + +Абстрактный базовый класс с единственным методом `update(event)`. Любой наблюдатель обязан его реализовать. + +### Класс `ConsoleView` + +Конкретная реализация наблюдателя. Обрабатывает события через `match/case` и вызывает `render()` для перерисовки лабиринта. + +Метод `render()` принимает лабиринт, опциональную позицию игрока и опциональный путь. Путь преобразуется в `set` для быстрой проверки принадлежности клетки — это O(1) вместо O(n) при каждом обходе: + +```python +path_set = set(path) if path else set() +``` + +Лабиринт обрамляется рамкой из `+` и `─` для читаемости в консоли. Символы игрока и пути вынесены в константы класса — легко поменять без правки логики: + +```python +PLAYER_SYMBOL = "P" +PATH_SYMBOL = "·" +``` + +## 5.2. Паттерн Command + +### Идея + +Каждое перемещение игрока оборачивается в объект `MoveCommand`. Это позволяет сохранить предыдущее состояние и отменить ход — реализация `undo` становится тривиальной. + +### Класс `Player` + +Простой контейнер для текущей клетки игрока. Намеренно минималистичный — вся логика перемещения и проверок находится в команде, а не в игроке. + +### Класс `Command` + +Абстрактный интерфейс с двумя методами: `execute()` и `undo()`. `execute()` возвращает `bool` — это отличие от классического варианта паттерна, где команды не возвращают значений. Возврат `False` нужен чтобы не добавлять неуспешный ход в историю. + +### Класс `MoveCommand` + +Хранит ссылку на игрока, направление и лабиринт. При `execute()` проверяет проходимость целевой клетки, сохраняет текущую в `_prev_cell` и перемещает игрока. При `undo()` восстанавливает `_prev_cell`. + +Направления вынесены в словарь `DIRECTIONS` на уровне модуля: + +```python +DIRECTIONS = { + "w": (0, -1), # вверх + "s": (0, 1), # вниз + "a": (-1, 0), # влево + "d": (1, 0), # вправо +} +``` + +### Класс `CommandHistory` + +Стек выполненных команд. Хранит только успешные ходы — неуспешные (`execute()` вернул `False`) в историю не добавляются. `undo()` снимает последнюю команду со стека и вызывает её `undo()`. + +Пример игрового цикла: + +```python +cmd = MoveCommand(player, 'd', maze) +if cmd.execute(): + history.push(cmd) # добавляем только успешный ход + +history.undo() # отмена последнего хода +``` \ No newline at end of file diff --git a/skorohodovsa/task_2/docs/source/stage6.md b/skorohodovsa/task_2/docs/source/stage6.md new file mode 100644 index 0000000..410842c --- /dev/null +++ b/skorohodovsa/task_2/docs/source/stage6.md @@ -0,0 +1,63 @@ +# Этап 6. Экспериментальная часть + +В шестом этапе проведено сравнение эффективности трёх стратегий поиска пути на лабиринтах разной сложности. Эксперимент реализован в Jupyter Notebook (`practice/main.ipynb`). + +## Подготовка + +Лабиринты загружаются из папки `source/templates` автоматически — все файлы считываются через `os.listdir` и передаются в `TextFileBuilder`. Стратегии собраны в словарь для удобной итерации: + +```python +strategies = { + "BFS": BFSStrategy(), + "DFS": DFSStrategy(), + "A*": AStarStrategy(), +} +``` + +## Замеры + +Каждая пара лабиринт + стратегия запускается **10 раз**, результаты усредняются. Это сглаживает разброс из-за кэширования и фоновой активности системы. + +Лабиринты типа `noexit` пропускаются автоматически — стратегия выбрасывает `ValueError`, который перехватывается через `try/except`, и выполнение продолжается. + +Результаты собираются в список словарей и затем преобразуются в `DataFrame` через pandas. + +## Результаты + +### 10×10 (простой путь) + +На маленьких лабиринтах все три алгоритма показывают практически одинаковое время (~0.03–0.07 мс) и одинаковую длину пути. Разница незначительна — лабиринт слишком мал чтобы эвристика A* давала преимущество. + +### 50×50 (тупики) + +BFS стабильно быстрее DFS по времени при одинаковой длине пути. DFS заходит в каждый тупик до конца и тратит время на возврат, хотя финальный путь совпадает. A* показывает время между BFS и DFS. + +### 100×100 (запутанный, spaghetti) + +Наиболее показательные результаты: + +|Стратегия|Время (мс)|Длина пути| +|---|---|---| +|BFS|~9|~210| +|DFS|~7|~2200| +|A*|~8|~210| + +DFS быстрее по времени, но находит путь в 10 раз длиннее — обходит почти весь лабиринт. BFS и A* находят кратчайший путь, A* при этом чуть быстрее за счёт эвристики. + +### 30×30 (пустой) + +Неожиданный результат: DFS быстрее всех (~0.73 мс против 1.1 у BFS и 2.0 у A*), хотя находит путь из 379 клеток против 55 у BFS. На пустом поле без стен DFS сразу уходит вглубь без возвратов, тогда как BFS строит очередь и обходит клетки волнами во все стороны — это накладные расходы на структуру данных. + +A* на пустом лабиринте медленнее всех — накладные расходы на `heapq` и вычисление эвристики не окупаются когда препятствий нет. + +## Выводы + +- **BFS** — надёжный выбор по умолчанию. Всегда находит кратчайший путь, время предсказуемо. + +- **DFS** — быстрый по времени, но путь непредсказуем. На запутанных лабиринтах может пройти весь граф. Подходит когда важна скорость, а не оптимальность пути. + +- **A*** — лучший выбор для больших лабиринтов с препятствиями. Находит кратчайший путь быстрее BFS за счёт эвристики. На простых или пустых лабиринтах проигрывает из-за накладных расходов на приоритетную очередь. + +## Визуализация + +![[results.png]] \ No newline at end of file diff --git a/skorohodovsa/task_2/docs/source/stage7.md b/skorohodovsa/task_2/docs/source/stage7.md new file mode 100644 index 0000000..ceaa5a9 --- /dev/null +++ b/skorohodovsa/task_2/docs/source/stage7.md @@ -0,0 +1,81 @@ +# Этап 7. Отчёт + +## Описание задачи + +Разработать программу для загрузки лабиринта из файла, поиска пути с выбором алгоритма и сравнения их эффективности. Применены четыре паттерна GoF: + +|Паттерн|Где применён| +|---|---| +|**Builder**|`TextFileBuilder`| +|**Strategy**|`BFSStrategy`, `DFSStrategy`, `AStarStrategy`| +|**Observer**|`ConsoleView`| +|**Command**|`MoveCommand`, `CommandHistory`| + +## Диаграмма классов + +```{mermaid} +classDiagram + class Cell { + +int x, y + +bool is_wall, is_start, is_exit + +is_possible() bool + } + class Maze { + +get_cell(x, y) Cell + +get_neighbors(x, y) list + +start, exit, shape + } + class MazeBuilder { <> } + class TextFileBuilder + class PathFindingStrategy { + <> + +find_path(maze, start, exit) list + } + class BFSStrategy + class DFSStrategy + class AStarStrategy + class MazeSolver { + +set_strategy(strategy) + +solve() SearchStats + } + class ConsoleView { + +update(event) + +render(maze, player, path) + } + class MoveCommand { + +execute() bool + +undo() + } + class CommandHistory { + +push(command) + +undo() bool + } + + Maze *-- Cell + MazeBuilder <|-- TextFileBuilder + TextFileBuilder ..> Maze : creates + PathFindingStrategy <|-- BFSStrategy + PathFindingStrategy <|-- DFSStrategy + PathFindingStrategy <|-- AStarStrategy + MazeSolver --> PathFindingStrategy + MazeSolver --> Maze + MoveCommand --> Maze + CommandHistory --> MoveCommand +``` + +## Результаты экспериментов + +| Лабиринт | Быстрее всех | Кратчайший путь | +| ----------------- | ------------- | --------------- | +| 10×10 path | все одинаково | все одинаково | +| 50×50 deadends | BFS | BFS = A* | +| 100×100 spaghetti | DFS | BFS = A* | +| 30×30 empty | DFS | BFS = A* | + +**BFS** — надёжный выбор, всегда кратчайший путь. +**DFS** — быстрый, но путь длиннее. На 100×100 обошёл в 10 раз больше клеток. +**A*** — лучший на больших лабиринтах с препятствиями, проигрывает на простых из-за накладных расходов на `heapq`. + +## Выводы + +Паттерны сделали код расширяемым: новый алгоритм — один класс, новый формат файла — один класс, новый способ отображения — один класс. Без паттернов каждое такое изменение потребовало бы правки существующего кода. \ No newline at end of file diff --git a/skorohodovsa/task_2/docs/source/task.md b/skorohodovsa/task_2/docs/source/task.md new file mode 100644 index 0000000..0c9274c --- /dev/null +++ b/skorohodovsa/task_2/docs/source/task.md @@ -0,0 +1,183 @@ +# Задание + +## Цель работы + +Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры. + +## Общая схема приложения (пример) + +```{mermaid} +classDiagram + class Maze { + -Cell[] cells + -int width, height + -Cell start + -Cell exit + +getCell(x,y): Cell + +getNeighbors(cell): List~Cell~ + } + + class Cell { + -int x, y + -bool isWall + -bool isStart + -bool isExit + +isPassable(): bool + } + + class MazeBuilder { + <> + +buildFromFile(filename): Maze + } + + class TextFileMazeBuilder { + +buildFromFile(filename): Maze + } + + class PathFindingStrategy { + <> + +findPath(maze, start, exit): List~Cell~ + } + + class BFSStrategy + class DFSStrategy + class AStarStrategy + class DijkstraStrategy + + class SearchStats { + +timeMs: float + +visitedCells: int + +pathLength: int + } + + class MazeSolver { + -Maze maze + -PathFindingStrategy strategy + +setStrategy(strategy) + +solve(): SearchStats + } + + class Command { + <> + +execute() + +undo() + } + + class MoveCommand { + -Player player + -Direction dir + -Cell previousCell + +execute() + +undo() + } + + class Player { + -Cell currentCell + +moveTo(cell) + } + + class Observer { + <> + +update(event) + } + + class ConsoleView { + +update(event) + +render(maze, player, path) + } + + MazeBuilder <|.. TextFileMazeBuilder + MazeBuilder --> Maze : creates + PathFindingStrategy <|.. BFSStrategy + PathFindingStrategy <|.. DFSStrategy + PathFindingStrategy <|.. AStarStrategy + PathFindingStrategy <|.. DijkstraStrategy + MazeSolver --> PathFindingStrategy : uses + MazeSolver --> Maze : uses + Command <|.. MoveCommand + MoveCommand --> Player + Player --> Cell + Observer <|.. ConsoleView + MazeSolver --> Observer : notifies +``` + +## Выполнение + +### Этап 1. Модель лабиринта (без паттернов, просто классы) +**Задача:** Создать классы `Cell` и `Maze`, которые представляют карту лабиринта. +- `Cell` хранит координаты (x, y), флаги `isWall`, `isStart`, `isExit`, метод `isPassable()` (возвращает `True` для прохода, если не стена). +- `Maze` хранит двумерный массив клеток, ширину, высоту, ссылки на стартовую и выходную клетку. Методы: `getCell(x, y)`, `getNeighbors(cell)` – возвращает список соседних проходимых клеток (вверх, вниз, влево, вправо, если в пределах границ и не стена). + +**Результат:** Лабиринт можно создать вручную в коде, но загрузку пока не делаем. + +### Этап 2. Загрузка лабиринта из файла – применение паттерна **Builder** +**Задача:** Реализовать загрузку лабиринта из текстового файла, где `#` – стена, ` ` (пробел) – проход, `S` – старт, `E` – выход. +- Создать интерфейс `MazeBuilder` с методом `buildFromFile(filename)`. +- Реализовать класс `TextFileMazeBuilder`, который читает файл, парсит символы, создаёт объекты `Cell`, задаёт координаты и флаги, после чего возвращает готовый `Maze`. + +Процесс построения лабиринта сложный (парсинг, валидация, установка старта/выхода). Builder скрывает детали создания от клиента. В будущем можно легко добавить другой формат (например, JSON или бинарный) через новую реализацию `MazeBuilder`. + +### Этап 3. Стратегии поиска пути – паттерн **Strategy** +**Задача:** Реализовать семейство алгоритмов поиска пути от старта до выхода. +- Создать интерфейс `PathFindingStrategy` с методом `findPath(maze, start, exit)`, возвращающим список клеток пути (от старта до выхода включительно) или пустой список, если пути нет. +- Реализовать минимум 3 стратегии: + - **BFS** (поиск в ширину) – гарантирует кратчайший путь по количеству шагов. + - **DFS** (поиск в глубину) – быстрый, но не обязательно кратчайший. + - **A*** (с эвристикой, например, манхэттенское расстояние) – компромисс между скоростью и оптимальностью. + - (Опционально) **Дейкстра** – полезна для взвешенных лабиринтов, но в базовом варианте все шаги имеют вес 1, тогда она совпадает с BFS. + +Каждая стратегия возвращает путь. Для BFS/DFS используйте очередь/стек, для A* – приоритетную очередь (heapq). Важно: алгоритмы не должны модифицировать сам лабиринт, только читать состояние клеток. + +Strategy позволяет легко переключать алгоритмы во время выполнения, не меняя код остальной программы. Новый алгоритм можно добавить, реализовав интерфейс. + +### Этап 4. Класс-оркестратор – **MazeSolver** (использует Strategy) +**Задача:** Создать класс, который принимает лабиринт и стратегию, выполняет поиск и собирает статистику. +- `MazeSolver` содержит поля `maze` и `strategy`. +- Метод `setStrategy(strategy)` для динамической смены алгоритма. +- Метод `solve()` вызывает `strategy.findPath(...)` и возвращает объект `SearchStats` (время выполнения в миллисекундах, количество посещённых клеток, длина найденного пути). +- Для замера времени используйте `time.perf_counter()` до и после вызова стратегии. + +### Этап 5. Визуализация и пошаговое управление – паттерны **Observer** и **Command** (по желанию) +**5.1. Наблюдатель (Observer)** – обновление консольного интерфейса. +- Создать интерфейс `Observer` с методом `update(event)`, где `event` может быть строкой или объектом с типом события (`"path_found"`, `"move"`, `"maze_loaded"`). +- Реализовать класс `ConsoleView`, который отображает лабиринт, текущее положение игрока (если реализован пошаговый режим) и найденный путь. Метод `render(maze, player_position, path)` рисует карту в консоли. +- `MazeSolver` (или отдельный контроллер) может иметь список наблюдателей и уведомлять их при изменении состояния. + +**5.2. Команда (Command)** – для пошагового перемещения игрока по найденному пути (или ручного управления). +- Создать интерфейс `Command` с методами `execute()` и `undo()`. +- Реализовать `MoveCommand`, который принимает игрока (`Player`), направление и изменяет его позицию, сохраняя предыдущую для отмены. +- Создать класс `Player`, хранящий текущую клетку. +- Консольное меню позволяет вводить команды (W/A/S/D), выполнять `MoveCommand`, при необходимости отменять последний ход (Ctrl+Z). Это опционально, но очень наглядно демонстрирует паттерн. + +*Observer можно реализовать только для вывода сообщений о начале/конце поиска, а Command – для демонстрации undo при ручном исследовании лабиринта.* + +### Этап 6. Экспериментальная часть (аналогично заданию со структурами данных) +**Задача:** Сравнить эффективность реализованных стратегий на лабиринтах разной сложности. +1. **Подготовка тестовых лабиринтов:** + - Маленький (10×10) с простым путём. + - Средний (50×50) с тупиками. + - Большой (100×100) с запутанной структурой. + - «Пустой» лабиринт (без стен) – для демонстрации максимальной производительности. + - «Без выхода» – чтобы проверить обработку отсутствия пути. +2. **Замеры:** + - Для каждого лабиринта и каждой стратегии запустить `solve()` 5–10 раз, усреднить время, количество посещённых клеток, длину пути. + - Записать результаты в CSV: `лабиринт,стратегия,время_мс,посещено_клеток,длина_пути`. +3. **Анализ:** + - Построить графики для каждого лабиринта. + - Проанализировать и написать выводы по итогам (эффективность того или иного алгоритма в разных случаях). + +4. **Дополнительное задание:** Реализовать взвешенные клетки (например, болото – вес 3, песок – вес 2, асфальт – вес 1) и сравнить Дейкстру с A* на взвешенном графе. + +### Этап 7. Отчёт +**Структура отчёта:** +1. Описание задачи и выбранных паттернов (с диаграммой классов из Mermaid). +2. Листинги ключевых классов (можно выборочно) или ссылка на репозиторий. +3. Результаты экспериментов (таблицы, графики). +4. Анализ эффективности алгоритмов и применимости паттернов. +5. Выводы: как ООП и паттерны помогли сделать код гибким и расширяемым. Что было бы сложно изменить без них. + +## Советы +- Для A* самая простая эвристика: `abs(x1 - x2) + abs(y1 - y2)`. +- При поиске пути надо хранить предшественников (`parent` для каждой посещённой клетки), чтобы восстановить путь. +- Для BFS/DFS используй `deque` (очередь) и `list` (стек). +- Визуализацию в консоли можно сделать с помощью `os.system('cls' if os.name == 'nt' else 'clear')` для перерисовки. \ No newline at end of file diff --git a/skorohodovsa/task_2/practice/main.ipynb b/skorohodovsa/task_2/practice/main.ipynb new file mode 100644 index 0000000..0f49667 --- /dev/null +++ b/skorohodovsa/task_2/practice/main.ipynb @@ -0,0 +1,937 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b41a67fc", + "metadata": {}, + "source": [ + "# Экспериментальная часть " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "3986182c", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import sys\n", + "\n", + "sys.path.insert(0, os.path.abspath(\"..\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c304f83d", + "metadata": {}, + "outputs": [], + "source": [ + "from source.build.builder import TextFileBuilder\n", + "from source.models.base import Maze\n", + "from source.strategy import BFSStrategy, DFSStrategy, AStarStrategy\n", + "from source.strategy.solver import MazeSolver, SearchStats" + ] + }, + { + "cell_type": "markdown", + "id": "f4d32c9b", + "metadata": {}, + "source": [ + "**Задача:** Сравнить эффективность реализованных стратегий на лабиринтах разной сложности." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "4233a72f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "filenames: list[str] = os.listdir(\"../source/templates\")\n", + "\n", + "list_maze: list[Maze] = [\n", + " TextFileBuilder().build_from_file(filename=\"../source/templates/\" + filename)\n", + " for filename in filenames\n", + "]\n", + "\n", + "list_maze" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "8c9592fb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "50" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(filenames)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "04e5e6a6", + "metadata": {}, + "outputs": [], + "source": [ + "strategies = {\n", + " \"BFS\": BFSStrategy(),\n", + " \"DFS\": DFSStrategy(),\n", + " \"A*\": AStarStrategy(),\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "3ccf351f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v1.txt | BFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v1.txt | DFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v1.txt | A*\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v10.txt | BFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v10.txt | DFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v10.txt | A*\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v2.txt | BFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v2.txt | DFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v2.txt | A*\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v3.txt | BFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v3.txt | DFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v3.txt | A*\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v4.txt | BFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v4.txt | DFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v4.txt | A*\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v5.txt | BFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v5.txt | DFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v5.txt | A*\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v6.txt | BFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v6.txt | DFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v6.txt | A*\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v7.txt | BFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v7.txt | DFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v7.txt | A*\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v8.txt | BFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v8.txt | DFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v8.txt | A*\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v9.txt | BFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v9.txt | DFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v9.txt | A*\n" + ] + }, + { + "data": { + "text/plain": [ + "[{'лабиринт': '100x100_spaghetti_v1.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 9.698449999996228,\n", + " 'посещено_клеток': 205.0,\n", + " 'длина_пути': 205.0},\n", + " {'лабиринт': '100x100_spaghetti_v1.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 7.488289999992048,\n", + " 'посещено_клеток': 2129.0,\n", + " 'длина_пути': 2129.0},\n", + " {'лабиринт': '100x100_spaghetti_v1.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 7.629470000074434,\n", + " 'посещено_клеток': 205.0,\n", + " 'длина_пути': 205.0},\n", + " {'лабиринт': '100x100_spaghetti_v10.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 8.57915999999932,\n", + " 'посещено_клеток': 207.0,\n", + " 'длина_пути': 207.0},\n", + " {'лабиринт': '100x100_spaghetti_v10.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 8.580700000084107,\n", + " 'посещено_клеток': 2489.0,\n", + " 'длина_пути': 2489.0},\n", + " {'лабиринт': '100x100_spaghetti_v10.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 8.167220000041198,\n", + " 'посещено_клеток': 207.0,\n", + " 'длина_пути': 207.0},\n", + " {'лабиринт': '100x100_spaghetti_v2.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 8.6136500000066,\n", + " 'посещено_клеток': 217.0,\n", + " 'длина_пути': 217.0},\n", + " {'лабиринт': '100x100_spaghetti_v2.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 6.336869999950068,\n", + " 'посещено_клеток': 2063.0,\n", + " 'длина_пути': 2063.0},\n", + " {'лабиринт': '100x100_spaghetti_v2.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 10.417360000155895,\n", + " 'посещено_клеток': 217.0,\n", + " 'длина_пути': 217.0},\n", + " {'лабиринт': '100x100_spaghetti_v3.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 8.563159999994241,\n", + " 'посещено_клеток': 217.0,\n", + " 'длина_пути': 217.0},\n", + " {'лабиринт': '100x100_spaghetti_v3.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 5.783009999913702,\n", + " 'посещено_клеток': 2107.0,\n", + " 'длина_пути': 2107.0},\n", + " {'лабиринт': '100x100_spaghetti_v3.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 6.953359999988606,\n", + " 'посещено_клеток': 217.0,\n", + " 'длина_пути': 217.0},\n", + " {'лабиринт': '100x100_spaghetti_v4.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 8.79095000000234,\n", + " 'посещено_клеток': 205.0,\n", + " 'длина_пути': 205.0},\n", + " {'лабиринт': '100x100_spaghetti_v4.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 6.422280000060709,\n", + " 'посещено_клеток': 2409.0,\n", + " 'длина_пути': 2409.0},\n", + " {'лабиринт': '100x100_spaghetti_v4.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 6.4153799999985495,\n", + " 'посещено_клеток': 205.0,\n", + " 'длина_пути': 205.0},\n", + " {'лабиринт': '100x100_spaghetti_v5.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 8.864429999994172,\n", + " 'посещено_клеток': 217.0,\n", + " 'длина_пути': 217.0},\n", + " {'лабиринт': '100x100_spaghetti_v5.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 7.053909999922325,\n", + " 'посещено_клеток': 2071.0,\n", + " 'длина_пути': 2071.0},\n", + " {'лабиринт': '100x100_spaghetti_v5.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 10.017580000112503,\n", + " 'посещено_клеток': 217.0,\n", + " 'длина_пути': 217.0},\n", + " {'лабиринт': '100x100_spaghetti_v6.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 9.399200000052588,\n", + " 'посещено_клеток': 243.0,\n", + " 'длина_пути': 243.0},\n", + " {'лабиринт': '100x100_spaghetti_v6.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 7.747049999989031,\n", + " 'посещено_клеток': 1869.0,\n", + " 'длина_пути': 1869.0},\n", + " {'лабиринт': '100x100_spaghetti_v6.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 14.121079999995345,\n", + " 'посещено_клеток': 243.0,\n", + " 'длина_пути': 243.0},\n", + " {'лабиринт': '100x100_spaghetti_v7.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 8.773490000021411,\n", + " 'посещено_клеток': 211.0,\n", + " 'длина_пути': 211.0},\n", + " {'лабиринт': '100x100_spaghetti_v7.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 6.66454999995949,\n", + " 'посещено_клеток': 2283.0,\n", + " 'длина_пути': 2283.0},\n", + " {'лабиринт': '100x100_spaghetti_v7.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 7.22499999997126,\n", + " 'посещено_клеток': 211.0,\n", + " 'длина_пути': 211.0},\n", + " {'лабиринт': '100x100_spaghetti_v8.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 8.687369999961447,\n", + " 'посещено_клеток': 221.0,\n", + " 'длина_пути': 221.0},\n", + " {'лабиринт': '100x100_spaghetti_v8.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 7.4791699999423145,\n", + " 'посещено_клеток': 2473.0,\n", + " 'длина_пути': 2473.0},\n", + " {'лабиринт': '100x100_spaghetti_v8.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 10.324829999944996,\n", + " 'посещено_клеток': 221.0,\n", + " 'длина_пути': 221.0},\n", + " {'лабиринт': '100x100_spaghetti_v9.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 9.33376999992106,\n", + " 'посещено_клеток': 209.0,\n", + " 'длина_пути': 209.0},\n", + " {'лабиринт': '100x100_spaghetti_v9.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 6.118089999927179,\n", + " 'посещено_клеток': 1939.0,\n", + " 'длина_пути': 1939.0},\n", + " {'лабиринт': '100x100_spaghetti_v9.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 7.4974299999212235,\n", + " 'посещено_клеток': 209.0,\n", + " 'длина_пути': 209.0},\n", + " {'лабиринт': '10x10_path_v1.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.03240000014557154,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v1.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.03378000001248438,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v1.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 0.03725999999915075,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v10.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.030790000027991482,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v10.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.035979999984192546,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v10.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 0.03738000000339525,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v2.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.035089999892079504,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '10x10_path_v2.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.045300000101633486,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '10x10_path_v2.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 0.04377000000204134,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '10x10_path_v3.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.035430000025371555,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '10x10_path_v3.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.055879999990793294,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '10x10_path_v3.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 0.04907000002276618,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '10x10_path_v4.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.053650000018024,\n", + " 'посещено_клеток': 29.0,\n", + " 'длина_пути': 29.0},\n", + " {'лабиринт': '10x10_path_v4.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.09142999997493462,\n", + " 'посещено_клеток': 29.0,\n", + " 'длина_пути': 29.0},\n", + " {'лабиринт': '10x10_path_v4.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 0.09646000012253353,\n", + " 'посещено_клеток': 29.0,\n", + " 'длина_пути': 29.0},\n", + " {'лабиринт': '10x10_path_v5.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.035020000041185995,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v5.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.03366999999343534,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v5.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 0.037369999972725054,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v6.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.03128999992441095,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v6.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.03374999996594852,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v6.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 0.037149999934626976,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v7.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.040439999975205865,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '10x10_path_v7.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.05008999996789498,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '10x10_path_v7.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 0.04825999994864105,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '10x10_path_v8.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.04994999994778482,\n", + " 'посещено_клеток': 29.0,\n", + " 'длина_пути': 29.0},\n", + " {'лабиринт': '10x10_path_v8.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.058220000073561096,\n", + " 'посещено_клеток': 29.0,\n", + " 'длина_пути': 29.0},\n", + " {'лабиринт': '10x10_path_v8.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 0.08468999990327575,\n", + " 'посещено_клеток': 29.0,\n", + " 'длина_пути': 29.0},\n", + " {'лабиринт': '10x10_path_v9.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.048520000018470455,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '10x10_path_v9.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.04457000009097101,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '10x10_path_v9.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 0.04779000005328271,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '30x30_empty_v1.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.3075400000161608,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v1.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.7962099999531347,\n", + " 'посещено_клеток': 379.0,\n", + " 'длина_пути': 379.0},\n", + " {'лабиринт': '30x30_empty_v1.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 3.0833899998924608,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v10.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.6444799998680537,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v10.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.1289500000657426,\n", + " 'посещено_клеток': 379.0,\n", + " 'длина_пути': 379.0},\n", + " {'лабиринт': '30x30_empty_v10.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 3.0003100000158156,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v2.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.5947500000493164,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v2.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.0904399999617453,\n", + " 'посещено_клеток': 379.0,\n", + " 'длина_пути': 379.0},\n", + " {'лабиринт': '30x30_empty_v2.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 2.242679999972097,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v3.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.3086099999327416,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v3.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.0075400000459922,\n", + " 'посещено_клеток': 379.0,\n", + " 'длина_пути': 379.0},\n", + " {'лабиринт': '30x30_empty_v3.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 2.17491000003065,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v4.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.5689699999711593,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v4.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.0746400001153233,\n", + " 'посещено_клеток': 379.0,\n", + " 'длина_пути': 379.0},\n", + " {'лабиринт': '30x30_empty_v4.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 2.2863700000471,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v5.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.3352300000406103,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v5.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.8594300000368094,\n", + " 'посещено_клеток': 379.0,\n", + " 'длина_пути': 379.0},\n", + " {'лабиринт': '30x30_empty_v5.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 2.264869999999064,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v6.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.158610000175031,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v6.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.7807300000422401,\n", + " 'посещено_клеток': 379.0,\n", + " 'длина_пути': 379.0},\n", + " {'лабиринт': '30x30_empty_v6.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 2.158290000170382,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v7.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.1674999999286229,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v7.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.7544099999449827,\n", + " 'посещено_клеток': 379.0,\n", + " 'длина_пути': 379.0},\n", + " {'лабиринт': '30x30_empty_v7.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 2.252279999993334,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v8.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.248879999957353,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v8.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.7802099999480561,\n", + " 'посещено_клеток': 379.0,\n", + " 'длина_пути': 379.0},\n", + " {'лабиринт': '30x30_empty_v8.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 2.2779600000831124,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v9.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.201050000054238,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v9.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.7864399999107263,\n", + " 'посещено_клеток': 379.0,\n", + " 'длина_пути': 379.0},\n", + " {'лабиринт': '30x30_empty_v9.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 2.155060000040976,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '50x50_deadends_v1.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.8954199999825505,\n", + " 'посещено_клеток': 729.0,\n", + " 'длина_пути': 729.0},\n", + " {'лабиринт': '50x50_deadends_v1.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.8011099999966973,\n", + " 'посещено_клеток': 729.0,\n", + " 'длина_пути': 729.0},\n", + " {'лабиринт': '50x50_deadends_v1.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 2.820240000028207,\n", + " 'посещено_клеток': 729.0,\n", + " 'длина_пути': 729.0},\n", + " {'лабиринт': '50x50_deadends_v10.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.7890000001225417,\n", + " 'посещено_клеток': 261.0,\n", + " 'длина_пути': 261.0},\n", + " {'лабиринт': '50x50_deadends_v10.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.94999999994252,\n", + " 'посещено_клеток': 261.0,\n", + " 'длина_пути': 261.0},\n", + " {'лабиринт': '50x50_deadends_v10.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 1.0044500000731205,\n", + " 'посещено_клеток': 261.0,\n", + " 'длина_пути': 261.0},\n", + " {'лабиринт': '50x50_deadends_v2.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.0780099999919912,\n", + " 'посещено_клеток': 249.0,\n", + " 'длина_пути': 249.0},\n", + " {'лабиринт': '50x50_deadends_v2.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.8470900000920665,\n", + " 'посещено_клеток': 249.0,\n", + " 'длина_пути': 249.0},\n", + " {'лабиринт': '50x50_deadends_v2.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 1.1555500000213215,\n", + " 'посещено_клеток': 249.0,\n", + " 'длина_пути': 249.0},\n", + " {'лабиринт': '50x50_deadends_v3.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.9816600000704057,\n", + " 'посещено_клеток': 297.0,\n", + " 'длина_пути': 297.0},\n", + " {'лабиринт': '50x50_deadends_v3.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.8496700000014243,\n", + " 'посещено_клеток': 297.0,\n", + " 'длина_пути': 297.0},\n", + " {'лабиринт': '50x50_deadends_v3.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 1.18772000005265,\n", + " 'посещено_клеток': 297.0,\n", + " 'длина_пути': 297.0},\n", + " {'лабиринт': '50x50_deadends_v4.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.5539699999862933,\n", + " 'посещено_клеток': 413.0,\n", + " 'длина_пути': 413.0},\n", + " {'лабиринт': '50x50_deadends_v4.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.6492100000050414,\n", + " 'посещено_клеток': 413.0,\n", + " 'длина_пути': 413.0},\n", + " {'лабиринт': '50x50_deadends_v4.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 1.9135399998958746,\n", + " 'посещено_клеток': 413.0,\n", + " 'длина_пути': 413.0},\n", + " {'лабиринт': '50x50_deadends_v5.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.1528699998507363,\n", + " 'посещено_клеток': 309.0,\n", + " 'длина_пути': 309.0},\n", + " {'лабиринт': '50x50_deadends_v5.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.7570300000897987,\n", + " 'посещено_клеток': 309.0,\n", + " 'длина_пути': 309.0},\n", + " {'лабиринт': '50x50_deadends_v5.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 1.199769999993805,\n", + " 'посещено_клеток': 309.0,\n", + " 'длина_пути': 309.0},\n", + " {'лабиринт': '50x50_deadends_v6.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.0987699999986944,\n", + " 'посещено_клеток': 337.0,\n", + " 'длина_пути': 337.0},\n", + " {'лабиринт': '50x50_deadends_v6.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.9475800000236632,\n", + " 'посещено_клеток': 337.0,\n", + " 'длина_пути': 337.0},\n", + " {'лабиринт': '50x50_deadends_v6.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 1.3809099999434693,\n", + " 'посещено_клеток': 337.0,\n", + " 'длина_пути': 337.0},\n", + " {'лабиринт': '50x50_deadends_v7.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.8687199999258155,\n", + " 'посещено_клеток': 261.0,\n", + " 'длина_пути': 261.0},\n", + " {'лабиринт': '50x50_deadends_v7.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.8890300000293792,\n", + " 'посещено_клеток': 261.0,\n", + " 'длина_пути': 261.0},\n", + " {'лабиринт': '50x50_deadends_v7.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 1.1017699999683828,\n", + " 'посещено_клеток': 261.0,\n", + " 'длина_пути': 261.0},\n", + " {'лабиринт': '50x50_deadends_v8.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.7002200001115853,\n", + " 'посещено_клеток': 565.0,\n", + " 'длина_пути': 565.0},\n", + " {'лабиринт': '50x50_deadends_v8.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.7665699999724893,\n", + " 'посещено_клеток': 565.0,\n", + " 'длина_пути': 565.0},\n", + " {'лабиринт': '50x50_deadends_v8.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 2.3602600000231178,\n", + " 'посещено_клеток': 565.0,\n", + " 'длина_пути': 565.0},\n", + " {'лабиринт': '50x50_deadends_v9.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.7835800000066229,\n", + " 'посещено_клеток': 209.0,\n", + " 'длина_пути': 209.0},\n", + " {'лабиринт': '50x50_deadends_v9.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.0580000001027656,\n", + " 'посещено_клеток': 209.0,\n", + " 'длина_пути': 209.0},\n", + " {'лабиринт': '50x50_deadends_v9.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 0.8593499999733467,\n", + " 'посещено_клеток': 209.0,\n", + " 'длина_пути': 209.0}]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "RUNS = 10\n", + "results = []\n", + "\n", + "\n", + "for filename, maze in zip(filenames, list_maze):\n", + " for strategy_name, strategy in strategies.items():\n", + " try:\n", + " solver = MazeSolver(maze, strategy)\n", + "\n", + " times, visited, lengths = [], [], []\n", + " for _ in range(RUNS):\n", + " stats = solver.solve()\n", + " times.append(stats.elapsed_ms)\n", + " visited.append(stats.visited_count)\n", + " lengths.append(stats.path_length)\n", + "\n", + " results.append(\n", + " {\n", + " \"лабиринт\": filename,\n", + " \"стратегия\": strategy_name,\n", + " \"время_мс\": sum(times) / RUNS,\n", + " \"посещено_клеток\": sum(visited) / RUNS,\n", + " \"длина_пути\": sum(lengths) / RUNS,\n", + " }\n", + " )\n", + " except Exception as ex:\n", + " print(ex, filename, strategy_name, sep=\" | \")\n", + "\n", + "results" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "f6cfb407", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "120" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(results)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "c6d14f8d", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABKYAAAVuCAYAAABP0jSLAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjksIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvJkbTWQAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3Qm8TdX/+P/lulzzlHkmmTJXMlWmlCIipAlpQIo0oIyV0GTIWDLVh6IkUSQRlcisEDJGZhkz7//jvX7/c77nuvdc3LPPOmev+3o+Hptz9773vM97nT2uvdbaqRzHcRQAAAAAAABgWIzpgAAAAAAAAICgYgoAAAAAAAARQcUUAAAAAAAAIoKKKQAAAAAAAEQEFVMAAAAAAACICCqmAAAAAAAAEBFUTAEAAAAAACAiqJgCAAAAAABARFAxBQAAAAAAgIigYgoAAMBS3377rYqNjVWbNm0yEm/fvn0qQ4YMatKkSUbiAQAA76NiCgAAWGXRokUqVapU8aZ06dKp4sWLq3bt2qmNGzeqlODChQvqhRdeUA8//LAqXbq0kZh58+ZVHTp0UK+++qo6ffq0kZgAAMDbUjmO40T6QwAAALhZMVWnTh3VunVrdc899+h5//33n1q3bp0aN26cSpMmjVq/fr0qUqSI1YU+depU9dBDD6k1a9aoihUrGou7Y8cOXQn4/vvvq2eeecZYXAAA4E2xkf4AAAAA4VClShX1yCOPxJt3ww03qC5duqgZM2ao559/3uqCHzVqlKpQoYLRSilRtGhRddttt6mxY8dSMQUAAK6IrnwAACDFyJ8/v/4/bdq08Vr4SHe/fv366VZGUpkjXf8KFy6s50mXuMv9888/qmPHjvp35L3kfZ966il14MCBeL8nfy/vHRcXl2CZuOmmm/Ty2rVrx5t/5swZ9dprr+kueDJmU2C3xLZt217VWE8//fSTv8WYG59JLFy4UN17773quuuu83ePbN++vTp06FC832vYsKFulWZqbCsAAOBdtJgCAABWkjGOfBUm0pXv999/12Mf5cyZUzVv3jzB78+aNUtt27ZNt/KRsZLk5/79+6udO3eqCRMm+H9v165dqnr16urcuXO6Uub6669XW7duVaNHj9YVNytWrFBZs2aN996pU6fW3QhfeeUV/7xff/1VrVq1SlfwXK5Hjx5q2LBh6u6779YtuzJmzKjnP/roo1eV+48//qj/r1q1atDfudbPJC2gpDKuQIEC+n/pCill8fXXX6u///5bl6uPlI+vW6Wp8a0AAIA3UTEFAACs1LdvXz0FKlu2rFqyZImueLrc2rVr1W+//aa7AIrOnTurZs2aqYkTJ6qnn35aVatWTc9/9tln1fnz59Xq1atVwYIF/X/fokUL/TtDhgzRrZICPfjgg+qDDz7QFU4xMTH+rnYyFpZUhl1u+vTpqlSpUmrOnDn+37+WiqkNGzbo/6XSLJhr+UxS8fTcc8/pSqZffvlFZcuWzb/s9ddfV5cuXYr3+764f/zxx1V9XgAAkHLRlQ8AAFhJutbNnz9fT9KqZ/DgwboFlXRvk1ZQl7vzzjv9lVJCurO9/PLL+vWXX36p/z927JiaPXu2uu+++3SrInk/3yRjK5UoUUJ99913Cd77ySefVHv37tV/K+T3p02bpjp16pToZz9x4oTKnj17vEqpa3Hw4EH9f44cOYL+zrV8JqkokxZiUtEXWCnlc/nnlK5+IrGuggAAAIGomAIAAFaSgc7r16+vp0aNGulKJumet337dtW9e/cEv1+mTJkE86SFlfC1IPrzzz9166CPPvpI5cqVK8Eky/fv35/gffLly6fuv/9+3SJJyN9L5U3Tpk0T/ex33HGHWrZsmXrvvfd0JZqv8utqSaWaSOrhy9fymbZs2aL/r1y58lXF98X1fQ4AAIBg6MoHAABSjFtvvVWP//TDDz8k6+99FS7ytL82bdok+jvp06dPdL60RJJucps3b1ZjxozRLZZiYxM/FZMudk888YR68cUX1QsvvHDNn1MqycSRI0dUoUKFgv7etXymayFxAz8HAABAMFRMAQCAFEWesnf27NkE8zdu3Bh0rCZ5+pyQrnrSCki6tUlLrGshraCkBVbr1q31mE3S1TCp1kzyhEBpoSRd56Qboq+74dUoV66cv6VTxYoVQ/5MJUuW1P+vWbPG/zopMhh84OcAAAAIhq58AAAgxZDxpk6dOqVuuummRJfJE+kCW0e99dZb+rWve5t0dZMxqmbMmKGfYHc5+Rvf+E7BWihJDBmjKn/+/El+1rZt26qjR4+qL774wt8l8WpJhZNI7DMm5zM98MADKm3atPophcePH0+w/PIug764vs8BAAAQDC2mAACAlaSy5ZNPPtGvpYWUPCFOusilSZNGvfHGGwl+X1oW1a1bVz3zzDO6xdJXX32lvv/+e/0kvOrVq/t/b/To0apWrVrq9ttvV4899phu1STjTsk4VPI3Mu/yp/L5yNP9WrZsqTJmzJjkZ5cKsZkzZ+qByWVQ9WslXehq166tvvnmG/XOO+8k+btX85nk6YNDhw7VZVO+fHmdY5EiRdSePXt0zuPHj1eVKlXy/77Eld+Tp/gBAAAkhYopAABgJekKJ5PvqXHS2qlBgwaqZ8+e6pZbbknw+9JiqFSpUmrgwIF6EPPcuXOr3r176ymQjNm0cuVK3b1OKmWk8kue0CfzGzdurCt5gkmdOrXKmTNnkp970aJF6pVXXlGvvvqqbp2VXB07dlStWrXSnzWxFmLX8pl873f99dert99+Ww0fPlxX9kkLq3r16sUbx2rHjh3qp59+Uu+//36yPzsAAEg5UjlJPa4FAADAclKRUqxYMdW3b9+gLZ286OLFi7oVmLRk8rUcM+H5559X06dP1wOqZ8iQwVhcAADgTYwxBQAAYCFpCSXd+KTVWGIDu4fDP//8o5/uN2DAACqlAADAVaErHwAAgKXuvvtu3XLKFBmb67///jMWDwAAeB8tpgAAAAAAABARjDEFAAAAAACAiKDFFAAAAAAAACKCiikAAAAAAABEhLWDn1+6dEnt3btXZc6cWaVKlSrSHwcAAAAAACBFcBxHnThxQuXPn1/FxMSkzIopqZQqVKhQpD8GAAAAAABAirR7925VsGDBlFkxJS2lfIWQJUuWSH8cAAAAAACAFOH48eO6sZCvbiZFVkz5uu9JpRQVUwAAAAAAAGZdzdBKDH4OAAAAAACAiKBiCgAAAAAAABFBxRQAAAAAAAAiwtoxpgAAAAAAAK7VxYsX1fnz5ym4JKRJk0alTp1auYGKKQAAAAAAkOI5jqP27dun/v333xRfFlcjW7ZsKm/evFc1wHnUVUwtXrxYvf3222rlypXqn3/+UV9++aVq2rRpor/boUMHNXbsWDVkyBDVtWtX458VAAAAAADYz1cplTt3bpUhQ4aQK1xsrsA7ffq0OnDggP45X7583quYOnXqlKpYsaJ6/PHHVbNmzYL+nlRY/frrryp//vxGPx8AAAAAAEhZ3fd8lVLXXXddpD9O1EufPr3+XyqnpMxC6dYXkYqphg0b6ikpe/bsUc8++6yaN2+euvfee419NgAAAAAAkLL4xpSSllK4Or6ykrLzXMXUlVy6dEk9+uij6qWXXlI33nhjpD8OgCS0/Kxj0GXTWo2m7AAAAAB4Bt33zJdVVFZMDR48WMXGxqrnnnvuqv/m7NmzevI5fvy4v5JLJgDhkUoF3xmx7QEAAADwArl2kbGTfBOuzFdWidW7XMu1YNRVTMmA6MOGDVOrVq26ptq3gQMHqv79+yeYf/DgQXXmzBmXPyUAn/wxOYMWhm8wPAAAAACIZtIdTSpTLly4oCdcmZSTlNnhw4dVmjRp4i07ceKE8mzF1JIlS/TFbOHCheMNQvbCCy+ooUOHqh07diT6dz179lTdunWL12KqUKFCKleuXCpLlixGPjuQEu29dCjoMhkEDwAAAACinTRokcoU6b0lk899L84y+jlmvXPfNf9Nu3bt1KRJk/w/58iRQ91yyy26N1qFChX0vJiYmAR/V7NmTV0HIz788EM1cuRI9ddff+n8ixUrplq0aKHrWoKR35P3lcHi06VLF2/Z5T97qmJKxpaqX79+vHl33XWXni+FHUxcXJyeLieFlNgXAMAdjgrezJVtDwAAAIAXyLWL9NryTZGSKpmx7777bjVhwgT9et++fapXr16qcePGateuXf7fkeXyez5p06bV8caPH6+ef/55NXz4cHXHHXfoYZLWrVunfv/99yQ/j6+sEqt3uZZrwYhUTJ08eVJt3brV//P27dvVmjVrdK2etJS6/NGM0iQsb968qlSpUhH4tAAAAAAAANErLi5O15sI+b9Hjx7qtttu08MbSU8ykS1bNv/vBJo1a5Zq2bKlat++vX+eyQfRRaQp0YoVK1TlypX1JKQLnrzu06dPJD4OAAAAAACAFU6ePKk++eQTVaJEiQQNfxIjlVW//vqr2rlzp4qEiLSYql279jWNch9sXCkAAAAAAICUbvbs2SpTpkz69alTp1S+fPn0vMAuda1bt1apU6f2/yyVV02bNlV9+/ZVzZo1U0WLFlUlS5ZU1atXV/fcc4964IEHjAzPwuBLAAAAAAAAHlanTh09RJJMy5cv12N1N2zYMF4rqCFDhvh/R6Y777xTz5dKrKVLl6r169erLl266KfttWnTRo9HJU/dCzcqpgAAAAAAADwsY8aMuuueTPJEvnHjxumWU/K0vcAue77fkUn+JlC5cuVUp06ddEuq+fPn6+nHH38M+2enYgoAAAAAAMAiqf7/p+X9999/yfr7smXL6v+lcsvKMaYAAAAAAADgjrNnz6p9+/bp10ePHlUjRozQg6A3btz4in/bsWNHlT9/flW3bl1VsGBB9c8//6g33nhDP81PxpsKNyqmAAAAAAAAPGzu3Ll6rCiROXNmVbp0aTV9+nT98LkrqV+/vho/frwaPXq0Onz4sMqZM6eukFqwYMFVPdUvVFRMAQAAAAAAJOLrd5tEfblMnDhRT0lxHCfosubNm+spUhhjCgAAAAAAABFBxRQAAAAAAAAigoopAAAAAAAARAQVUwAAAAAAAIgIKqYAAAAAAAAQEVRMAQAAAAAAICKomAIAAAAAAEBEUDEFAAAAAACAiKBiCgAAAAAAABFBxRQAAAAAAAAiIjYyYQEAAAAAAKLbtgHNjcYr/uoX1/w3bdu2VZMmTdKvY2NjVY4cOVSFChVU69at9bKYmP/XJqlo0aJq586d8f62QIEC6u+//9avv/zySzV48GC1ceNGdenSJVW4cGF15513qqFDh6pwosUUAAAAAACAh919993qn3/+UTt27FDffvutqlOnjurSpYtq1KiRunDhgv/3XnvtNf17vmn16tV6/oIFC1SrVq1U8+bN1fLly9XKlSvVgAED1Pnz58P+2WkxBQAAAAAA4GFxcXEqb968/lZQVapUUdWqVVP16tVTEydOVE888YReljlzZv/vBfr6669VzZo11UsvveSfV7JkSdW0adOwf3ZaTAEAAAAAAFimbt26qmLFimrGjBlX/F2prPrjjz/U77//rkyLSMXU4sWLVePGjVX+/PlVqlSp1MyZM/3LpJlY9+7dVfny5VXGjBn17zz22GNq7969kfioAAAAAAAAnlS6dGndvc9H6lsyZcrkn4YPH67nP/vss+qWW27RdTEyFtWDDz6oxo8fr86ePWtnxdSpU6d0rd3IkSMTLDt9+rRatWqV6t27t/5favb+/PNPdd9990XiowIAAAAAAHiS4zi6QZCPdNVbs2aNf5KGQEIaBs2ZM0dt3bpV9erVS1davfDCC6pq1aq6nsa6MaYaNmyop8RkzZpVzZ8/P968ESNG6MLYtWuXHhUeAAAAAAAASZMn7BUrVsz/c86cOVWJEiWC/v7111+vJxmT6tVXX9XjTH322WeqXbt2KkUPfn7s2DFdw5ctW7agvyPNywKbmB0/flz/L484lAlAeKRS/1f7fjm2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VZs3b070ySZuGThwoDp27JgKNzl4nz592oo4NuViaj0zlY+pXEzEuf/++9WhQ4eULfszeepXTEyMFXFsysXUumZTLibjmDhPt+1YY1McU7mYuh60KR9TZWYLuvIBgIft3btXd32SAcoB1rPo2WbYNgEgOvebNsWx7VhjWz64elRMAQD8pGXUvn379Ou8efPGe8qQ1+3fv1+dPXs2rOMXyZOZnnnmGZUzZ04VTnLSJk+v8RppPRDusgEiSVr+FipUSMXG2tEp4cKFC9bkImRoXXkaGIJ/33/88Ue884CyZct68ngDwFsYY8rjF1lujskjj4cdNmyY6tmzp57ktcxz299//61OnjyZ6IWWG32KDx8+rBYuXKiOHDnivxAaPHiwLquNGzeqcJKxBKTJZjhPqCS3Dz/8UM2ePVuXmVvfSWBz8yVLlqiHH35Y3XbbbeqRRx5RS5cuDTmGPPp1586dygQpG+kS+PPPP+uff/jhB3XPPfeou+++W33wwQeuxfnvv//U+PHj9dgrDRs2VPfee6969tln1YIFC5TJk8hdu3aF/D7jxo3TJ585cuTQ/we+/uijj5SbY4nJuBstW7ZMUE6yDroxHseJEyf0eit322TMknPnzunKonz58ukxMu644w51/PjxkGLI318+SaXegAED1LZt2/zzQjVt2jT9+X1GjBih80qXLp2u4AnXuGyyXs2fP19/999//71rTerz5Mmj6tWrp6ZMmaIrCcNFPq98D5cuXdI/Sywpy08//VQfO9106tQpfez67LPP1PTp09XKlSv1vtqEv/76y7UxeWT7e+utt3T3I3nEvUzy+u2331YHDx5Ubg6i/Mknn6hvvvkm3rrtK0u31mlZf/v27av3/0K+I9lPS3lNmDBBhUupUqXCeh4grQkkLzlGv/jii2rTpk2uvO/cuXPV+vXr9WvZbl5//XXdtS4uLk4VLFhQDRo0KOT1unHjxurjjz/Wx85wku1dyub222/X53/ijTfeUJkyZVKZM2dWDz30kCv7Z7F27VrdTUeOXenTp1cZM2ZU5cuXV71793YtxtXug0Ih33mvXr1Urly5VOXKlfW2IpO8zp07t87Htz/1ksuPXcuWLdNl5db5czDt2rXT22q4yOeX/Uw4h1n4999/9fWGfPdyjuhWLDlGmnLgwAF9DPB9djn+y3FO9me+/Z0b5HxDurvK/kaOmTI8iant3yoRfSYgQrJmzRonJiYm5FLcv3+/U6tWLSdVqlROkSJFnKpVq+pJXss8WSa/E6q9e/c6t9xyi/7MqVOndh599FHnxIkT/uX79u0LOZ9ly5Y5WbNm1Z87e/bszooVK5xixYo5N9xwg3P99dc76dOnd1auXBlyLsOGDUt0krx69uzp/zlUDRs2dP7991/9+vDhw86tt96qc8uVK5cuq9KlSzsHDhwIOY58319//bV+PXPmTP3e9913n9O9e3fn/vvvd9KkSeNfnlzyuaV86tev73z66afO2bNnnXAYM2aMExsb69x0001OlixZnI8//tjJnDmz88QTTzhPP/20XgeGDh0acpwtW7bobSR37txOoUKFdH733nuv/o4kzxYtWhh5fLQb+4G33nrLyZAhg9OjRw9n4cKFzoYNG/Qkr2V9zpgxo/P222+H/Fllm5A4zzzzjPPII484adOmdd58801X9wGic+fOetsYPny4foRvkyZNnHLlyjk//fST8+OPPzply5Z1XnnllZBiyOdMbJL1IPD/UMl7+Pa/48ePd9KlS+f06dPHmTNnjvPGG2/o7+bDDz90pcx82/ju3bt1+cl6nCdPHv1/+fLlnb///jvkOFIud999t/7uZR8tcVevXu24ae3atU6+fPl02cn3vmvXLv2/lFWmTJl03OXLl4cc5+LFi85LL72k1+nA7993LJ01a5bjlfMAKQ8plwIFCjht2rRxXn75ZT3J64IFCzo5cuRwfvvtN1fiZMuWTe+bZV9cokQJ5/fff3d9HyD7fTkOVKlSRX/nEyZM0HHlOPD444/r9W/69OkhxZBjY2KTfH45zvl+DpWUk+84/8cff+hzHCk3OcbIdirrn6zzoSpVqpSzePFi/Vr2y9ddd53z3nvvOd9++60+Zsq+YNCgQSHFkG1DvhfJoUOHDvocLRyef/55J3/+/M4LL7zglClTxunUqZNTuHBh55NPPnGmTJmiy+/ZZ58NOc7cuXP199O8eXN9TJPvQvZpcu4kMeS8859//nG8sB+QfZmcW8o51Pbt253Tp0/rSV6PHTtWn+vIPiFU586d07GkbOSa4KOPPoq33K19gFxz1KxZUx+/br/9dufIkSP6HM23jy5ZsqT+nVDJtpfYJOfNX375pf/nUAwePFh/F+LChQt6vZZ9mJSTbE/t2rXT5Roq2V/5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iCBPYnLrBN73VLHLH0seOD+cJ6Ru5xOpGMShzEyuA25eyMuTxaQJsjz1J7Hp6aefDlsu8tQiubMkd5jkiXDyZEivxrEpF5MVbSbzsaHSUKxevdp54YUXnAIFCuinJYVKjvPyZLHAJz8FTrIsnGXmdj6RjGNTLqbi2JSLyThChhBwY18j+175zJc/BdI3ycV1uPebbuWSkuLYlIttcUzlYhMqpjxE7sB07NjRfwdGHrksj3ANlTzi9qOPPtKPnE5smjNnTlg2qnDlYzoGcSgzk+tAOC7k5fHZo0aNSvIk2819wOUtSxo0aKBblpw4ccK1GKbi2JSLyQoj0/nYUGnoa5FZtmxZfSdWHnU+btw4599//w35vaXySVpHm9oHhDsf03FsysVUHJtyMRnHN9CybK9NmjTRN5bkMfXdu3cP6T1lOALphmy6S384crE9jk252BbHVC62omLKA3r06KFPGtOmTevce++9zpQpU1ztsy4XBPJEkaQORnI31Sv5mIpBHMrM5DoQzgv55557Tl9MJzXwce3atR23yP6katWq+iRYHq4QLibi2JSLyQojE/nYVGl466236otCufv69ttvO3///bfjpubNm+vueqbOA8Kdj8k4NuViKo5NuZiMM3fuXD2Is3Sry5Ejh/PUU0+59qS8AQMG6BbSwezatctp27at44VcbI1jUy62xTGVi+2omPIAuYsRzqd8yIBtH3/8cdDlR44c0c14vZKPqRjEocxMrgOmKiZMuNqnl0klnzyiOprj2JSLyfXMRD42VRq+8sorYX3qp7x3UgO1njt3Treg9ko+JuPYlIupODblYjKODKzcokULZ+bMmXqb9DJTudgUx6ZcbItj07YZSVRMWUSeALJ3717HFibyMVVmxKHMQl0HTFVMRBPpEvnXX39ZEccruUTbehZKPrZVGkbTemaKV7abaIlhWxybcnEjjgysfDUGDhzoHD16NNldkUwwkYttcWzKxbY4pnKxXYyCNRYvXqz++++/ZP/9Tz/9pGzKJ1piEIcyc2MduOGGG67q955++mm1f//+ZMUoUaKE6tevn9q8ebOKBnLzxJY4XsnFxHpmKh9TuURTmYX6/b/xxhtq+/btKlp4ZbuJlhi2xbEpFzfiZM6c+ap+780331RHjhxJVozcuXOrtm3bqvnz56tLly6pcDGRi21xbMrFtjimcrEd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7rrr1GeffeapfEyVGXEoM1PrgKkKkMTs379fjR07Vj+xyw1ykjtlyhT9hLZAX3zxherQoYM6ePCgZ+LYlIvJ9cxEPjZUGsrTMF988UX1v//9T+XMmTPeskOHDqnu3bu78sRMU0zlYyKOTbmYimNTLibj+HTr1k0988wzep8jvV6WL1+upk6dqgYOHKjGjRvnSgypcJebbXKDWp4u/Mgjj6iPPvpIP637+eefV17KxbY4NuViWxxTuVgv0k22EN0OHz7sXLp0KdIfA0jxpP/66tWrE5TD559/7uq4acHGynHzyZwyZlVcXJwzaNAg/bMMotymTRvdZfi9997zVBybcjG5npnIx1Qu4Yyza9cup1y5ck5sbKxTuXJl5+6779aTvJZ58iRd+R2vdOUzlY+JODblYiqOTbmYjBPok08+cUqUKOEfZL1AgQLOuHHjnHD55ZdfnHfffdeZNWuW6+9tKheb4tiUi21xTG+bNtKjcka6cgxJ+++//3Tta4YMGfTPO3fuVF9++aUqW7asatCgQViKb/fu3WFr+WEiH1NlRhzKzNQ60KlTJzV+/HjVv39/fRdWWjfK3Zlp06apAQMGhHQnc926dUku37Rpk2rdurW6ePGicsucOXPUE088oUqUKKFblWTKlEl98sknukuxm0zEsSmXcK5npvMxlUu441y6dEnNmzdP/frrr2rfvn16Xt68eVX16tX1PiYmJkaZljlzZt1lOTnd/E3lYyKOTbmYimNTLibjXO706dPq5MmTKnfu3MrrTOViUxybcrEtjk3bpnGRrhnDld15553O6NGj9eujR4/qgRQLFiyoB1OTx1O65fz58/rRtjK4obSOkEley6Nwz50756l8TJUZcSgzU+uAkCflyGCktWrVcq6//nqnYsWK+okfoZI7O7K9X/6468D5braYEvII7U6dOun3TpMmTZIDyEZ7HJtyCed6Fol8TOViKk60uPHGGyPSUgtA+E2ePNmpUaOGky9fPv+AzkOGDHFmzpxJ8QMIG8aY8oBVq1apIUOG6Neff/65vhMjAy7LWBwy5kvHjh1diSN9yWVwZhn0XO70iKVLl6p+/fqpw4cPq9GjR3smH1NlRhzKzNQ6IBo2bKgHU5dtMTY2Vn399deutC7JkSOH3u5lsPbE/PHHH6px48bKLX/99Zd66KGH9N1ludP8448/qvvuu091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RlWqVCGr3Llzh7p160YrV66kOXPmUN68ecmEBQsW6CGva9eubcnn8Xll+fLlesh7Hk45W7ZsVLFiRXr66act+Xwepvn111/3yzE4+nFg//79VLBgQUqZMiXdunWLfvrpJ7p586YewpnnW2HHjh00a9YsPfw4/w34s620d+9efd565plnYl3Ox7QzZ85Yct7k4+LQoUP1PspDrDM+v/B1QOfOnem1117zOcb48ePpr7/+oqlTp8a6fPDgwTR27Fi9/fmCj8UZMmSgypUrx7p80KBBehvp378/WcV1/j9+/Djt2rWLihQpYtk+44m33fLly7t/5xx4v/322299jsXb04QJE+j333/Xw597HgPatWtHOXLk8Onzq1atqv82cR2brTR9+nT3dUCzZs1o1apV1KtXL70/NWjQgD7//HN9nvAFX7fytQWfO13HgOHDh1PGjBn18k2bNuljT1zbYaDhv/fSpUtjvQ6oVauWz+uL8f7Bx+a4zll8PONzkFX3NnHhv03y5MmpWLFiln82X3PytQBff/q6zzwO/nvxOdV1/ekrPv7zecDzOoDPA6lSpbLk8/kcwMcUvscw4b///tPnA5cVK1boawHerj3nxxdf6/Mxk68Dq1WrZtmxPyFCwRQYxxdvlSpVcnwMiXEYl1VzoQufjPjiim96nZqLJ7445Ituq06sLocOHdIXpIUKFTJ2kgUA8Bc+Tv7777/6tT/OAZLxQx1+yFO4cGFKkiSJ3V8HhOHrGCseRCakXCTFkZSLtDiS9k07+V70DX7HN/J2ioyMpH79+ln2eVyazLUJevTooZ8G+4OJGBLj8LbGtX+yZMmin5D444bEZC6e+IRhdaGUa//gJ27+LpTKnz8/9enThw4ePEh23KieOHHCcbmYiCMpF884XODq9Hy4lpeJdeaK48919sUXX/hci+hx8XGfj//+OgeYzMdEHM8YXDuvZMmSfimUkrrOEOfxcU3vVq1a6RpF/PDQJK5huHr1asflIikOx+CaZBJyMR3H3+vNzn1TlMds8gc24j4FuE1x9+7dLe234HFZ0ali9E4UeSQ77uOF2/1y3yw8BPrJkycdFUNiHM9tLfoIgFaRlIvJ/ZOHOuf+P3hf5J/cp4Br9C+nHQNM5WIijqRcPOPwvun0fCT9bbh/F/587nya+3qKrYNif+K+7HgESqflYyKOpFxMxZGU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o2pTAQAAAAAAAADEB/qYAurSpQtFRUXFuSby589PK1euxJoCAAAAAAAAAEuhjykAAAAAAAAAALAFmvIBAAAAAAAAAIAtUDAFAAAAAAAAAP9fe/cBJkWxPX6/Fpacc85cJUdFkkiSJCjCVUSUIMoVhauACKhERcQIKoiKgKBI8KIiIEEEQUSQjIDkKFFykNzvc+r/zvxmNpCmp2a69vt5nmZnu4c5c2p7OtRUUEAkUDEFAAAAAACAiKBiCgAAAAAAABFBxRQAAAAAAAAigoopAAAAAAAARAQVUwAAAAAAAIgIKqYAAAAAAAAQEVRMAQAAAAAAICKomAIAAAAAAEBEUDEFAAAAAACAiKBiCgAAAAAAABFBxRQAAAAAAAAigoopAAAAAAAARAQVUwAAwAq7du1SMTExiS7t27eP9FsEAABAHLFxVwAAAHjZvffeq9q2bev//cyZM6pz584RfU8AAABIGBVTAADACo7j6J+33Xabeuyxx/zr//77byqmAAAAohRd+QAAgBUuXryofyZPnvym/p908Uus+1+gwoULq9q1a8eL+a9//Us/d8CAAf71Cxcu1OvGjRsXL56sk23yHJ/9+/erHj16qAoVKqgsWbKo1KlTq1KlSqmhQ4eqK1euhJyHLPL+fcqXL68KFiyorl69Gu91pk6dqp8/fvx4/3u93nIrZQQAACBoMQUAAKyqmEqVKtUt/f8JEyb4H3/yySdq8eLF1/0/7733ntq2bZsK1bp169S0adPUgw8+qIoVK6YuXbqkZs+erXr37q127NihPv7441vKw2fw4MHqn3/+8f/+1FNPqa5du6p58+aphg0bBj33s88+U5kyZVIPPfSQOnDgQNDryXv85ptvdN7Zs2e/offjVhkBAAA7UTEFAACscOLECf0zXbp0N/X/pBJIWvMEdv/78ccfr1sxJZU2r732mnrggQfUd999p0Jxzz336AqowNZHzz//vHr88cfV6NGjdUujPHny3NBrBebhI68hg8MHPufFF1/UlVCBFVN79+7VlVX/+c9/VJo0aVTRokX14iMVTFIx1bx586AWWCbKCAAA2ImufAAAwApHjhzRP3PmzHnTLa1upZWVtGaSLnfX6p4mA6/LGFeBi6yLSyqBfJVS8n6OHTumnyuVRtLdbsWKFcpNmTNnVg8//LCuLDp69Kh//dixY3W8jh07uhLn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2X6EXbt2Vd98841auHChKlKkyHX/z5o1a/RPaTklqlWrpgYPHqwOHz6sm32JefPm6UqnUqVK+Z8za9asoNeR58h6AAjVjsEtE91W9OX/UcAAAAAAYEjszXbfmzhxovruu+9UhgwZ/GNCyUjr0pJJuuvJ9iZNmqhs2bLpMaa6deumZ+wrV66cfm6DBg10BdTjjz+u3nzzTf0ar7zyin5tX4unp59+Wn344YfqxRdfVE888YT66aef1JQpU/RMfQAAAAAAACY06/Gd0YL+/p0Hbvr/tG/fXn3++ef+36Vr3Z133qnrXHx1MQm1ApMxw33DM3366ae6HkbqdaQrozREevjhh/WwSeF2U33cPvroIz0TX+3atXULKN8yefJkvV2mCPzxxx915VOJEiVUjx49VMuWLdX333/vf43kyZPrboDyU1pAPfbYY6pt27Zq0KBB/udIAUgllLSSKl++vHrnnXfU6NGj9cx8AAAAAAAA+D+NGjVSBw4c0Mv8+fN15VLTpk0DnqHU2LFj/c+RZfr06Xr9mDFj9OR2//3vf3WvtyVLluiGQmfOnFEm3HRXvmuRwcZ//vnn676OzNoXt6teXFL5tXr16pt5ewAAAAAAAElOqlSpgsbt7t27t7r77rvVkSNH9KRwInPmzP7nBJIKKmkd1bFjR/+60qVLG3vvjAoOAAAAAABgiTNnzqgvvvhCFS9eXA+zdD1SWfXbb7+p3bt3q0hgDkQgTBhgGwAAAABgwowZM1T69On147Nnz+phl2Rd4Ox4rVu31sMq+UjlVfPmzVX//v1VixYtVOHChdVtt92mh12SscP//e9/39TsereKFlMAAAAAAAAeVqdOHT0+lCzLly/XY3Q3btw4qBXUe++953+OLPfee69eL5VYS5cuVevXr1fPPfecunz5smrXrp0et+rq1athf+9UTAEAAAAAAHhYunTpdNc9WWRGPplATlpOyWx7gV32fM+RRf5PoDJlyqhnnnlGt6SSyehkuZFxxENFxRQAAAAAAIBFYmJidDe8f/7555b+f6lSpfRPqdwKN8aYAgAAAAAA8LALFy6ogwcP6sfHjx9XH374oR4EvVmzZtf9v507d1Z58+ZVdevWVfnz51cHDhxQr732mp7NT8abCjcqpgAAAAAAADxs9uzZeqwokSFDBlWiRAk1depUVbt27ev+3/r166sxY8aojz76SB09elRlz55dV0jNnz//hmb1CxUVUwAAAAAAAAn4/p0Hor5cxo0bp5drcRwn0W0tW7bUS6QwxhQAAAAAAAAigoopAAAAAAAARAQVUwAAAAAAAIgIKqYAAAAAAAAQEQx+DiCq7Bic8KB7RV/+n/H3AgAAAAAIL1pMAQAAAAAAICKomAIAAAAAAEBEUDEFAAAAAACAiKBiCgAAAAAAABFBxRQAAAAAAAAigln5AAAAAAAAbmLW8HApeguzkbdv3159/vnn+nFsbKzKmjWrKleunGrdurXelizZ/2uTVLhwYbV79+6g/5svXz61b98+/fibb75RQ4cOVZs2bVJXr15VBQsWVPfee68aNmyYCidaTAEAAAAAAHhYo0aN1IEDB9SuXbvUDz/8oOrUqaOee+451bRpU3X58mX/8wYNGqSf51tWr16t18+fP1+1atVKtWzZUi1fvlytXLlSDR48WF26dCns750WUwAAAAAAAB6WKlUqlTt3bn8rqEqVKqmqVauqevXqqXHjxqknn3xSb8uQIYP/eYG+//57VaNGDdWzZ0//uttuu001b9487O+dFlMAAAAAAACWqVu3ripfvryaNm3adZ8rlVUbNmxQf/zxhzKNiikAAAAAAAALlShRQnfv8+nVq5dKnz69f3n//ff1+q5du6o777xTlS1bVo9F9cgjj6gxY8aoCxcuhP090pUPAAAAAADAQo7jqJiYGP/v0lVPBkT3yZ49u/6ZLl06NXPmTLV9+3a1YMEC9dtvv6kePXqo4cOHq6VLl6q0adOG7T3SYgoAAAAAAMBCmzZtUkWKFAmqiCpevLh/yZw5c9DzixUrpsejGj16tFq1apXauHGjmjx5cljfIy2mLJ668lammQQAAAAAAN73008/qfXr16tu3brd0v+XLn3SUurs2bMqnKiYAgAAAAAA8LALFy6ogwcPqitXrqhDhw6p2bNnqyFDhqimTZuqtm3bXvf/DxgwQJ07d041adJEFSpUSJ04cUKPP3Xp0iV17733hvW9UzGFJCmxlma0MgMAAAAAeM3s2bNVnjx5VGxsrMqSJYuejU8qltq1a6eSJbv+KE733HOPGjFihK7EkooteY2KFSuquXPnqttvvz2s752KKQAAAAAAAI82Xhg3bpxeridwdr646tSpo5dIYPBzAAAAAAAARAQtpgCPYwB8AAAAAIBXUTGFqMLYTwAAAAAAJB031ZVPRnS/8847VYYMGVTOnDlV8+bN1ebNm4Oec/78efXss8+qbNmyqfTp06uWLVvqgbMC7dmzR91333162kF5nZ49e6rLly8HPWfhwoWqUqVKKlWqVKp48eI31F8SAAAAAAAAllZM/fzzz7rS6bffflPz5s3T0wY2aNBAnT171v+cbt26qe+//15NnTpVP3///v2qRYsW/u0ydaFUSl28eFH9+uuv6vPPP9eVTv369fM/Z+fOnfo5MvDWmjVr1PPPP6+efPJJNWfOHLfyBgAAAAAAgJe68sn0g4GkQklaPK1cuVLVqlVLnTx5Un322Wdq4sSJqm7duvo5Y8eOVSVLltSVWVWrVtVTDW7cuFH9+OOPKleuXKpChQrq1VdfVb169VIDBgxQKVOmVKNGjVJFihRR77zzjn4N+f+//PKLeu+991TDhg3dzB8AAAAAAABeHGNKKqJE1qxZ9U+poJJWVPXr1/c/p0SJEqpgwYJq6dKlumJKfpYtW1ZXSvlIZVPnzp3Vhg0bVMWKFfVzAl/D9xxpOZWYCxcu6MXn1KlT+ufVq1f1YitHxSS6zc28Ix3H7b+hiTiRLjO345hi098GAAAAgDfIfYDjOP4F1+crq4TqXW7mvuqWK6YkiFQU1ahRQ5UpU0avO3jwoG7xlDlz5qDnSiWUbPM9J7BSyrfdt+1az5HKpn/++UelSZMmwfGvBg4cGG/9kSNH9LhXtjqdIV+i2w4fPmxNHDdjmIoT6TJzO44pNv1tAAAAAHiDNLKReg4Z/zruGNhImJSTlNnRo0dVihQpgradPn1ahb1iSsaa+uOPP3QXu2jQp08f1b17d//vUolVoEABlSNHDpUxY0Zlq7On/0p0m3SztCWOmzFMxYl0mbkdxxSb/jYAAAAAvEEatEhlSmxsrF5wfVJOyZIl05PfpU6dOmhb3N+v+TrqFnTp0kXNmDFDLVq0SOXPn9+/Pnfu3HpQ8xMnTgS1mpJZ+WSb7znLly8Pej3frH2Bz4k7k5/8LhVMCbWWEjJ7nyxxSSHJYqsYlXgTQzfzjnQct/+GJuJEuszcjmOKTX8bAAAAAN4g9wExMTH+BdfnK6uE6l1u5r7qpiqmpO9g165d1TfffKMWLlyoBygPVLlyZd18a/78+aply5Z63ebNm9WePXtUtWrV9O/yc/Dgwbq7jK9lgszwJ5VOpUqV8j9n1qxZQa8tz/G9BgAAAAAAQLg9PLmz0UKe0uqjW/p/MlZ3zZo1VaNGjdTMmTPjbZfJ60T79u1VtEl2s933vvjiCz3rXoYMGfRYULLIuE8iU6ZMqmPHjrpL3YIFC/Rg6B06dNAVSjLwuWjQoIGugHr88cfV2rVr1Zw5c9Qrr7yiX9vX4unpp59WO3bsUC+++KL6888/1ciRI9WUKVNUt27dwlEGAAAAAAAAnvXZZ5/phkTSs23//v3+9e+9917QeE/yWNZ5tmLqo48+0jPx1a5dW+XJk8e/TJ482f8cSbBp06a6xVStWrV0t7xp06b5tydPnlx3A5SfUmH12GOPqbZt26pBgwb5nyMtsaSGT1pJlS9fXr3zzjtq9OjRemY+AAAAAAAA/D9nzpzR9TKdO3dW9913n791lMiSJYu699579fjgsshjWRdNbror3/XIAFcjRozQS2IKFSoUr6teXFL5tXr16pt5ewAQVXYM/n9dmuMq+vL/jL8XAAAAAHaaMmWKKlGihLr99tt145/nn39eTxAn4z9J1726deuqKlWq6OfKmN8FCxZU0YRRfgEAAAAAADzcje+xxx7Tj2WMKenp9vPPP+vfZTimhx9+WLekkkUey7poQsUUAAAAAACAB23evFm3gmrdurX+PTY2VrVq1UpXVgmZeE6GSbr77rv1Io9lnWe78gEAAAAAACA6fPbZZ+ry5csqb968QcMwyeRyH374oZ6cLpBMZBd3XaRRMQUAAAAAAOAxly9fVuPHj9cTxjVo0CBoW/PmzdVXX32lnn76af27jDUVraiYAgAAAAAA8JgZM2ao48ePq44dO6pMmTIFbWvZsqVuTeWrmIpmVEwBuCHMMAcAAAAA0eOzzz5T9evXj1cp5auYevPNN9W6detUuXLlVDSjYgoAAAAAACABU1p9FLXl8v333ye6rUqVKnqsKS9gVj4AAAAAAABEBBVTAAAAAAAAiAgqpgAAAAAAABARVEwBAAAAAAAgIqiYAgAAAAAAQERQMQUAAAAAAKCUZ2ays6msqJgCAAAAAABJWooUKfTPc+fORfqteIavrHxld6tiXXo/AAAAAAAAnpQ8eXKVOXNmdfjwYf172rRpVUxMTKTfVtS2lJJKKSkrKTMpu1BQMQUAAAAAAJK83Llz6zLwVU7h2qRSyldmoaBiCgAAAAAAJHnSQipPnjwqZ86c6tKlS0m+PK5Fuu+F2lLKh4opAAAAAACA/59UuLhV6YLrY/BzAAAAAAAARAQVUwAAAAAAAIgIKqYAAAAAAAAQEVRMAQAAAAAAICKomAIAAAAAAEBEUDEFAAAAAACAiKBiCgAAAAAAABFBxRQAAAAAAAAigoopAAAAAAAARAQVUwAAAAAAAIgIKqYAAAAAAAAQEVRMAQAAAAAAICKomAIAAAAAAEBEUDEFAAAAAAAAb1RMLVq0SDVr1kzlzZtXxcTEqG+//TZoe/v27fX6wKVRo0ZBzzl27Jhq06aNypgxo8qcObPq2LGjOnPmTNBz1q1bp+6++26VOnVqVaBAAfXmm2/eao4AAAAAAACwoWLq7Nmzqnz58mrEiBGJPkcqog4cOOBfvvrqq6DtUim1YcMGNW/ePDVjxgxd2dWpUyf/9lOnTqkGDRqoQoUKqZUrV6q33npLDRgwQH3yySc3+3YBAAAAAAAQpWJv9j80btxYL9eSKlUqlTt37gS3bdq0Sc2ePVv9/vvv6o477tDrPvjgA9WkSRP19ttv65ZYX375pbp48aIaM2aMSpkypSpdurRas2aNevfdd4MqsAAAAAAAAJCEKqZuxMKFC1XOnDlVlixZVN26ddVrr72msmXLprctXbpUd9/zVUqJ+vXrq2TJkqlly5apBx98UD+nVq1aulLKp2HDhmro0KHq+PHj+nXjunDhgl4CW12Jq1ev6sVWjopJdJubeUc6jtt/QxNxIl1mpuLwt4l8mQEAAABANLmZex7XK6akG1+LFi1UkSJF1Pbt29VLL72kW1hJZVPy5MnVwYMHdaVV0JuIjVVZs2bV24T8lP8fKFeuXP5tCVVMDRkyRA0cODDe+iNHjqjz588rW53OkC/RbYcPH7YmjpsxTMWJdJmZisPfJvJlBgAAAADR5PTp05GrmHrkkUf8j8uWLavKlSunihUrpltR1atXT4VLnz59VPfu3YNaTMmg6Tly5NCDrNvq7Om/Et0WtwLQy3HcjGEqTqTLzFQc/jaRLzMAAAAAiCYykV1Eu/IFKlq0qMqePbvatm2brpiSsafitha4fPmynqnPNy6V/Dx06FDQc3y/JzZ2lYxrJUtc0kVQFlvFKCfRbW7mHek4bv8NTcSJdJmZisPfJvJlBgAAAADR5GbuecJ+d7Rv3z519OhRlSdPHv17tWrV1IkTJ/Rsez4//fST7n941113+Z8jM/VdunTJ/xyZwe/2229PsBsfAAAAAAAAvOemK6bOnDmjZ8iTRezcuVM/3rNnj97Ws2dP9dtvv6ldu3ap+fPnqwceeEAVL15cD14uSpYsqceheuqpp9Ty5cvVkiVLVJcuXXQXQJmRTzz66KN64POOHTuqDRs2qMmTJ6vhw4cHddUDAAAAAABAEquYWrFihapYsaJehFQWyeN+/frpwc3XrVun7r//fnXbbbfpiqXKlSurxYsXB3Wz+/LLL1WJEiV0174mTZqomjVrqk8++cS/PVOmTGru3Lm60kv+f48ePfTrd+rUya28AQAAAAAAEGE3PcZU7dq1leMkPqbNnDlzrvsaMgPfxIkTr/kcGTRdKrQAAAAAAABgJ0bgBQAAAAAAQERQMQUAAAAAAICIoGIKAAAAAAAAEUHFFAAAAAAAACKCiikAAAAAAABEBBVTAAAAAAAAiAgqpgAAAAAAABARVEwBAAAAAAAgIqiYAgAAAAAAQERQMQUAAAAAAICIoGIKAAAAAAAAEUHFFAAAAAAAACKCiikAAAAAAABEBBVTAAAAAAAAiAgqpgAAAAAAABARVEwBAAAAAAAgIqiYAgAAAAAAQERQMQUAAAAAAICIoGIKAAAAAAAAEUHFFAAAAAAAACKCiikAAAAAAABEBBVTAAAAAAAAiAgqpgAAAAAAABARVEwBAAAAAAAgIqiYAgAAAAAAQERQMQUAAAAAAICIoGIKAAAAAAAAEUHFFAAAAAAAACKCiikAAAAAAABEBBVTAAAAAAAAiAgqpgAAAAAAABARVEwBAAAAAADAGxVTixYtUs2aNVN58+ZVMTEx6ttvvw3a7jiO6tevn8qTJ49KkyaNql+/vtq6dWvQc44dO6batGmjMmbMqDJnzqw6duyozpw5E/ScdevWqbvvvlulTp1aFShQQL355pu3miMAAAAAAABsqJg6e/asKl++vBoxYkSC26UC6f3331ejRo1Sy5YtU+nSpVMNGzZU58+f9z9HKqU2bNig5s2bp2bMmKEruzp16uTffurUKdWgQQNVqFAhtXLlSvXWW2+pAQMGqE8++eRW8wQAAAAAAECUib3Z/9C4cWO9JERaSw0bNky98sor6oEHHtDrxo8fr3LlyqVbVj3yyCNq06ZNavbs2er3339Xd9xxh37OBx98oJo0aaLefvtt3RLryy+/VBcvXlRjxoxRKVOmVKVLl1Zr1qxR7777blAFFgAAAAAAAJJQxdS17Ny5Ux08eFB33/PJlCmTuuuuu9TSpUt1xZT8lO57vkopIc9PliyZbmH14IMP6ufUqlVLV0r5SKuroUOHquPHj6ssWbLEi33hwgW9BLa6ElevXtWLrRwVk+g2N/OOdBy3/4Ym4kS6zEzF4W8T+TIDAAAAgGhyM/c8rlZMSaWUkBZSgeR33zb5mTNnzuA3ERursmbNGvScIkWKxHsN37aEKqaGDBmiBg4cGG/9kSNHgroR2uZ0hnyJbjt8+LA1cdyMYSpOpMvMVBz+NpEvMwAAAACIJqdPn45MxVQk9enTR3Xv3j2oxZQMmp4jRw49yLqtzp7+K9FtcSsAvRzHzRim4kS6zEzF4W8T+TIDAAAAgGgiE9lFpGIqd+7c+uehQ4f0rHw+8nuFChX8z4nbWuDy5ct6pj7f/5ef8n8C+X73PSeuVKlS6SUu6SIoi61ilJPoNjfzjnQct/+GJuJEusxMxeFvE/kyAwAAAIBocjP3PK7eHUn3O6k4mj9/flDLJRk7qlq1avp3+XnixAk9257PTz/9pPsfylhUvufITH2XLl3yP0dm8Lv99tsT7MYHAAAAAAAA77npiqkzZ87oGfJk8Q14Lo/37NmjYmJi1PPPP69ee+01NX36dLV+/XrVtm1bPdNe8+bN9fNLliypGjVqpJ566im1fPlytWTJEtWlSxc9MLo8Tzz66KN64POOHTuqDRs2qMmTJ6vhw4cHddUDAAAAAACAt910V74VK1aoOnXq+H/3VRa1a9dOjRs3Tr344ovq7NmzqlOnTrplVM2aNdXs2bOD+hd++eWXujKqXr16unlXy5Yt1fvvvx80k9/cuXPVs88+qypXrqyyZ8+u+vXrp18TAAAAAAAASbRiqnbt2spxEh/TRlpNDRo0SC+JkRn4Jk6ceM045cqVU4sXL77ZtwcAAAAAAACPYAReAAAAAAAARAQVUwAAAAAAAIgIKqYAAAAAAAAQEVRMAQAAAAAAICKomAIAAAAAAEBEUDEFAAAAAACAiKBiCgAAAAAAABFBxRQAAAAAAAAigoopAAAAAAAARAQVUwAAAAAAAIgIKqYAAAAAAAAQEVRMAQAAAAAAICKomAIAAAAAAEBEUDEFAAAAAACAiKBiCgAAAAAAABFBxRQAAAAAAAAigoopAAAAAAAARERsZMICAAAAAG5Usx7fJbh+eNbxif6f3sVzJrh+SquPKHgAUYMWUwAAAAAAAIgIKqYAAAAAAAAQEVRMAQAAAAAAICKomAIAAAAAAEBEUDEFAAAAAACAiKBiCgAAAAAAABFBxRQAAAAAAAAigoopAAAAAAAARAQVUwAAAAAAAIgIKqYAAAAAAAAQEVRMAQAAAAAAICKomAIAAAAAAEBEUDEFAAAAAACAiIiNTFgAAAAAAIDwadbjuwTXD886PtH/07t4zgTXT2n1kWvvC2FuMTVgwAAVExMTtJQoUcK//fz58+rZZ59V2bJlU+nTp1ctW7ZUhw4dCnqNPXv2qPvuu0+lTZtW5cyZU/Xs2VNdvnzZ7bcKAAAAAAAA21pMlS5dWv3444//FyT2/8J069ZNzZw5U02dOlVlypRJdenSRbVo0UItWbJEb79y5YqulMqdO7f69ddf1YEDB1Tbtm1VihQp1Ouvvx6OtwsAAAAAAABbKqakIkoqluI6efKk+uyzz9TEiRNV3bp19bqxY8eqkiVLqt9++01VrVpVzZ07V23cuFFXbOXKlUtVqFBBvfrqq6pXr166NVbKlCnD8ZYBAAAAAABgQ8XU1q1bVd68eVXq1KlVtWrV1JAhQ1TBggXVypUr1aVLl1T9+vX9z5VufrJt6dKlumJKfpYtW1ZXSvk0bNhQde7cWW3YsEFVrFgxwZgXLlzQi8+pU6f0z6tXr+rFVo6KSXSbm3lHOo7bf0MTcSJdZqbi8LeJfJkBAAD7xdzCtWAM1yJI4vjcRM7N3PO4XjF11113qXHjxqnbb79dd8MbOHCguvvuu9Uff/yhDh48qFs8Zc6cOej/SCWUbBPyM7BSyrfdty0xUvklseI6cuSIHtfKVqcz5Et02+HDh62J42YMU3EiXWam4vC3iXyZAQAA+xXIevPXgnmTZUpwPdciSCr43ETO6dOnI1cx1bhxY//jcuXK6YqqQoUKqSlTpqg0adKocOnTp4/q3r17UIupAgUKqBw5cqiMGTMqW509/Vei22TgeFviuBnDVJxIl5mpOPxtIl9mAADAfnuPJbw+g0r8WnB/rksJrudaBEkFn5vIkR50Ee3KF0haR912221q27Zt6t5771UXL15UJ06cCGo1JbPy+cakkp/Lly8Peg3frH0JjVvlkypVKr3ElSxZMr3YKkY5iW5zM+9Ix3H7b2giTqTLzFQc/jaRLzMAAGA/5xauBR2uRZDE8bmJnJu55wn73dGZM2fU9u3bVZ48eVTlypX17Hrz58/3b9+8ebPas2ePHotKyM/169cHNS+dN2+ebvVUqlSpcL9dAAAAAAAAGOJ6i6kXXnhBNWvWTHff279/v+rfv79Knjy5at26tcqUKZPq2LGj7nKXNWtWXdnUtWtXXRklA5+LBg0a6Aqoxx9/XL355pt6XKlXXnlFPfvsswm2iAIAAAAAAIA3uV4xtW/fPl0JdfToUT2+U82aNdVvv/2mH4v33ntPN+lq2bKlnkVPZtwbOXKk//9LJdaMGTP0LHxSYZUuXTrVrl07NWjQILffKgAAAAAAAGyqmJo0adJ1B8AaMWKEXhIjra1mzZrl9lsDAAAAAABAFGEEXgAAAAAAAERE2GflAwAAAADAVs16fJfg+uFZxyf6f3oXz5ng+imtPnLtfQFeQYspAAAAAAAARAQVUwAAAAAAAIgIuvIBAAB42I7BLRPdVvTl/4U9jpsxTMWhzLxXZqbieHF/BgCvo8UUAAAAAAAAIoKKKQAAAAAAAEQEXfkAAAAAAECiMwxea5bBxGYYFMwyiBtBiykAAAAAAABEBBVTAAAAAAAAiAi68gEAAAAA4NFudnSxg9dRMQUAAAAAsI6b4yUxVhIQPnTlAwAAAAAAQERQMQUAAAAAAICIoCsfAAAAAOCWxjESdH/DzaKbJQJRMQXgBk8QCa9/eHLnRP8PffEBAAAQzkoJrjkB76MrHwAAAAAAACKCFlMAkmBT9MT/T2ItwGj9BQAAAADuo2IKxm/kTXUXczNONFRK2FTJktT/NnR/BAAAAJLu2Gx0TQ1GxZRH2FQpYRv+NgAAAAAA3BrGmAIAAAAAAEBEUDEFAAAAAACAiKBiCgAAAAAAABFBxRQAAAAAAAAigoopAAAAAAAARAQVUwAAAAAAAIgIKqYAAAAAAAAQEVRMAQAAAAAAICKomAIAAAAAAEBEUDEFAAAAAACAiKBiCgAAAAAAABERG5mwAAAAuBnNenyX4PrhWRP/Pw9P7pzg+imtPrqpGNeKk1gMU3ESi2GqzG4lTqTL7FbiRLrMTMWJ5r8NANgqqltMjRgxQhUuXFilTp1a3XXXXWr58uWRfksAAAAAAACwvWJq8uTJqnv37qp///5q1apVqnz58qphw4bq8OHDkX5rAAAAAAAAsLkr37vvvqueeuop1aFDB/37qFGj1MyZM9WYMWNU7969I/32AAC4gS4c4xNc37t4zkT/D104ovNvw98FAAAgCVVMXbx4Ua1cuVL16dPHvy5ZsmSqfv36aunSpQn+nwsXLujF5+TJk/rniRMn1NWrV5XXXb5wLsH1p85fTvz/nLuU4Hopk0jGSSzGteIkFsNUnEiXmak4/G3M/G1uZX9u/coPif6fIVkmJbh+UNEcif6fMQ++fVNxEotxrTiJxYjmOLdSZqY+N9FaZrcSx9T+bOo8YOpvw3kgOs+dtxIn0tc1txIn0mVmKg5/G4417M/cq0XrscZrTp06pX86jnPd58Y4N/Isw/bv36/y5cunfv31V1WtWjX/+hdffFH9/PPPatmyZfH+z4ABA9TAgQMNv1MAAAAAAAAkZO/evSp//vzKcy2mboW0rpIxqXykldSxY8dUtmzZVExMjEoKpEayQIEC+g+fMWNGz8YgDmVm2z5gUy6m4tiUi21xbMrFtjg25WIqjk252BbHplxsi2NTLrbFsSkXU3FsyiXaSBuo06dPq7x58173uVFZMZU9e3aVPHlydejQoaD18nvu3LkT/D+pUqXSS6DMmTOrpEh29HDv7CZiEIcys20fsCkXU3FsysW2ODblYlscm3IxFcemXGyLY1MutsWxKRfb4tiUi6k4NuUSTTJlyuTdWflSpkypKleurObPnx/UAkp+D+zaBwAAAAAAAO+KyhZTQrrltWvXTt1xxx2qSpUqatiwYers2bP+WfoAAAAAAADgbVFbMdWqVSt15MgR1a9fP3Xw4EFVoUIFNXv2bJUrV65Iv7WoJV0Z+/fvH69Lo9diEIcys20fsCkXU3FsysW2ODblYlscm3IxFcemXGyLY1MutsWxKRfb4tiUi6k4NuXiZVE5Kx8AAAAAAADsF5VjTAEAAAAAAMB+VEwBAAAAAAAgIqiYAgAAAAAAQERQMQUAAAAAAICIoGIKAAAAAAAAEUHFFAAAQBRbtGiRunz5crz1sk62ec2ePXtUQpNCyzrZ5qU4NuVial+zKReTcQDAZlRMeUDy5MnV4cOH460/evSo3uaWunXrqhMnTsRbf+rUKb3NLUWLFtXvPS6JLdu8EsPGOCb2NZtyMRnniSeeUKdPn463/uzZs3qbG8aPH68uXLgQb/3Fixf1NrcMGjRInTt3Lt76f/75R2/zUhybcjG1n5nKx1QuJuLUqVNHHTt2LN76kydP6m1eO54VKVJEHTlyJN56yVG2eSmOTbmY2tdsysVkHBPX6bada2yKYyoXU/eDNuVjqsys5yDqxcTEOIcOHYq3/q+//nJSp04d9jiyLjY2NuxxDh486KRMmdIzMZJSHDf3NZtyMRknWbJkCcY5cuSIkzx58rDG+Pvvv/U2t9gUx6ZcTO1nkf7bmMrFzThynDl8+HC89Zs3b3YyZMjgSgzTx82E8tm1a5eTNm1aT8WxKRdT+5pNuZiOE+7r9KRyrvFiHFO5mLoftCkfU2Vmu9hIV4whce+//77+GRMTo0aPHq3Sp0/v33blyhXdPLhEiRIhF+G6dev8jzdu3KgOHjwYFGf27NkqX758IceZPn26//GcOXNUpkyZguLMnz9fFS5cOOpj2BjHxL5mUy4m48i3LdK9QRb5Zil16tRBcWbNmqVy5syp3CAxJJ+49u3bF/T3ClectWvXqqxZs3oqji25mNzPwp2PqVxMxGnRooX+KWXVvn17lSpVqqAYcv6uXr268srxrHv37v44ffv2VWnTpg2Ks2zZMlWhQgVPxLEpF1P7mk25mIxj6jrdpnONTXFM5WJqP7MpH5OfzaSAiqko9t577+mf8sEdNWpUUDP6lClT6pt4WR8quQiQk5AsCTU3TJMmjfrggw9CjtO8eXP9U+K0a9cuaFuKFCl0Pu+8807Ux7Axjol9zaZcTMbJnDmz//N52223xdsu6wcOHBhSjIoVK/pj1KtXT8XGxgadWHfu3KkaNWqkQpUlS5agXAIvfiXOmTNn1NNPP+2JODblYmo/M5WPqVxMxPFVCMtxJkOGDPp8HHicqVq1qnrqqaeUV45nq1ev9sdZv369fu3AOOXLl1cvvPCCJ+LYlIupfc2mXEzGMXGdbtu5xqY4pnIxdT9oUz6myiypiJFmU5F+E7g26Z8+bdo0fdIIh927d+uTqozvs3z5cpUjR46gE6vUWrs9tsTvv/+usmfP7tprRiKGjXHCva/ZlouJOD///LP+fMoJ73//+1/QN5by+SxUqJDKmzdvSDF8FwDys0ePHkGtJXw3pS1btgy6ibgVn3/+uc5Fxg4YNmxYUCssX5xq1aqFFMNUHJtyMbWfmcrHVC6m4vg+m3LDni5dOmXDcbNDhw5q+PDhKmPGjJ6PY1MupvY1m3IxEcfEdbpt5xqb4pjKxdT9oE35mL6Htl6k+xLi+v75559Et+3fv99IEV69etXIa509e9YzMWyMY2JfsykXk3Fk7I0rV6444TRu3Lhr5uOWhQsXOhcvXrQijk25mNrPTOVjKhcTcTZt2pTottmzZ3vueJbQeDw+69at81Qcm3Ixta/ZlIvJOCau020719gUx1Qupu4HbcsnkjFswax8HlCpUiW1Zs2aeOullrlcuXKuxZH+8TITQly7du1StWrVci1O/fr11V9//RVvvVtjC5iKYWMcE/uaTbmYjDN27NgE18usP61bt3YlRsGCBYP6+gf6+OOPlVuka6B030xoau0+ffp4Ko5NuZjaz0zlYyoXE3HkODNixIigdTKDZpcuXdQDDzzgSgyTx7OyZcuqmTNnxlv/9ttvqypVqngqjk25mNrXbMrFZBwT1+m2nWtsimMqF1P3gzblY6rMrBfpmjFcX+fOnZ1UqVI5b7zxhv79zJkzTrt27Zw0adI47777rmtFWKFCBado0aLOr7/+GtSCImPGjE7z5s1di9OkSRMna9aszqRJk/TvUlvev39/J0WKFM5zzz3nmRg2xjGxr9mUi8k4+fPnd6pVq+Zs377dv27BggVOgQIFnDvvvNOVGDIr4gsvvBD0banMjtK0aVMnc+bMjltklqJ///vfzrFjx/zr/vzzT6dSpUpOoUKFPBXHplxM7Wem8jGVi4k4kydP1sfNxo0b6xlMV69e7ZQsWdK5/fbbneXLlzteO54NHTpUx3n66aedc+fOOfv27XPq1q3r5MiRw5k2bZqn4tiUi6l9zaZcTMYxcZ1u27nGpjimcjF1P2hTPqbKzHZUTHnEjBkznNy5czs1a9Z0ihUr5pQvX95Zv369qzHkZlRuSuXmtE+fPs5DDz3kpE+f3vnkk08ct3344Yd6SuDWrVvrg1LevHmdOXPmeC6GjXFM7Gs25WIqjlwkymdSLhrlMymfVanMe+mll5xLly65EmPJkiX+979hwwadV65cuZxatWrpJtdu2bZtm1O1alUnX758zty5c/37w6OPPuqcOHHCU3FsysXUfmYqH1O5mIqzd+9ep379+k62bNmc1KlT65t6N7s/mz5urlq1yildurRTvHhx/039gQMHPBnHplxM7Ws25WIqjonrdNvONTbFMZWLqftBm/IxeQ9tMyqmPEJalTzzzDNOTEyM/tCGs896v379/HECa37d1rt3b38cuSH2agzb4pja12zKxeTnU054vjg//vij669/+vRpp02bNvrbbIkhLSfC0T9eyqxr165OsmTJdJyJEye6HsNUHJtyMbWfmczHRC4m4siNr1QSS+tFiTFw4MCwjM9h6nh26tQpp1WrVk5sbKxe5Ntlr8axKRdT+5pNuZiMY+I63bZzjW1xTOVi6n7QpnxMlZmtqJjyAPn2okqVKk7BggX1txcvv/yyrpHt2bOnqwMUymt1795d35BKbbWcYOVb05kzZzpu15C3aNHCyZQpk65JlpvgdOnSOSNGjPBUDBvjmNjXbMrFZBzx/vvv+7+5lC4CpUqVctasWeNqjJUrV+rXlpYS0n2nQ4cOujuP26ZPn667bdSoUUP/rFevnvPXX395Mo5NuZjaz0zlYyqXcMf56quv9A1vs2bN9MDRcqyRFg3Vq1cP6gbhlePZL7/84hQuXFh3D9q4caPz6aef6m/NH3744aAuRF6IY1MupvY1m3IxGcfUdbpt5xqb4piIYWo/sykfk2VmMyqmPECaAsq3SsePH4/X5Ub6tLqlXLlyukn10qVL9e/SSkJaS8iHTMadcIt025KT3Y4dO/zrZLwhacot4w95JYaNcUzsazblYjJOw4YNdReBqVOn6t9lXA7pKiBdBmS8DjcMGTJE34R26dJFz84l3XcS6jcfqk6dOunjyttvv62PM9J9Q7pxyD4gY3V4KY5NuZjaz0zlYyoXE3Hkwn3kyJGJdoPw2vFMjjO9evUKquwK7ELkpTg25WJqX7MpF5NxTFyn23ausSmOqVxM3Q/alI+pMrMdFVMeMH78+ESbQT/xxBOuxZHXSqhlhG8cALcMGjQowebNvv75XolhYxwT+5pNuZiMI2WT0DeWvvFg3CCvM2vWrET7zbtFjicJfSMmY1lI6zkvxbEpF1P7mal8TOViIo4MPnyzx6BoPp7JlPQJkXODnCO8FMemXEztazblYjKOiet02841NsUxlYup+0Gb8jFVZrajYsoDfv755wQHgZN1ss2ECxcuuPZau3fvTnTMGrcGWDYRw8Y4JvY1m3IxGedaZOa8cL9OYjcSt+L8+fO3dIEfjXFsysXUfhYN+biZi4k4Ml5NQgMpyzfMss1rx7PPP/88wX1ArjVkm5fi2JSLqX3NplxMxjFxnR7pY7MXj8/REMdULm7eDyaVfEyVmQ2omPIAGXzw0KFD8db//fffehtxKDMv7QPsz7emSJEi+u8Ql3S5kW1ukPGkpGVEXPItkGxzS506dYK6CvmcPHlSb/NSHJtyMbWfmcrHVC4m4nDcjN5y429Dmdm0D9h2rrEpjqlcTO3PNuVjqsxsl0wh6kkFYkxMTLz1R48eVenSpXM1TkIuXLigUqZMGfZ8zpw5o1KnTu2ZGEkpjpv7mk25mIyza9cudeXKlQQ/n/v27XMlxueff67++eefeOtl3fjx45VbFi5cqC5evBhv/fnz59XixYs9FcemXEztZ6byMZWLiTiJHWfWrl2rsmbN6kqMaDhuSnllypTJU3FsysXUvmZTLqbjhPs63bZzjU1xTOVi6n7QpnxMlZntYiP9BpC4Fi1a6J9ysmvfvr1KlSqVf5t8kNetW6eqV68echG+//77/jijR49W6dOnD4qzaNEiVaJEiZDjdO/e3R+nb9++Km3atEFxli1bpipUqBD1MWyMY2JfsykXk3GmT5/ufzxnzpygi3aJM3/+fFWkSJGQYpw6dUqfVGU5ffp0UAWhxJg1a5bKmTOnCpWUic/GjRvVwYMHg+LMnj1b5cuXzxNxbMrF1H5mKh9TuZiIkyVLFn2MkeW2224LuvmVGFKh//TTTyuvHM8qVqzoz6devXoqNjY2KM7OnTtVo0aNPBHHplxM7Ws25WIyjonrdNvONTbFMZWLqftBm/IxVWZJBRVTUcz3QZWbxQwZMqg0adL4t0nta9WqVdVTTz0Vcpz33nvPH2fUqFEqefLkQXEKFy6s14dq9erV/jjr168PqkGWx+XLl1cvvPBC1MewMY6Jfc2mXEzGad68uf+k165du6BtKVKk0J/Pd955J6QYmTNnDrq4jkvWDxw4UIVKKh59cerWrRtvu5ThBx984Ik4NuViaj8zlY+pXEzEGTZsmD7GPPHEE/ozGHgB7zs/V6tWTXnteLZmzRrVsGHDoIt4Xz4tW7b0RBybcjG1r9mUi8k4Jq7TbTvX2BTHVC6m7gdtysdUmSUZke5LiOsbMGBAgiP9u6127dp6ettwa9++ve6r7vUYNsYxsa/ZlIvJOIULFw7bYJAysPmCBQucmJgYZ9q0afp33/Lrr78mOGvKrZDB7Xfu3Knj/P777/p337J//37n8uXLnoljUy6m9jPT+YQ7F5Nx5LOY0KDkXj2ejRs3zvnnn3+siGNTLqb2NZtyMRknnNfptp1rbIxjKhdT94M25WOqzGwXI/9EunIMABB5u3fvVgULFkxwrAwAAAAACAcGP/ewl156STcfDrfvvvvO1YGPEzNy5Eg1aNAgz8ewMY6Jfc2mXEzGWbFihe7D7oZChQpFtFLqwIEDas+ePVbEsSkXt/ezSOdjKhcTcerXr6+KFi2qbDmeSbeOhLoReTGOTbmY2tdsysVkHBPX6bada2yKYyoXU/eDNuVjqsxswRhTHvbXX3+pvXv3hj1Or1691NatW1Xbtm3DGud///ufHviyX79+no5hYxwT+5pNuZiM8/jjj6stW7YkOLOJW0qWLBn2GEJuSGyJY1MupvYzU/mYysVEnAcffFD9/fffypbjmQyunCxZMivi2JSLqX3NplxMxjFxnW7bucamOKZyMXU/aFM+psrMFnTlAwAP279/v7p06ZJu7RQu3377rTp58mS8QSrd9vvvv6tz586pe+65x/NxbMrF1H5mKh9TuZiKAwDhZtu5xqY4tp1rbMsHN46KKQAAAMAF0vK3QIECKjbWjk4Jly9ftiYXIUPrMo4iAEQfxpjysEOHDrk6Js/y5cvV8OHDVZ8+ffQij2Wd2/bt26fOnDkTb73UjrvRp/jo0aNqwYIF6tixY/p3aUY9dOhQXVabNm1S4SRjCUiTzXBeUElun376qZoxY4YuM7f+JoHNzRcvXqzatGmj7r77bvXYY4+ppUuXhhxDpn6VwbVNkLKRLoFLlizRv//000+qSZMmqlGjRuqTTz5xLc4///yjxowZo8deady4sbrvvvtU165d1fz585XJmwY3x32QllGbN2/Wizy27ZgZ7jEyZMpwE1033PrsX2u/mjdvnvrss8/Ujz/+6FqTehNlI+T97tixQ129elX/fuHCBTVlyhQ1adIkvR+46ezZs/rcNXnyZDV16lS1cuVKfaw2Yfv27a6NySN/mzfffFN3P5Ip7mWRx2+99ZY6cuSIcnOsmi+++ELNmjVLXbx4MV5ZunVdI/tv//799fFfyN9IjtNSXmPHjlXhcvvtt4f1OkBaE0heco5+4YUX1J9//unK686ePVutX79eP5bPzauvvqq71qVKlUrlz59fvfHGGyHv182aNVMTJkzQ585wks+7lE2tWrX09Z947bXXVPr06VWGDBnUo48+qk6dOuVKrLVr1+puOnL9lyZNGpUuXTpVtmxZ1bdvX9di3OgxyK1jv+Q0Z84cvcjjcJ9vwinuuWvZsmW6rMKdU4cOHfRnNVzk/ctxJpzXaSdOnND3G7Ivjx492rVYco405fDhw/oc4Hvvcv6X85wcz3zHOzfI9YaMJSXHGzlnyvAkpj7/Von0tIC4dWvWrHGSJUsWchEeOnTIqVmzpp4itlChQk6VKlX0Io9lnWyT54RKppu988479XtOnjy58/jjjzunT5/2bz948GDI+SxbtszJlCmTft9ZsmRxVqxY4RQpUsT517/+5RQrVsxJkyaNs3LlypBzGT58eIKL5NWnTx//76Fq3Lixc+LECf346NGjzl133aVzy5Ejhy6rEiVKOIcPHw45jvy9v//+e/3422+/1a99//33O7169XIefPBBJ0WKFP7tt0ret5RP/fr1nUmTJjkXLlxwwmHUqFFObGysU7lyZSdjxozOhAkTnAwZMjhPPvmk85///EfvA8OGDQs5ztatW/VnJGfOnE6BAgV0fvfdd5/+G0meDz30kJHpo906Dnz66adOyZIl9WsFLrJu9OjRjltGjBjh1KtXT5fPjz/+GLRNpg2Wz2uoTp065bRp08YpWLCg07ZtW72vPfPMM/pvJDnVqlXLOXnyZEgx5P/HXeSzKp8VOQ751oVq8uTJQZ+VDz74QOcleWTLls0ZOHCg44YuXbr4P+N79+7VxxbZj3PlyqV/li1b1tm3b1/IceR9161b1/nyyy+d8+fPO+Gwdu1aJ0+ePDpWmTJlnD179uif6dKlc9KnT6/PDcuXLw85zpUrV5yePXs6adOm9X9eZB/znUunT5/ueOXzL+Uh5ZIvXz6nXbt2zosvvqgXeZw/f34na9asegp5N+JkzpxZH5vlWFy8eHHnjz/+cPU6QMhxX84DlSpV0n/zsWPH6rhyHnjiiSeclClTOlOnTg0phpwbE1rk/ct5zvd7qKScfOf5DRs26GscKTc5hsrnVPY/2edDdfvttzuLFi3Sj19//XV9fHn33XedH374QZ8z5VjwxhtvhBRDPhvyd5Ecnn76aX2NFg7dunVz8ubN6/To0UOfw+T4L8fNL774wpk4caIuv65du4YcZ/bs2frv07JlS+exxx7Tfws5lsq1k8SQ684DBw44XjgOyPHs5Zdf1p8T33HMt8i6V155RT8nVBcvXtTHTSkbuSf47LPPgra7dQyQe44aNWro85ec848dO6av0Xw53Xbbbfo5oZLPXkKLXAt88803/t9DMXToUOfcuXP68eXLl/V+LccwKSf5PHXo0EGXa6jkeOU7LspxOXv27Pp+Q65r5fOfO3duZ+PGjSHHkfKXv//gwYOdv/76ywmXBQsW6PO+xJP3Lp8TOZ/JPaEc71KlSuXMmTMnpBhnzpxx/v3vf/v3K/mbSCzZ7+Tc8+GHH7qWT1JAxVQUS+xg51vkhsWNg7ecUKtVq+b8+eef8bbJuurVq+sPXajkBlEObnJxO2/ePF1xcMcdd+iThe9kJB/qUMjFoFx4yo3pW2+9pQ9A8ruPHLybN28eci7yPuW1CxcuHLTIermwl8du3GDL6/kqBTt37uyUKlXK2bFjh//mUcpQLu5CJQdu3+vK3yjuxafcDFesWDHkXOTm4IEHHtAnbLnofe6555z169c7bpIy+uSTT/Tjn376yUmdOrWuDPGR9yAXqm5UGkpF19WrV/XvUmayTmzZskXvA/3793e8cEH65ptv6gvq3r176xO5XHjIIo+lolX2D/k8hUoqayXOs88+qy/i5cJKboDcviCVGwO5YXv//fed2rVr631OKiZ++eUX5+eff9b7yEsvvRRSjLgVeIEVE4E/QyWv4TsGjBkzRu/P/fr1c2bOnOm89tpr+m8jlYqhkotO32fx4Ycf1sdSqSj0VYo3bdrUlfOAlEujRo30314qQuRvtXr1asdNDRs21O9V8pFjjHze5SZeLtylslj2PckvVHLzKa8tFXpyTpObH7mB2LRpk9O3b19XLnoT+xLEt0jlkRv7mRz3O3Xq5D+eBZJ1sq1q1aohx5Fyl/Ow3ODKeVrOa3IuWLVqlavHgAoVKvi/HJIKcKk8kEoWn7ffflvftIa6L99zzz1O+/btgxZ5/3Kd4fvdzesAOZY1a9bM/6WHlOMjjzyiP5+hkv119+7d+rEcL6dMmRK0fcaMGbqyJdRcpHLtvffe05XdUlbly5fX1xi+a0E3yJdF8pkU27dv13HkSzefuXPn6spjN/azjz76KOh15dwj5HgjX8K4sQ+YuA6QyiKphJAv93bu3KkrQmSRxx9//LH+Ek6ON6GS6yI538g1hVSESSWlHF983LgXEPLlt9y/yBcErVq10o/vvvtu/QWL7Ofy+ZdrkVAFnvPjLm5dCwReB0i5yblTrgfksySVrfK3kXNPqOR15fwl5Hr20Ucf9X8xJvtzx44dnQYNGoQcR8rkqaee0u9bKtakwlAq8aTSzU3SsEL+xtIIQspN7s8C/+YvvPCC3i9CIfuu7EtyvSFfWMu1h3xOzp49qytd5ZpXvojDjaFiKoqZONgJqdH1XRQmRL7RkueESr69kpYEPvJtuVxgyYldbnzcuCCVg6qvNl8OovJ6gTGltZQcmEIlFRLyvuN+cyAHWDlRuCXwglRq97/77rug7XLB7UYFmFwY+L7RkRNF3G93tm3bpg+ubuUiP+UkKhdw8jeSb82kMkluVEIlNyC+i2shlWCBlV9ykRVqLkJeQyqgfOTkLbH+/vtv/btcBEvlVKikQvBai68MQyHfJEtFd2KkhZtc6IdKKoQCT9BLlizRF8JyE+/mTam8V6mUFPJtnOx7gS3+5AZLPk+hkOOIXExJnIULF+pFKvLkWzKp/PStC1Xg50ZaNkolYqCRI0eGXGkspMLLVzktle6Bx00hnyH59tStfKTSSyoHZJ+Qv7m0bJFc3GhlFngekBsr+ZsE5iPfBEtlSKikVZavhYmQGx45V/pagg0aNEh/6RNqecm5M+6XIL5FtrnxmZG/v++GJCGyTZ7jxt9m8+bNQeuGDBnib8Xm1jEg8MsWIcfmwPOa5BPqPvDVV1/pz4rcIJq6DpBjW+A+J+T6TfbFUMlrLF26VD+WioO414VyvpPzq1u5CPlcyo2dXIPIa7du3dqZP3++E47rgMCWeW5dB8hnQl4rsBJXYvla4sjfSs5xoZLPx7UWaYEY6udG/ubSAiwxsk2uD0MllZuB52O5mZd1UoEn5efWMSBwf5Z7DNn3Altpy35WtGjRkONIxapcC8gxZdeuXXqRfUKOA1I56lvn1udGzvdSURhIKqdKly7tuPG5ket9X/nFPQbIsVs+q6Hy5SMV7F9//bXTpEkTfwttqdSJe464VfK58OUjseRvEvhFmBzTQs1HrosCW35KBbscF6RiSkiLKblfxI1hjKkoljVrVt23VwbSjLtIX1YZR8cNMn7AtfrBnj59Wj8nVNK/N0uWLEFxp02bpgoXLqzq1Kmj+wGHSsarkD7+IkWKFCpt2rQqe/bs/u3yWMagCtWoUaP0GEYNGzZUH374oQon3yCdx48fV8WKFQvaVrx4cVf6sMssK1999ZV+XLFiRbVw4cKg7TKulYw14ZacOXOqF198UY/5JbFKlSqlunXrpvLkyRPya2fLls0/lpWUTdwxmGSbfLZClTlzZv3Z8JHZaiRWypQp9e/lypXTY6mEauPGjfq1HnjggQQXN2bIkc+ejImRGNnmxthAcuyqXr26/3d5LH3/ZdwvGdfOLZKPfDZE3rx59THhtttu828vU6aM2rt3b0gx1q1bp48xMg6LxJK/Q+3atfXntUqVKvp3t2Yv8h0D5LjfoEGDoG3y+7Zt20KOIeXjG1NQxmCJe06Qfd03XpMb5Fjco0cPtWHDBvXLL7+oChUq6GmV5RgQ6rTK8qWbb7DmuD9F8uTJXclFxkoMPC7Kez9//rw+VouWLVvq8VlCIbMSvffeewleB8gyc+ZM5YbcuXNfc0xJ2ZYrVy5XYkkZBerdu7d66aWX9L7866+/uhJDPpuB41fJ9YaMLxT4e6jjHD3yyCN6PEYZh03+1r6/u9vk8+87BiRLlkxlypQp3rnIjdgyntjgwYP1mDxybhk5cmTQmFIffPCB/py6SY6VH3/8sT5XSzw5Lt97770hv27BggX9Y2PKbHJSfoH7t4wz5MY1jbyGjMcYOOabHFvkOkTI2FwJjal6K2NmyViWcixIaJFjaajkGC/ny8TI8U3GsgrVX3/9pc/BPnL+lOtA+ew//vjjro1nKJ8J399YrvnkfiBwljeJ68Y1muxX8lpyDJCxbSWG3NcIKU/53Y3Z5XzHALmeDbyOEvK7nA9CJdeavjH55JwQd1xY+d13j+UGOS9Lucl5TF772WefVV9//bUqWbKkHh8uVHI97jvfyPlAPpuB5x85B8i5IhRy3Z8xY0b/73KekXW+z4qc19waBzBJuMEKLESANJd89dVXr9l0143mrtL3Xpo0T5s2Lejbanks6+RbWeluESppti0143FJLbY0e/eNmxIKaT0S+G2btIzw9csWv/32m/6G0y3yDbmMlyJdU2QcgXB8UyrfJEi/b/lWLO44T5KPfMMQKmldIN8eS3dL2efkW3/p7iL9v2WdNPGXViBuNUVOiOxvvi54oZBmutJ/XLo5SQsTGSNF9gsZJ0O+8ZP9UMYYCZW8rnTjkG/J5Jt5aSoe2HJFWsu40cpIumtKS5LEyLc/oX5upHm7/J0TGhNLmlbLNummFKqEvu0X8pmR/VjiuPFNqbQiCRxLTr6FD9z35Jtz+Ty5Qf42Ek/GLRHhOAaMHz9et5aUY9evv/4atF1ykW8FQyWfb3l9afUl8aSLmny7LC3OpFWYfG4Cu0WH4zggYzXIeGahNq2X7jPS5UCOzzIGl3wbL93HAs95ss+HSt6nHGcCW9DIWCyBrcxC3c+kq/21us+4dR0g3+rKcf6///2v3tfk3CKLPJZ18k16YJfoWyXlHtj1KZC0opX34MYxQIYJCOy6JeeXwG6K0pJBxphxg3Snk+61cnyTc4y0mHH7GCD7lexL8toyflYg6T7mRutcGSNPyk0+L9INSr71l2vDe++9V7fMlpYFsk+42WIqIW60lpCugvL+peuolJt065ZxX+SzJF3WJRdp0RgqOb7IcVP2aWk5J10gA8cVk2toaRXqxrHmWmNjutGVT6415b7D14U7kKyT61xpGRQq2Zfiji8p5Fwjn0nZ39w4Bsg9RWBLWel6LS2nAsvMjVbAPrNmzdL7ggxPIMcEN68F5HMj1+PSPVlaMsmQBIGkNagb1zRyzyTjCcr1gCxyXJFzsrRul/1bjnHS5TNU17sfkP1DuhGGSro+SzdnGcZBWmbK8U32YbnWkBZN0u1O9utQyP4a2D1QugwGtmCVVmdu7me2o2IqiskJLe4FSCBpLjhu3LiQ40i3AxmnyDeQnpzMZZHHsk7GgHBjkFq5IEisb7LcEMtg26GejAYMGKBvDhIj48q0aNHCcZNc7MqJyDfYnZsXpHHHrojb3UpOEDKeihukuatUrshA4b4uo3IRLBdE0vc7VDdyQeoGOeFI33W5QJQTkXSxkxOF7MvyHmTMITfeh7yGjLni61IrF/CBzZ5lAEm5GA6V3BTKODnX+rtJTqGQixrZf6VyUi6q5XggizyWdXKSdWMsMKkgev755xPcJhUsvkH9QyUXGjJORmLkgivUyo9A8pmX5vySXzgqpgKXwIoQIReNbnTlE++8847u3iKVEL7zgW+RLw8CJ6uI5uOAdAmT/Vbet+xTsm/JGEqyj0slouSX0I3RzZLXkIoUqQCXilv528sNsY8cd+SLi1DIvnStQcely3qo3UQCu+xKOUkevv1NHsu6a3X1vRkyHpp86ZEYGavPjUoWuX6Ke/MWt/ugDObspsWLF+ubbtnv3DwGyHVe4OLrnuQjFSwy2LcbZH+SShappJAvdKSiQL6AkWsnGdcyVHKuOn78uGOCdBuXL1V9XxpIpbtUjMqXPXKt6MZA3nLtKte2clyRY47cTAdW7EjFyLX2wxsllRLynhMjEzyEOpaVb5II+czLOUXOo7LIY1lXrlw5/ZxQyZcGiX05KF8mSMWoG9cBck9xrco8qYwP9fgcl3RDlLGZZD9z81pAri8Du3AHnmeE5OnGGIBCGhBIBVvcoWTkvlCu39wYB8rU/YB01ZMvqiWefNkm+5fsF/K3kUWuD0KdEEv+v1TmyfWFVIbKtVPgfajsZ/KlK26M/pot0q22EB2k64ZM4Xnw4EF/M87KlSsHNVEMhTRtlO5Oib2ebJcmvm40eU2MxJduHG50TYxLyk66pEg3lMAui+EkTUUln9SpU7v2mnJIkK5Q0uRVutuE2sw1WkjzXZleV7opuUmm65Vm9iVKlAjqLuQ10oxfpnD/7bffgo4BMmW8TK3txnFAur/J50SmUU7IH3/8oafYlanQQyHN6aXLi3RxScgPP/ygm6NL1zu3SDNx6ZIk3V6li3KRIkWUCdKlWz6j0q3Yre4P8+bN090C5Bgg3Tdq1Kih/vWvf7ny+p9//rnuAhWOY3DcY6M0n7/99tt103r5/H/55Ze66b50FZL1bpCuelOmTNHHAPkbuNENKdLkOOnrumvTOcAE6bYl3bmkK4qvWzfgFXLMnzNnToLXAdIlSc6roZIuW3JsTuycJd065RzUrl07FU7SBU+69wV2K3TL+++/r68FpPurdOcMN/l7yTlVhuJwg3SnlGu1wOsAuR906/r5559/1tcVpq6ZZQgXX/daMX/+fH0tIPt14PpbJV1Cv//+e30dWLduXT08CW4NFVMeIJUdNWvWVLYwkY+pMiMOZWbb5xNA9HnttddUmzZtjFV42pKPiTg25WIqjk25mIwjFexufhEZSaZysSmOTbnYFsemz2YkMfi5B0jtq5zsZHBQGQjZtEOHDqlBgwZ5Kh9TZWZzHBmUONwxvJ6LyXxkcM0BAwaoLVu2qEi0oAgcQN4ruZiIY1MugXGkJaDX85FWXibKzBcnnGU2depUXWYyyK0MEu3GZAQ3Q1oBybHOa/mYiGNTLqbi2JSLyTgycUz79u11iyI3J6O40VaoixYt8lwuNsWRGNKSzIZcTMcJd7lF8rNplRvs8ocIkv7qH3zwgR4TRfrJylgmMmW4G/39b4QbgyqazsdUmRGHMjO1D7z77rt64Eb5LMpPGVNABtz34jHAVC4m4tiUS2Ac2Ze9no9tfxsZK6tPnz56HCMZ/0/GAZJxdHzTUnvpGGAyHxNxbMrFVBybcjEVR8ZOkwGbZZw8GdNGxp+81hh00XwMMJWLTXFsysW2OJH8bNqEiimPkdm/ZPDb0qVL64G269SpE/JrysDH11pk0FO3L0jDmU8kYhCHMjO1D8iMRTIDlAzoKIM3yowgn3/+ueO1m1KTuZiIY1MutsWxKRcfmWVIZheUwVtlwopQyWxP11pkgOdwXQeEI59IxrEpF1NxbMrFRJxTp07pWdLkGCPXGnLMkRkCvXgdYCoXm+LYlIttcSLx2bQJFVMeJDMifP/9906FChVcOUn4ZhWLO/tT4PpwXpC6nU+kYhCHMjO5DwiZncmNODLrzrUWmZ3JK7lEQxybcrEtji25rF692unRo4eTL18+PVtSqOQ8LzOLBc78FLjItnCWmdv5RDKOTbmYimNTLibjCJn5zY1jTZYsWa65ZMyYMezHTbdySUpxbMrFtjimcrEJFVMeIt/AdO7c2f8NjEy5/MMPP4T8ujLF7WeffaannE5omTlzZlg+VOHKx3QM4lBmJvcB3xTU0kxYmgunTZvWadWqVUivJ1Pet2vXTk9HndDyn//8J2wnVrdziWQcm3KxLY4NufhaZJYqVUp/EytTnY8ePdo5ceJEyK8tlU/SOvpaN9puHwPCmY/pODblYiqOTbmYjCP++ecf/Xl94IEH9Plbpqnv1atXSK8pxyupTBs3blyCi7T6CMd1QDhysT2OTbnYFsdULraiYsoDevfurS8aU6ZM6dx3333OxIkTXe2z3qBBA+fVV1+9ZvNd+TbVK/mYikEcyszkPhC3m5B8bqWb0OnTp0N+7cqVKzsjR440dlMazlxMx7EpF9vi2JTLXXfdpT+D8u3rW2+95ezbt89xU8uWLXV3PVPXAeHOx2Qcm3IxFcemXEzGmT17ttO2bVvdeilr1qxOp06dnJ9//tmV15ZxMmV8PFNd+cKZi61xbMrFtjimcrEdFVMeICeLESNG6EGWw0EGbJswYUKi248dO6a/LfFKPqZiEIcyM7kPyE1hlSpV9IXjwYMHXX3t//73v7qVR2K2bdvm1K5d2xO5mI5jUy62xbEpl5deekl3CwgXee1rDdR68eJF3YLaK/mYjGNTLqbi2JSLyTgysPJDDz3kfPvtt/oz6abBgwfrFtKJ2bNnj9O+fXtP5GJrHJtysS2OqVxsR8WURWQGkP379zu2MJGPqTIjDmUW6j6wZcuWG3qetNg6c+aME81M5WIijk252BbHplxulHQj3r59u2MLU/mYiGNTLqbi2JSLG3FkYOUbMWTIEOf48eNONDOVi01xbMrFtjg2fTYjiYopi6RPnz6kE97ixYsdm/KJlhjEocxM7gM23Zh65WYhWmIQhzIL9Tgj3fplrJxoYdM52qZcTMWxKReTcUI538gYOdGEcydlZtM+YNM1ejgkU8D/r27duqpIkSLqpZdeUhs3bqRcAA+SLxxuVfHixdWAAQPUli1blNdzibY4NuViWxybcgnV1KlT9XGgevXqauTIkervv/+O9FsCYPBYkzNnTtW+fXs1b948dfXq1YiXvW3HZ85pSTuOF64DIomKKfjt379f9ejRQ/3888+qTJkyqkKFCuqtt95S+/bto5SAJODZZ59VM2fOVCVLllR33nmnGj58uDp48GCk3xYAQ9auXavWrVunateurd5++22VN29edd9996mJEyeqc+fO8XcALPf555+rs2fPqgceeEDly5dPPf/882rFihWRflsAkgAqpuCXPXt21aVLF7VkyRK1fft29dBDD+kTVOHChXVrKgB269atm/r999/Vpk2bVJMmTdSIESNUgQIFVIMGDdT48eMj/fYAGFC6dGn1+uuvqx07dqgFCxboawC5Oc2dOzflD1juwQcf1C0nDx06pI8D0oOiatWq6rbbblODBg2K9NsDYDEqppAg6dLXu3dv9cYbb6iyZcvqVlQAkga5AB04cKDu0rd48WJ15MgR1aFDh0i/LQDXERMT42oZpUuXTqVJk0alTJlSXbp0yfP5RDKOTbmYimNTLibjuCFDhgz6vD937lzdilKOBXJdAADhQsUU4pEWU88884zKkyePevTRR3W3PuneAyDpWL58uW4lId+eSgWVtKAEEN3cGL9i586davDgwbrl1B133KFWr16tb0gj0a2XsUUoM/azyDh//ryaMmWKat68uapUqZI6duyY6tmzZ4TeDYCkgIopi8ig5VmzZr3l/9+nTx/dUkq67e3Zs8c/vsyECRNUo0aNlNfyiZYYxKHMTO4DhQoVUilSpLil/ysVUP3799ctpmrUqKG79A0dOlQ36Z80aZIyLZRcoi2OTbnYFscLuUiXuhvxww8/6HFhbpV02ZHBz7/++mvdWmL37t1q/vz5qmPHjipTpkzKLabyMRHHplxMxbEpF5NxbtTdd9+tWzreijlz5qh27dqpXLlyqc6dO+uf0mpKjgXSi8K0UHJJqnFsysW2OKZy8aoYmZov0m8C8U2fPl01btxYX8TK42u5//77XSlCuRFt06aNevjhh/V4U17Lx1SZEYcyi8Tn0+fixYvq8OHD8WbLKViwYMivnSxZMj3oubSUfOSRR/QFaTiFMxfTcWzKxbY4NuSSKlUqlT9/fl1ZJDeNMvZbOLz88sv6OqBUqVIqnEzlYyKOTbmYimNTLibjrFq1Sl93yBAb4rvvvlNjx47Vn1eZUVe63IYqbdq0qmnTpvo4IGNNhqvS3kQutsWxKRfb4pjKxXpSMYXoExMT4xw6dMj/OLElWbJkxt9bkyZNnP3790ddPqbKjDiUWSQ+n1u2bHFq1qypXzNwcTOOxLgREydOdM6cORPVuZiKY1MutsWxKZcjR4447777rlO+fHknNjbWadCggTN58mTnwoULTiRkyJDB2b59e9TnYyKOTbmYimNTLibj3HHHHc7XX3+tH8vnL3Xq1E7r1q2d4sWLO88995wrMU6dOnVDzxsyZIhz/PjxqM7Ftjg25WJbHFO52I6KKdy09OnTh3RBCuDmVa9e3alVq5Yza9YsZ/Xq1c6aNWuCFi/dlJrKxUQcm3KxLY5NuQRauXKl06VLFydbtmx66dq1q/FjgJvXAabyMRHHplxMxbEpl3DHyZgxo7Nt2zb9+I033tAVYOKXX35x8ufP73jpOsBULjbFsSkX2+JE02fTy6iY8oDPP//cOX/+fLz18k2MbDMt1AtSE/mYKjPiUGam9oG0adM6mzZtcqJBqMcAU7mYiGNTLrbFsSmXuP766y+nf//+TqpUqZx06dI5yZMn1622/vjjD09+QWUqHxNxbMrFVBybcglnHKkM8rVsrl+/vjNs2DD9ePfu3bqFhkmhHgNM5WJTHJtysS1ONH02vYyKKQ+QrgC+bkOB/v7774h05Qv1ZGQiH1NlRhzKzNQ+IM2EFy9e7ESDUI8BpnIxEcemXGyLY1Mu4uLFi87UqVOdxo0b6+5CVatWdT799FPdrXbnzp1OmzZtnJIlSzpeqZgylY+JODblYiqOTbmYilOnTh2nbdu2zvjx450UKVI4W7du1esXLlzoFCpUyDEp1GOAqVxsimNTLrbFiabPppdRMeUBMk7F4cOH462XpsFZsmQx/n5CPRmZyMdUmRGHMgvnPnDy5En/Mn/+fKdatWrOggULdKVX4DZZov0YYCoXE3FsysW2ODblEsjXNShr1qx6vIr169fHe86BAwf08cgL1wGm8jERx6ZcTMWxKReTcdauXeuUKVNGdxsaMGBAUHwZz8akUI8BpnKxKY5NudgWJ5o+m17GrHxRrGLFiiomJkatXbtWlS5dWsXGxvq3XblyRe3cuVM1atRITZkyxej7ypAhg35PRYsWjbp8TJUZcSgzE/uAzJInMXzky4TA3wPXScxoPgaYysVEHJtysS2OTbkEqlevnnryySdVixYt9AxgCbl8+bJasmSJuueee1S4ZcyYUa1Zs+amrwNM52Mijk25mIpjUy4m4yTm/PnzKnny5GGbQc/Ne4FoycWmODblYlucSHw2vez/7qQQdZo3b65/ysVfw4YNVfr06f3bZNrJwoULq5YtWyqvMJGPqTIjDmVmYh9YsGCBsoWpXEzEsSkX2+LYlEug/v37q+rVqwdVgPtudn/99VdVq1Ytvc1EpZSv0s0L+ZiIY1MupuLYlIvJOIlJnTq1soWpXGyKY1MutsWx6bNpAi2mPODzzz9XjzzySKLfwpg2ZMgQ1blzZ5U5c+aozcdUmRGHMjO1D+zZs0cVKFAgwVYZe/fuVQULFlSmlClTRv3www/6/URzLibi2JSLbXFsykW+cT1w4IDKmTNn0PqjR4/qdW61mJQKtzp16lz3eb/88ou68847b/m4ZyofE3FsysVUHJtyCXecLFmyxDu2JObYsWPKlCZNmqjPPvtM5cmTJ+pysSmOTbnYFidaP5teRospDxg4cKBq2rRpvAvAEydOqEqVKqkdO3bc8mtPnz5dNW7cWDcxlMfXcv/99+ufffr0UdGaj8kYxKHMTO4DRYoUSfDCV052ss3NrnwXL15Uhw8fVlevXg1a77vB/uOPPzyRi4k4NuViWxybckmoq6DvxjddunTKLdL9OH/+/KpDhw6qXbt2iVY+16xZ0xP5mIhjUy6m4tiUS7jjDBs2LOj1XnvtNd1Ku1q1anrd0qVL1Zw5c1Tfvn2VG1atWqXvCcqWLat//+6779TYsWNVqVKl1IABA3SLcDFr1qyozcWmODblYlsc05/NpIAWUx4gY1kcPHgw3kXvoUOH9I3ihQsXXHlteZwYN8ewCWc+JmMQhzIzvQ/Ia+bIkSNo/e7du/UF49mzZ0OOsXXrVvXEE0/orgfhHMfKRC6m4tiUi21xbMhFxqzx3RxKpVFgBbh8HtetW6duv/12NXv2bOWGv//+W02YMEG3BN2wYYOqW7eu6tixo+667LshDYWpfEzEsSkXU3FsysVkHB8ZHkBaNHbp0iVo/Ycffqh+/PFH9e2334YcQ1pC9u7dW8eSL9ZkDM0HH3xQ/f777+q+++4LuhmP9lxsi2NTLrbFMZWL7WgxFcUCWzBJjWumTJmCTnjz58/X49iEIrBFRNzWEV7Mx0QM4lBmJveB7t27659SMSTfuqRNmzYozrJly1SFChWUG9q3b6/HwZgxY4Zunn+jTZSjLRcTcWzKxbY4NuXiO65I5bAMNpwmTRr/Nqkoqlq1qnrqqaeUW7Jnz666deumF2k5IS0lnnnmGb08+uijupKqfPnyUZ+PiTg25WIqjk25mIwTeK0xdOjQeOulUkwqk9ywZcsW/3Fr6tSpenysiRMn6oHbZdgCtyqmTORiWxybcrEtjqlcbEfFVBTzDa4sF73SpD6QNLOVm9533nnHtXjjx49XrVq1itclSbr1TJo0SbVt2zbq8zFVZsShzEztA6tXr/Zf+K5fvz6o1YI8lpvEF154QblBBnJfuXKlKlGihAoHU7mYiGNTLrbFsSkXqRgScjyR13Kz+9H1SFfk3Llzq2zZsqk33nhDjRkzRo0cOVJ3Uxg1apRuSRGt+ZiIY1MupuLYlIvJOD7yWZTWWT169AhaL+tkmxvkeOb7olpaeshQBUK69UqLSi/lYlscm3KxLY6pXKznIOoVLlzYOXLkSNjjJEuWzDl06FC89X///bfe5qV8TJUZcSgzU/tA+/btnZMnT4Y1xh133OEsXrzYsSEXU3FsysW2ODbl4iPn6EWLFuklofO1Gy5evOhMnTrVady4sRMbG+tUrVrV+fTTT50zZ844O3fudNq0aeOULFnSM/mYimNTLqbi2JSLiThjx451kidP7jRt2tR59dVX9SKP5XMq29xQp04dp23bts748eOdFClSOFu3btXrFy5c6BQqVMjxUi62xbEpF9vimMrFdlRMecw///wTtteOiYlxDh8+HG/9mjVrnCxZsnguH5MxiEOZmdwH3CQ31L5l/vz5TrVq1ZwFCxboCunAbaZuvAHEd+rUKeexxx7TF7lyrpZFHksl0YkTJ1wrsi5dujjZsmVzsmbN6jz33HPO+vXr4z3nwIEDOr4X8jERx6ZcTMWxKReTccRvv/3mPProo07FihX1Io9lnVvWrl3rlClTxsmYMaMzYMCAoGND69atHS/lYmMcm3KxLY6pXGzG4OceIE1qBw8erJvOywCr0v+7aNGiekwLaT4sYz6EomLFiro70tq1a3XTfBljJnCcjJ07d+o+slOmTPFEPqZiEIcyM7kPiBUrVujPoUxPL11sA02bNu2WB24OHEsqodmF3B78PFy5RCqOTbnYFseWXKSbvXQd/OCDD4Jm/Hnuuef0eDDS3d4N9erVU08++aQe1Dlut36fy5cv6/Fm7rnnnqjPx0Qcm3IxFcemXEzGiaTz58+r5MmT66EKACAsIl0zhusbOHCgU7RoUeeLL75w0qRJ42zfvl2vnzRpkm5iHyr5RkQW+YbnhRde8P8uy+uvv+5MnDjRuXDhgmfyMRWDOJSZyX3gq6++0s3qpWlwypQp9c/bbrvNyZQpk+5KdKukef6NLtGeSyTi2JSLbXFsyiVt2rQJdrOVLkOyzS0///yzc+nSpXjrZZ1sc4upfEzEsSkXU3FsysVkHHHlyhVn8+bNOp58JgMXrzGVi01xbMrFtjg2fTYjhYopDyhWrJjz448/6sfp06f33/hu2rTJyZw5s2txxo0b55w/f96xIR9TZUYcyszUPlC2bFnnww8/DIpz9epV56mnnnL69evnSozdu3fr14xL1sk2L+ViKo5NudgWx6ZcChQo4Kxbty7Bbjf58uVzvDbWpKl8TMSxKRdTcWzKxWScpUuXOkWKFNGfRV+XQd8SyudTrlVkyI4bWaI9F5vj2JSLbXFM5WI7KqY8IHXq1M6uXbvi3fhu2LDBSZcunWtx5AMlF59xHT9+XG/zUj6myow4lJmpfUC+dZWBh4WM/+K7CN64caOTO3duT92UmsjFVBybcrEtjk25fPzxx079+vX1+E4+8rhBgwbOqFGjnHCPNSnfAmfIkMG1OKbyMRHHplxMxbEpF5Nxypcv7zz00EP62CLX5jJ+VeASyhfTvuWdd97RFVCPPPKIM3z4cL3IY1n37rvvRn0uNsexKRfb4pjKxXb/N5gQolapUqXU4sWLVaFChYLWf/3113p8KLfs2rUrwTFkLly4oP766y9P5WOqzIhDmZnaB7JkyaJOnz6tH+fLl0/98ccfqmzZsurEiRPq3LlzrsRIaHwpcebMGZU6dWrlpVxMxbEpF9vi2JTLRx99pLZt26YKFiyoFyHjWck4UEeOHFEff/yx/7mrVq266deXMaWEfP7bt28fNL6UXBesW7dOVa9eXbkl3PmYjGNTLqbi2JSLyThbt27V1xbFixdXbmrXrp3/ccuWLdWgQYNUly5d/Ov++9//qg8//FD9+OOPqlu3bq7EDFcuNsexKRfb4pjKxXZUTHlAv3799ElDKodkoGUZSHXz5s1q/PjxasaMGSG//vTp0/2P58yZozJlyhR0QTp//nw9iLNX8jEVgziUmcl9oFatWmrevHn6hvehhx7Sg6r+9NNPep0MWByK7t27+29KZdD2tGnTBh0Dli1bpgdw9UIupuPYlIttcWzKpXnz5iqcfOd9qZzOkCGDSpMmjX9bypQpVdWqVdVTTz3lWrxw52Myjk25mIpjUy4m49x11126AiycN79yHzB06NB462USpN69e3sqF9vi2JSLbXFM5WK9SDfZwo2RARSlmXCOHDn0AMs1atRw5syZ40rxBfaBjdsvVgZylUFcv//+e8/kYzIGcSgzU/vA0aNHnb/++ss/wOKQIUOcZs2aOd27d3eOHTsW0mvXrl1bL/KZr169uv93WaQrQqdOnZwtW7Z4IhfTcWzKxbY4NuViikx6cubMmUi/DQAJmDZtmlOqVCln7NixzooVK/QYVoGLGwoWLOi8/fbb8dbLOtnmpVxsi2NTLrbFMZWL7WLkn0hXjiE6FClSRP3+++8qe/bskX4rACKgQ4cOavjw4SpjxoyUPxClpGuttM4M5PZn9vDhw7rlp7j99ttVzpw5lZfzMRXHplxMxbEpl3DHSZYsWbx10tLZ1w0/oeE4bta4cePUk08+qRo3bqxbgQhpNT179mz16aef6q6+XsnFtjg25WJbHFO52I6ufB5y8eJFfbEY94Tn688eqp07d/ofnz9/3tUxZSKRj6kYxKHMTO0DcmL75ptv1KZNm/zjWz3wwAMqNtadQ/nYsWOVKeHOxWQcm3KxLY4tucj5WcZ8WbhwoT4/+7h90StjZT3zzDNq0qRJ/tdMnjy5atWqlRoxYkRQV38v5GMijk25mIpjUy6m44SbVDyVLFlSvf/++3poAiG///LLL/6KKq/kYlscm3KxLY6pXGxHiykPkAHVnnjiCfXrr78GrXf7hCc31IMHD1ajRo1Shw4dUlu2bFFFixbVY87IGFMdO3b0TD6myow4lJmpfWDDhg3q/vvvVwcPHtQtGIR8RnPkyKG+//57VaZMGVfirFixQk2ZMkUP3CqVbYF8F6leycVEHJtysS2OTbnUqFFDH1Nk/KpcuXLFm6TgnnvuUW6QCqjVq1erDz74QFWrVk2vW7p0qY4r48xJhZUbTOVjIo5NuZiKY1MuJuMAgM2omPIAOeHJt64y6GCePHninfDKly/vShyZhePzzz/XP2WQU5lZSCqmJk+erIYNG6YvTr2Sj6kyIw5lZmofkJtEudGVz6jMAiaOHz+uv92UWX/iVozdCrnpbNu2rWrYsKGaO3euatCggb7BlorqBx980LUWVSZyMRXHplxsi2NTLunTp1crV670V3yFS7p06fTgxzVr1gxaLzOPyuDHZ8+edSWOqXxMxLEpF1NxbMrFZBwxYcIE/QWytNCQ63KZEViu0WU4Dmml6dYX1TKQc0KtwGWyBy/lYlscm3KxLY6pXKwW6UGucH1p06Z1Nm3aFPaiKlasmPPjjz/qx+nTp3e2b9+uH0vszJkzeyofU2VGHMrM1D6QOnVq548//oi3fv369XqbG8qWLet8+OGHQceAq1evOk899ZTTr18/x0u5mIpjUy62xbEpF5mIYN68eU64FShQwFm3bl289TJ4a758+VyLYyofE3FsysVUHJtyMRln5MiRTvbs2Z3XXntNT7Tiu06XAZflPbhh6dKlTpEiRRKcEEnWeSkX2+LYlIttcUzlYjsqpjzgjjvucBYvXhz2OHIBvWvXrngVUxs2bHDSpUvnqXxMlRlxKDNT+0C5cuWc+fPnx1sv68qUKeNaJdvOnTv146xZs/pvUDdu3Ojkzp3b8VIupuLYlIttcWzKZdu2bXrmz3HjxoV1xp+PP/5Yxzlw4IB/nTyW2TlHjRrlWhxT+ZiIY1MupuLYlIvJOCVLlnS++eabeNfpUgmeLVs2V2KUL1/eeeihh/R5//jx486JEyeCFi/lYlscm3KxLY6pXGxHxVSUOnnypH+Ri9tq1ao5CxYscP7++++gbbK4pVKlSs6ECRPifagGDhzo1KxZM+rzMVVmxKHMIvH5nDlzplO6dGln6tSpzt69e/Uij6WVk2xzI6a0iPBVRsnrTpw4UT/+9ddfnYwZM3oqF1NxbMrFtjg25eJrxRC39YLbrRgqVKigz/8pUqTQrahlkceyrmLFikFLKEzlYyKOTbmYimNTLibjJPYF8pYtW1xrnSlfUG3dutUJNxO52BbHplxsi2MqF9sxK1+Uypw5c9BYNVKJWK9evbAOrtyvXz/Vrl079ddff+k+5TLQsUwXPX78eDVjxoyoz8dUmRGHMovE57Np06b658MPP+yPLTFEs2bNXIkpY0fMmzdPlS1bVj300EN6INeffvpJr4ubX7TnYiqOTbnYFsemXGSChYoVK6qvvvoqwcGV3dK8eXNlgql8TMSxKRdTcWzKxWQcGatmzZo1euyaQLNnz9Yz57lBZt6T8aWKFy+uwslELrbFsSkX2+KYysV2VExFqQULFhiPKQOzyQxCMvi5DIAqFVWVKlXS6+69996oz8dUmRGHMovE59NEzA8//NA/1fXLL7+sUqRIoQdubtmypXrllVdci2PTZ8imXGyLY1Muu3fvVtOnTw/7zWL//v2VCabyMRHHplxMxbEpF5Nxunfvrp599ll9npbK7uXLl+vKsCFDhqjRo0e7EqNr166qR48eepZR+ZJKrgMClStXzjO52BbHplxsi2MqF+tFuskWAMA9nTt3do4cOWJFkZrKxUQcm3KxLY4XcmnatKnz9ddfOyadPn06bF2TTeVjIo5NuZiKY1MuJuOIL774wilevLi/y6B0wR89erRrrx93wPNwdUs0kYuNcWzKxbY4pnKxWYz8E+nKMVzbunXrElwvTYVTp06tChYsqFKlSuVaMV68eDHBKWIljlfyMVVmxKHMTH8+rydjxoy6OXHRokVv6f9LV6NvvvlGbdq0Sf9eqlQp3ZoyNtZ8A9tQc4mmODblYlscL+TyySefqNdee013GUqoFcP999/vynuUaa67dOmiFi5c6G89GY6uyabyMRHHplxMxbEpF5NxAp07d06dOXNG5cyZ0/XWX9cSt6tSNOdicxybcrEtjqlcbETFlAckS5bsmv3V5QTYqlUr9fHHH+sb4Vu1detWfVKVrjuB3L4gNZGPqTIjDmVmah+4URkyZFBr1669pZvfDRs26AtoacJ/++2363VbtmxROXLk0F16y5Qpo0wKJZdoi2NTLrbF8UIucpxJjJvn5xo1auhzvowvl9BYOffcc48rcUzlYyKOTbmYimNTLibjAIDNGGPKA6T1Qq9evVTPnj1VlSpV9Drpu/rOO+/o8SAuX76sevfurceAefvtt285Tvv27XWrCBnoPE+ePGEbvNFEPqbKjDiUmal9wIQnn3xSlS5dWq1YsUJlyZJFrzt+/Lg+NnTq1ClepTUAM+K2YA4XqThbuXKlv2La6/mYiGNTLqbi2JRLuOPIoOo3ej2+atUqV2JOmDBBjRo1SregXLp0qW4lNWzYMD3As7SgjvZcbIpjUy62xYnEZ9N2VEx5wODBg9Xw4cNVw4YN/eukqXD+/PlV37599U2wDFYugxWGcuMrXQzkgrREiRLK6/mYKjPiUGam9gET5BgQWCkl5LHkeOedd0b0vQH4f6SLXbhaX8rnfO/evWGvmDKVj+k4NuViKo5NuYQjTuBMmfLaI0eO1F3sq1Wrptf99ttvurXzM88840q8jz76SE9+9Pzzz+tzv6+1l8xGLJVToVRMmcrFpjg25WJbHNOfzSQh0oNc4fpSp07tbNq0Kd56WSfbxM6dO500adKEVJx33HGHs3jxYivyMVVmxKHMTO0DNyp9+vTO9u3bb+n/litXzpk/f3689bKuTJkyjmmh5BJtcWzKxbY4Xsjl8uXLzqBBg5y8efM6yZMn97/OK6+84urgqtu2bXPq16/vjBs3zlmxYoWzdu3aoMUtpvIxEcemXEzFsSkXk3E6duyoXzOufv36OR06dHAlRsmSJZ1vvvkm3jFr/fr1TrZs2Rwv5WJbHJtysS2OqVxsR8WUB1SoUMFp166dc+HCBf+6ixcv6nWyTfzyyy9O4cKFb/q1A2fbkZvPatWqOQsWLHD+/vvvsM3GE858TMYgDmVmch8wcfM7c+ZMp3Tp0s7UqVOdvXv36kUely1bVm8Lx/HA6xUG0RSDOPaW2cCBA52iRYvqWX+kktv3OpMmTXKqVq3q2ntcunSpU6RIkbDPyGUqHxNxbMrFVBybcjEZJ2PGjM6WLVvirZd1ss0N8oXarl274h2zJIbvyzav5GJbHJtysS2OqVxsR8WUByxZskR/S5EjRw6nXr16esmZM6deJxeRYvz48c6bb75506/tu9j0LXF/D8cFaTjzMRmDOJSZyX3gRj399NO3PCV93JtR3+c/7u9uTxkdjlyiLY5NudgWxwu5FCtWzPnxxx/j3SxKy8zMmTO79h6ltUSLFi2c3377Tbf0lBvUwMUtpvIxEcemXEzFsSkXk3Fy5crljB07Nt56WSfXHW4dA7799tt4ubz//vtOxYoVHS/lYlscm3KxLY6pXGzHGFMeUL16dT0A4ZdffqlnyBIPPfSQevTRR/UsP+Lxxx+/pddesGCBsikfkzGIQ5mZ3AdkrCoZhFRmzBO5c+fW/dh9A64Hjg9xq0wdD0zkYiqOTbnYFsemXP766y9VvHjxBAddvnTpknJzqvjp06cnGMtNpvIxEcemXEzFsSkXk3Fk3KfOnTvrgZR9x5dly5apMWPG6DEt3dC9e3f17LPP6jFzpAGDHN+++uorNWTIEDV69GjlpVxsi2NTLrbFMZWL9SJdMwYASNyhQ4ecmjVr6pZKhQoVcqpUqaIXeSzrZJs8x6TOnTvfUssPU7mYiGNTLrbFsSkXn0qVKjkTJkyI14pBuhBJHLc0bdrU+frrr51wM5WPiTg25WIqjk25mIwjJk+e7FSvXt3JkiWLXuSxrHOTdEksXry4v8V0vnz5XB0ry2QutsWxKRfb4pjKxWa0mPKQjRs3qj179qiLFy8Grb///vtdef1169YluF6mwpQZRgoWLKhSpUqlvJKPqRjEoczCuQ/IbB4yK86mTZvizZS1efNm9cQTT+hvN6dOnapM+eKLL9QLL7ygsmfPHpW5mIhjUy62xbEpFx+ZJatdu3a6ZYa0wpg2bZqOMX78eDVjxgzllmbNmqlu3bqp9evX69lFU6RIEZZzp6l8TMSxKRdTcWzKxWQc8fDDD+slnNq0aaOXc+fOqTNnzqicOXOGJY6JXGyLY1MutsUxlYvVIl0zhuuTb15ktqzAQUgDx3xxS0LjSwUuqVKlctq2bev8888/UZ+PqTIjDmUW7n1Avn1dtWpVottl5ix5jkm3OoizqVxMxLEpF9vi2JRLoEWLFukZ82Q8OxlguUaNGs6cOXMcNwWOMxd3cXtsORP5mIpjUy6m4tiUi8k4AGArKqY8QJrWP/DAA7rrjFzkbty40Vm8eLHuMiAnQrfIYIe33367bq67bt06vchjGQhRZhaRpr358+d3evToEfX5mCoz4lBm4d4HZBD1hQsXJrpdZtF0cwrncFZMmcrFRBybcrEtjk253KyJEyc6Z86ccWxhKh8TcWzKxVQcm3JxI87ly5edt956y7nzzjv1YMu+LkO+5VbJDMIysPmNLG4JVy42x7EpF9vimMrFdlRMeYBc2K5du1Y/likn//zzT/14/vz5/uno3SAfptmzZ8dbL+tkm/jmm2/0lLjRno+pMiMOZRbufeCZZ57RY9ZMmzbNOXnypH+9PJZ1hQsXdrp06eJ4oWLKVC4m4tiUi21xbMrlZmXIkOGWPpsJCbV1dLTlE+k4NuViKo5NubgRp2/fvk6ePHmct99+20mdOrXz6quvOh07dtTXIcOHD7/l1x0wYIB/6d27t76WqVq1qtOtWze9VKtWTa+TbW4JVy42x7EpF9vimMrFdlRMeYBMNbtjxw79WCqFfvrpJ/1427ZturmwW+SDJFPbxiXrZJuQ6aNDjWkiH1NlRhzKLNz7wPnz5/U08ylTptRdaeSzKIs8lnUyELk8xwsVU6ZyMRHHplxsi2NTLqY+m4Hf+g4aNMjJmzevkzx5cv9rvfLKK2EZ/Djc+URTHJtyMRXHplzciCPXGDNmzPC/llxnCLnxbd26tSvvUW6m5fMeV79+/ZwOHTo4bjGRi21xbMrFtjimcrEdg597QJkyZdTatWtVkSJF1F133aXefPNNlTJlSvXJJ5+ookWLuhanRIkS6o033tCvK68vZJpbWSfbhAzsmCtXrqjPx1SZEYcyC/c+IBMOyDTzQ4cOVStXrgyakr5y5coqY8aMyitM5WIijk252BbHplxMGzx4sPr888/1ceypp54KOs4NGzZMdezYMaLvD0jK5BgjkxKI9OnTq5MnT+rHTZs2dW1KepmsYcWKFfHWP/bYY+qOO+5QY8aM8UwutsWxKRfb4pjKxXZUTHnAK6+8os6ePasfDxo0SO/kd999t8qWLZuaPHmya3FGjBihZ9zJnz+/KleunF4nM/PIrEO+WUV27NihZyKK9nxMlRlxKDNT+4Dc5NapU0dFA7lADeWm21QuJuLYlIttcWzKxRSZRUwq1evVq6eefvpp//ry5curP//8M6LvDUjq5Pr8wIEDepbsYsWKqblz56pKlSqp33//3bVZs9OkSaOWLFmi/vWvfwWtl3UyQ7eXcrEtjk252BbHVC7Wi3STLdyao0ePOlevXnW9+E6dOuV89NFH/n7lo0aN0uu8mo/pGMShzEzuA+LgwYPOwIEDXXmtZcuWOcOGDdPjSMgij2WdKW7mEuk4NuViWxybcnG7q5B0Rdy1a1e819qwYYOTLl06xzSvdLGKlhi2xbEpFzfi9OrVyxk8eLB+LJMSxcbGOsWLF9ddh2WbG4YMGaKPA127dnUmTJigFxkrL23atHqbW0zkYlscm3KxLY6pXGwXI/9EunIMN27v3r36Z4ECBawoNhP5mCoz4lBmkfh8SjdC+VZGWjbeqsOHD6uWLVvqb0Tl2x5fd91Dhw6pPXv2qBo1aqj//e9/KmfOnCrac4mWODblYlscm3KJK0OGDDrurXYjli6I3bp1060iA19LWoPOmzdPLV68WJkUaj7RFMemXEzFsSmXcMRZunSpXqR1U7NmzZRbpkyZooYPH642bdqkfy9ZsqR67rnn1MMPP6zCJVy52BzHplxsi2MqF9vQlc8DLl++rAYOHKjef/99debMGX//1a5du6r+/furFClSuBpv48aN+mb04sWLQeulm59X8jFVZsShzMK9D6xbt+6a2zdv3qxCJd1z5eZZLkJvv/32eK//xBNPqGeffVaPPRHtuZiKY1MutsWxKZebVahQoZCOOf369VPt2rXT40levXpVTZs2TechXfx8Xfq9lE80xbEpF1NxbMolHHGqVaumF7dJBVQ4K6FM5mJzHJtysS2OqVysE+kmW7g+mfUnZ86culudTEsvizzOnTu33uYWaV5crlw5JyYmRs8qJD99j2XxUj6myow4lFm494G4n8fAxbc+1M+ndC9YtWpVottXrFihn+OFXEzFsSkX2+LYlIvPpUuXnDVr1jizZ8/Wizy+ePGiEw6LFi1y6tev7+TIkUPPLFqjRg1nzpw5rsYwlY+JODblYiqOTbmYjCPGjx/vVK9eXU9N7+t2+9577znffvut4zWmcrEpjk252BbHps9mpFAx5QEZM2Z0Zs2aFW/9zJkz9Ta3NG3a1HnggQecI0eO6JvQjRs3OosXL3aqVKmiL1S9lI+pMiMOZRbufSBbtmzOZ599pk9yCS0SJ9SbX4mxcOHCRLcvWLBAP8cLuZiKY1MutsWxKZcrV644L7/8spM5c+Z4lV+yTqZ1l+eYNnHiROfMmTNRm4+JODblYiqOTbmYjOMzcuRIJ3v27M5rr72mK41941WNHTvWqV27tisxLl++7Lz11lvOnXfe6eTKlcvJkiVL0OKlXGyLY1MutsUxlYvtqJjyAPnWUiqJ4pJ18iFwi1xkS2sPITfUf/75p348f/58p0KFCp7Kx1SZEYcyC/c+0KBBA+fVV19NdLt8MysXwaF45plnnEKFCjnTpk1zTp486V8vj2Vd4cKF9eCnXsjFVBybcrEtjk259OzZUx9jpBXmzp07nXPnzulFHn/88ce6teaLL77omJYhQ4ZbGsTZVD4m4tiUi6k4NuViMo5PyZIlnW+++SbeQOrr16935csj0bdvX93i4+2339aDoMsxrmPHjvr1hw8f7ngpF9vi2JSLbXFM5WI7KqY8QGb1ad26tXP+/Hn/Onncpk0bZ8CAAa7FkW93duzYoR8XLVrU+emnn/Tjbdu26dpfL+VjqsyIQ5mFex+QiiGZFScxx44dc8aNGxdSDHm/0u1QZg+RFh5yMSqLPJZ1nTt3DsovmnMxFcemXGyLY1Mu0mJBugYlRrbJza9ptzq7mKl8TMSxKRdTcWzKxWSc682auWXLFr3NDXL9P2PGDH8MuQcQUikl1zpeysW2ODblYlscU7nYjsHPPWD16tVq/vz5Kn/+/Kp8+fJ6nczqIYOT16tXT7Vo0cL/XBmo9FaVKVNGv26RIkXUXXfdpd58802VMmVK9cknn7g6U4mJfEyVGXEos3DvAw8++OA1t2fJkkUPVhyKVKlSqY8++kgNHTpUrVy5Uh08eFCvz507t56lK2PGjMoNJnIxFcemXGyLY1Mup0+fVnnz5k10e548edTZs2eVV5jKx0Qcm3IxFcemXEzG8ZHr8zVr1uhB1APNnj1bz5znBjn/ly1b1j+Ry8mTJ/Xjpk2bqr59+yov5WJbHJtysS2OqVxsR8WUB2TOnFlP5R4oHNPRv/LKK/4TqEwNLSehu+++W2XLlk1NnjzZU/mYKjPiUGam9oHz58+r1KlTJ7jtwIED+gI4VFIBVadOHRVuJnIxFcemXGyLY0MutWvXVi+88IL68ssvVfbs2YO2/f3336pXr176OV5hKh8TcWzKxVQcm3IxGcene/fueoZcOeZIr5fly5err776Sg0ZMkSNHj3alRjyJZsctwoWLKiKFSum5s6dqypVqqR+//13/SWWl3KxLY5NudgWx1Qu1ot0ky1Et6NHjzpXr16N9NsAkjzpv7569ep45fD111+7Om5aQg4ePKi7LHotFxNxbMrFtjg25LJnzx6nTJkyTmxsrFOxYkWnUaNGepHHsk5m0pXneKUrn6l8TMSxKRdTcWzKxWScQF988YVTvHhx/yDr+fLlc0aPHu3a6/fq1csZPHiwfjxp0iSdh8STbv2yzUu52BjHplxsi2MqF5vpUTkjXTmGa/vnn3907WvatGn177t371bffPONKlWqlGrQoEFYim/v3r1ha/lhIh9TZUYcyszUPvDMM8+oMWPGqIEDB+pvYaV1o3w7M2XKFDV48GDVrVs3FS7SNVG+Mb1y5YqncjERx6ZcbItjSy5Xr15Vc+bMUb/99ltQN9tq1arpY0yyZMmUaRkyZNDHhVvp5m8qHxNxbMrFVBybcjEZJ65z586pM2fOqJw5c6pwWrp0qV7+9a9/qWbNmnk6F5vi2JSLbXFM5WKlSNeM4fruvfde56OPPtKPjx8/rgdSzJ8/vx5MTaandMulS5f01LYyI58MeiyLPJapcC9evOipfEyVGXEoM1P7gJABSXPnzu3UrFnTKVasmFO+fHk940eoZDbOay2TJ08Oedp7U7lEIo5NudgWx6Zcoknp0qUj0lILAADYiRZTHiD91n/++WdVunRp3U/1gw8+0AMu/+9//1P9+vVTmzZtciVO586d9eDMMr6UfNMj5FuSAQMGqObNm+vBkb2Sj6kyIw5lZmof8H0z27VrV/1ZjI2NVd9//71q2LBhyK8r3+jGxMToll9x+dbLT7daTIUzl0jEsSkX2+LYkIt8/nbt2qVbMMtry8QK0irzwoULqkmTJvHGtgnF5cuX1YYNG4JafkjrzxQpUrgWw1Q+JuLYlIupODblYiJOxYoV9fn3RqxatUq5YcKECWrUqFFq586d+j5ABnQeNmyYHuD5gQceiPpcbIpjUy62xYnEZ9N2DH7uAdIkUJrNCxmEUGb5khvJqlWr6m5Dbpk4caKaNGmSaty4sX9duXLl9Mm2devWrlVMmcjHVJkRhzIztQ9s375dPfroo/qGUboNSGXY/fffr5577jndXSiUG8esWbPqWThlFsGEyI2qm034w5mL6Tg25WJbHBty2bx5s67gku710m1OjjEPPfSQ+vPPP/1diH/99VfdzSbUijWpSB8xYoR/Fi6fTJkyqS5duuiuiqF2SzKVj4k4NuViKo5NuZiKI18M+8jAyiNHjtSVxb4vkKULoZyjpUuxG+RaX44Fzz//vD5++b6QkolepHIqlIopU7nYFMemXGyLY/qzmSREuskWrq9s2bLO8OHDdbN56Vr366+/6vUrVqxwcuXK5VoR5siRw9m4cWO89bLOzYFiTeRjqsyIQ5mZ2gdksOFWrVrp7oI+S5Ys0d2GKlSoENJrN2jQwHn11VcT3b5mzRo9kKMXcjEdx6ZcbItjQy4PPPCAc//99zvr1q1znn/+eT3QuqyT7vXnz593mjVr5jz22GMh59CzZ099DTBq1Chn586dzrlz5/Qijz/++GPdRfnFF18MOY6pfEzEsSkXU3FsysVkHJ+OHTvqITfi6tevn9OhQwdXYkgO33zzTbxJDqRrcrZs2Rwv5WJbHJtysS2OqVxsR8WUB0ydOtVJkSKFHuNFxrPxef311/XsH26RWbdat26tT6Y+8rhNmzbOgAEDPJWPqTIjDmVmah8YP358gutPnTrlPPHEEyG99rRp05wJEyYkuv3YsWPOuHHjHC/kYjqOTbnYFseGXKSyyDfj35kzZ3QF8eLFi4MqwAoWLOiESirRZ8+eneh22SaVU6EylY+JODblYiqOTbmYjOMjX35t2bIl3npZJ9vcIONj7tq1K17FlMSQbV7KxbY4NuViWxxTudiOiimPOHDggLNq1SrnypUr/nXLli1zNm3a5P997969QdtvVvPmzZ0MGTLo1lH16tXTizyWD9SDDz4YtHghHxMxiEOZmdwHACQtadKkcXbv3u3/XW4Ut23b5v9dWmqmSpUq5Dhp06bVrT4SI5MgpEuXzjP5mIhjUy6m4tiUi8k4gRXIY8eOjbde1rlRcexrMfXtt9/Gq5h6//33nYoVKzpeysW2ODblYlscU7nYjjGmPEIGIJUlUJUqVYJ+l36ta9asuaXpm339x1u2bBm0TsaX8mo+JmIQh7+NiX1ABlP99ttv9SCkgYMSV69eXY/3kDJlSuUG6SOfOnXqBLcdOHBA5cmTJ+QYpnIxEcemXGyLY0suefPmVXv27FEFCxbUv8tYcIFTUB85ckRlyZIlxCyUql27tnrhhRfUl19+GW+w5r///lv16tVLPydUpvIxEcemXEzFsSkXk3F8ZNwnmahIBlL2XWMsW7ZMjRkzRvXt29eVGN27d1fPPvusvh6QBgzLly9XX331lRoyZIie4MVLudgWx6ZcbItjKhfrRbpmDO4J/GbDBibyMVVmxKHMbnUf2Lp1q1O0aFHdhP6ee+5xHn74Yb3IY1lXvHhx/Ry3vin1dUsI9PXXX7syzpypXEzEsSkX2+LYlMt//vMf59NPP010+5AhQ5wmTZo4oZKWHWXKlHFiY2N1qwjphiyLPJZ15cqV088Jlal8TMSxKRdTcWzKxWScQJMnT3aqV6/uZMmSRS/yWNa56YsvvtDHL+maKEu+fPmc0aNHO24zkYttcWzKxbY4pnKxGRVTFgm18kMGOj179qz/d+lj/t577zlz5sxxIoGKqaQdx6ZcQolTv359PZjqyZMn422TdbJNBi93Q+fOnXW3gzfeeMM/Zka7du10d4V333035Nc3lYuJODblYlscm3K5nh07djj79+935bWkq/GsWbP0YK2dOnXSizz+4YcfjHVDdjOfSMexKRdTcWzKxWScuCZOnKjP36GSe4JDhw45keRWLkkpjk252BbHVC5eRcWURUK9wZaBmz/66CP9WGYXkj6x+fPn19/8jhw50jGNyo+kHcemXEKJI5VCMhtOYmRcGHmOW2bMmOHkzp3bqVmzpp5VrHz58teMH425mIhjUy62xbEplxsR+IWSDUzlYyKOTbmYimNTLibjBJLxYm3pQWEqF5vi2JSLbXFs+myGQ7JIdyVE9JB+sXfffbd+/PXXX+txMnbv3q3Gjx+v3n///Ui/PSBJkrHfdu3aleh22SbPcUvjxo1VixYt1JIlS/TYGUOHDlVlypTxVC4m4tiUi21xbMrFp169euqvv/6Kt17Gf6lQoYIrMeTLyp07d6rLly/7x8+aPHmyvgaQcabcZCIfU3FsysVUHJtyMRnnZj7LN6NixYqqUqVKN7REey7EocyieR8wtT97FRVTFomJiQnp/587d05lyJBBP547d66+OU2WLJmqWrWqrqDyWj7REoM4lFko+8CTTz6p2rZtq9577z21bt06dejQIb3IY1nXvn171alTJ1f20+3bt6tq1aqpGTNmqDlz5qgXX3xR3X///frnpUuXQn59U7mYiGNTLrbFsSkXH5mUoFy5crqiSFy9elUNGDBA1axZUzVp0iTk19+8ebMqUqSIKl68uCpZsqSuoJIB3Dt27KgHdJV1W7duVW4Jdz4m49iUi6k4NuViMk64NG/eXE/WIEvDhg31tUCqVKn0hAeySH6yTrYBQNiEpR0WIiLULklly5Z1hg8frgc4zZgxo/Prr7/q9StWrNDTYJpGd7GkHcemXEKNI2M+5cmTRw9CmixZMr3IY1k3dOhQV99jq1atdFdenyVLlugufRUqVHAlhqlcTMSxKRfb4tiUi8+HH37opE2b1mndurVTrVo1J2/evK6NASnjYd1///26++Hzzz+vJ0KQdRcvXnTOnz/vNGvWzHnsscccr+RjOo5NuZiKY1MuJuOE+3qjY8eOziuvvBJvvYw316FDB8c0L1yjRVscm3KxLY5tE5W5jYopD5GLQ1kSIxVKly9fvuXXnzp1qpMiRQp9YS3jTfm8/vrrenYer+VjKgZxKDNT+4AMpCoVxrLIY7eNHz8+wfWnTp1ynnjiCVdjhTsXk3FsysW2ODblInr37q0rvuRcLZXGbsmRI4d/Rk4ZmFViLF682L9dYhUsWNDxSj6RiGNTLqbi2JSLyTjhvPmVL6a3bNkSb72sk22m2VQpYSqOTbnYFoeKqWujYirKzZ0712ncuLGTOXNm/7ex8ljWzZs3z/V4Bw4ccFatWhU0A8+yZcucTZs2+X/fu3fvLc/QYyIfU2VGHMrM9OczsQqvSHyL6eVcTMSxKRfb4ngxl2PHjjktWrRwMmXK5HzyySdOmzZtnHTp0jkjRoxw5fVlkPbdu3cHXTxv27YtKBeZsdMt4c7HZBybcjEVx6ZcTMYxcfMrPSTGjh0bb72sk0mRTLOpUsJUHJtysS0OFVPXRsVUFBs3bpwTGxvrPPLII/qEINM4yyKPpamwfCOTWAuHaJxRwEQ+psqMOJRZtHw+16xZoyvEQnXhwgVn8uTJuhuP5CSLPJ4yZYreZoJbuURDHJtysS2OF3ORbkE1atQIao01adIkJ2vWrE6TJk1Cfn3prhvYQkpm4pWWkj4rV67Us3W6Jdz5mIxjUy6m4tiUi8k4N6p06dK6MvlWDBkyRM/G3bVrV2fChAl66dKli+6mKNu8lEtSjWNTLrbFMZWLV8XIP+EbwQqhuO2229Rzzz2nnn322QS3jxw5Ug+w6uaApDdCBkhfu3atKlq0aNTlY6rMiEOZmdoHpk+ffs3tO3bsUD169FBXrly55Rjbtm3Tg5ru379f3XXXXSpXrlx6vQzkvGzZMpU/f371ww8/6IGRoz0XU3FsysW2ODbl4vPqq6+ql19+WU9IEmjfvn2qQ4cOat68eSG9/tNPP63uuOMOPaB7Qt544w21ePFiNXPmTOWGcOdjMo5NuZiKY1MuJuPIjJkbNmxQBw8e1L/L7NmlSpVSKVKkUG6aMmWKGj58uNq0aZP+XSY/kOudhx9+2LUYpnKxKY5NudgWx1Qu1ot0zRgSJ83m//zzz0S3yzb5VsO0W22GaCIfU2VGHMrM1D7gG1RZfia2hNoqo379+nqg45MnT8bbJutkW4MGDRwv5GIqjk252BbHplyihbQE2b9/f6TfBpAkyfAZL7/8sh4qIO4xRtbJYOW3OsTGrZo4caIejy5ac7Epjk252BYnGj+bXkbFVBSrVKmS07Nnz0S3v/jii/o5XqmYMpGPqTIjDmVmah+QLgLffvttottlwOJQb35lfJn169cnul1m6pLneCEXU3FsysW2ODblEjjW47Bhw/TgyrLIY1ln0tmzZ117LVP5mIhjUy6m4tiUi4k4cq0hExSMGjXK2blzp3Pu3Dm9yOOPP/5Yj/0k1xxeGNbDVC42xbEpF9viRONn08uomIpiCxYs0IMnli1b1unWrZuelloWeVyuXDldQfTzzz97pmLKRD6myow4lJmpfUCmae/bt+81x7GRb2ZCIVPbf//994lunz59un6OF3IxFcemXGyLY1Muhw4dcmrWrKlfp1ChQk6VKlX0Io9lnWyT57ilbt26zr59++Ktl5vsf/3rXyG/vql8TMSxKRdTcWzKxWQcGZB89uzZiW6XbaYHJr/VewFTudgUx6ZcbIsTjZ9NL6NiKspJjavUtNaqVcu57bbb9CKPe/XqpbdFwq1+S2IqH1NlRhzKzMQ+sGjRIueHH35IdLs0pV+4cGFIMeTmOkuWLM67777rrF271jl48KBe5LGskwFc+/fv73ghF1NxbMrFtjg25dKyZUunWrVqCXYblnXVq1d3/v3vfztukYGa5fMuAzcL6YIgn32ZzOG5554L+fVN5WMijk25mIpjUy4m48jA49JyOTFyrpYvyrxQMWUqF5vi2JSLbXGi8bPpZVQ0S/ebAAAZkElEQVRM4aYx1SVgH2ntJa2ifOPi+MbOkXVDhw6N9NsDkuz5dtWqVYluX7FihX6Omz788EN9sS2zi8pNt3RZnDNnjqfyMRHHplxMxbEpF5NxpMJYxnk8cuRIvG2yrlGjRs59993neOFewFQuNsWxKRfb4kTjZ9PLYiM9+DpufqT/PHny6BkywjXS/4ULF/TPVKlSJbh948aNKm/evFGdj6kyIw5lZvrzGS69evXSy86dO4NmFSlSpEik3xqQZMl5+NSpU4luP336dKLn6lslM43KbGJDhw5VsbGxauHChap69eqeysdEHJtyMRXHplxMxhk1apRq0qSJvr4oW7Zs0My569ev17N/zZgxQ3mBqVxsimNTLrbFsemzGRUiXTOG6Bjpf+7cuU7jxo316/paS8hjWTdv3jxXYtg0OwJxKLOkNBPHnj17nA4dOkT6bQBJzjPPPKPHq5k2bVrQrJnyWNYVLlzY6dKli2vxjh075rRo0cLJlCmT88knnzht2rTR3RBGjBjhqXxMxLEpF1NxbMrFZBwh1xOzZs1y+vXr53Tq1Ekv8li6E0fiWiOU3hOmcrEpjk252BYn2j6bXkbFVBQzNdL/uHHjnNjYWOeRRx5xxo4dqz9csshjacovY0uMHz/eE/nYNNODbXFsysVknGgggzjbMu094CXnz593nn76aSdlypT6M5g6dWq9yGNZ17lzZ/0ct0i3vRo1ajg7duzwr5PxpmTcKemy4JV8TMSxKRdTcWzKxWScaFS6dGn9pRUAuEVPFxPpVltImHSj+fzzz1XDhg0T3D5nzhzVtm1b3VwwFLfddpt67rnndPP9hIwcOVK99957auvWrVGfj6kyIw5lZmofMGH69OnX3L5jxw7Vo0cPdeXKFWPvCcD/ke5CK1euDOpmW7lyZZUxY0ZXi+nVV19VL7/8skqWLFnQeuna16FDBzVv3jxP5WMijk25mIpjUy4m4wSSbvfbtm3TXYjKlCkTtuEJJBfpjhTO4QnClYvNcWzKxbY4pnKxkmtVXHCdqZH+U6VKleCMIj6yTb4BCpVNsyMQhzKzaSYO34DncbskBi60mAIiY+PGjc6YMWOcTZs26d/lp7TSkO618+fP99yfxVQ+JuLYlIupODblYiqOtLw6ffq0fiwts2U2wMBzc506dfzbo314AhO52BbHplxsi2Mql6SCiqkoZmqk/0qVKuluSYmR7kjynFDZNDsCcSgzm2bikO473377baLbV69eTcUUEAEyRoV0CZKudPIFkfwuXYjr16/v1K1b10mePLmrN9nLli1zhg0b5vTu3Vsv8ljWeS0fE3FsysVUHJtyMRlHbnAPHTqkH/fp08fJnz+/89NPPzlnz551fvnlF6dYsWL68+qF4QlM5GJbHJtysS2OqVySCiqmopj03S5Tpowe/6lixYr6RlcWeSzrypUr50r/7gULFuiWHWXLlnW6deump42XRR5LDBng8Oeff/ZEPqbKjDiUmal9wIRmzZo5ffv2veYYU/LtDwCzqlWrplsxiK+++srJkiWL89JLL/m3ywXvvffeG3IcubCuWbOm/pzLYM5VqlTRizyWdbLNd/HthXxMxLEpF1NxbMrFZBz5DPo+f3LdMXHixKDt3333nXPbbbeFFCNXrlzO7NmzE90u26Ryygu52BbHplxsi2Mql6SCMaai3NWrV/VYNb/99ltQf+9q1aqpBg0axBsH4lbt2rVLffTRRwnGefrpp1XhwoU9k4+pMiMOZWZqHwi3xYsXq7Nnz6pGjRoluF22rVixQt1zzz3G3xuQlGXKlEmPXVO8eHF9vJGp55cvX64qVqyot//xxx+qfv36/uPPrfr3v/+t9u/fr8aOHatuv/32oG2bN29WTzzxhMqbN6+aOnWqJ/IxEcemXEzFsSkXk3HkWkLGq8yRI4deFi5cqEqXLu3fvnv3blWyZEl17ty5W46RLl06fS0jU94nZN26dap69erqzJkzKtpzsS2OTbnYFsdULklFbKTfAK6/wzdu3Fgv4SQVT0OHDrUiH1NlRhzKzNQ+EG533333dS9YqZQCIiMmJsZ/vEmdOrW+GfbJkCGDOnnyZMgxpIJ90aJF8SqlhKx7//33Ve3atZVX8jEVx6ZcTMWxKReTcfr27avSpk2r40glcuDN79GjR/V5OhTy+X7hhRfUl19+qbJnzx607e+//1a9evVy7RgQ7lxsjGNTLrbFMZVLUuCNr/ORaCsGuZB0cyaOtWvX6gtUWeTbkUuXLnk2n0jFIA5lZnIfAGAv+dIocEbcpUuXqoIFC/p/37Nnj575J1TS0kNmFkvM6dOn9XO8ko+JODblYiqOTbmYjFOrVi3dcnH16tV6hjxphRFo1qxZQTfDt2LUqFH6plreb6VKlfxfusljWSfbpGeFF3KxLY5NudgWx1QuSQVd+TxMKpHkhBHqFO7S/Lhfv35qxIgR8b7ZkW9+unTpogYOHBj2bklu5RPpGMShzEzuAwDsJTeLBQoUUPfdd1+C21966SV1+PBhNXr06JDiPPvss2rmzJnqvffeU/Xq1fNPcy+VVfPnz1fdu3dXTZs2VR988IEn8jERx6ZcTMWxKReTca5nx44dKmXKlCp//vyeH57ArVySUhybcrEtjqlcbEHFlIe5deP74osvqnHjxqlXX31VNWzYUOXKlUuvlz6zc+fO1U0U27dvH/auflRMJe04NuViMg4AhOrChQvq+eefV2PGjNGtp+VCWly8eFHFxsaqjh076korN1pNAXCfjGEj3YlsYCoXm+LYlIttcWz6bIYbFVNRLGvWrNfcLje8MghhqDe+8m3I559/riulEiLfnrRt21ZXVEV7PqbKjDiUmal9AABMkRZSMphzYGuJypUr+1tQAYgcac04fvx4lS9fvqD1MuD6Y489prZs2eJqvJ07d6pt27bprnxlypTxZC42xbEpF9vimP5s2orBz6P8G8zOnTsnOkOG9GOVLnahkrEjZLadxMgJScbL8UI+psqMOJSZqX0AAEzYtGmT7sIj3Xbq1Kmj/vzzTzV8+HA1YcIEfWFdt25d/hBABMng6uXKlVMjR45UrVq10l3vBg0apF5//XX1zDPPhPTa8v/ffPNNlT59evXPP/+oxx9/XE2bNs0/wLtMgDJ9+nS9PdpzsTWOTbnYFsdULtZzELWqV6/uDBs2LNHta9ascZIlSxZynCZNmjgNGjRwjhw5Em+brGvUqJFz3333eSIfU2VGHMrM1D4AAOH2ww8/OClTpnSyZs3qpE6dWv+eI0cOp379+k7dunWd5MmTO/Pnz+cPAUTYhx9+6KRNm9Zp3bq1U61aNSdv3rzOnDlzQn5duV45dOiQftynTx8nf/78zk8//eScPXvW+eWXX5xixYo5vXv3dryQi81xbMrFtjimcrEZLaaimAymeOLEiWt2JZIudqGSwRubNGmiW0ZJ64/AMabWr1+vZxmYMWOGJ/IxVWbEocxM7QMAEG7yzW7Pnj3Va6+9piZNmqQeffRR3SJ08ODBenufPn3UG2+8QaspIMJkooJ9+/bpcV9l/LeFCxeq6tWrh/y6juP4H3///fe69ZS0nBQ1atRQ7777rj5GDBkyREV7LjbHsSkX2+KYysVqka4ZQ3S4cuWKM2vWLKdfv35Op06d9CKP5VtT2QYAAOyUMWNGZ+vWrfqxnPNjY2OdVatW+bevX7/eyZUrVwTfIYBjx445LVq0cDJlyuR88sknTps2bZx06dI5I0aMCLlwYmJinMOHD+vH2bNnd/7444+g7bt27XLSpEnjiVxsjWNTLrbFMZWL7aiY8rirV686NjGRj6kyIw5lZtvnE4C9FVPbtm3z/54+fXpn+/btQTel0sUPQORI16AaNWo4O3bs8K+bNGmS7oIrw3KEWjH1n//8x+nWrZuTM2dOZ+7cuUHbV65cqSusvJCLrXFsysW2OKZysV2ySLfYwvW1b98+wcHHd+3apWrVqhX2IpTYixYt8lQ+psqMOJRZpD+fABCqwoULq61bt/p/X7p0qSpYsKD/9z179uju/gAi5+mnn9bX40WKFPGvk4GW165dqy5evBjSa8v1yubNm9Xq1av1EB4ygUugWbNmqdKlSysv5GJrHJtysS2OqVysF+maMVxfhQoVnKJFizq//vqrf924ceP0N5zNmzcPexG6PYiziXxMlRlxKLNIfz4BIFQfffSRM2PGjES3y2DIHTt2pKCBJEpaUO7duzfSbwOAxWLkn0hXjuHaLl26pF566SX1/vvvqx49eqht27apH374QQ9E+NRTT4W9+KS2t1KlSurKlSueycdUmRGHMov05xMAANhv+fLlujXjwYMH9e+5c+dW1apVU1WqVDES/9y5cypt2rSeysWmODblYlucSH82rRHpmjHcOBmMXPqAp0iRIqh1RqiyZMlyzUVafoRj2vtw5WM6BnEoM5P7AAAASDoOHTrk1KxZU19jFCpUyKlSpYpe5LGsk23yHDfUrVvX2bdvX7z1y5Ytc/71r395Jheb4tiUi21xTH42kwIqpjzg4sWLTvfu3Z1UqVI5L730klOrVi0nd+7czsyZM115/bRp0zo9evTQ3Y8SWgYOHOhqxVS48zEVgziUmcl9AAAAJD0tW7Z0qlWr5vz555/xtsm66tWrO//+979diSUDNcuAzTJws2+Wzv79++sv3Z577jnP5GJTHJtysS2Oyc9mUkDFlAeUK1fOKV68uLN06VL/TF9vvPGGvhHu3LlzyK8vH5phw4YZG2Mq3PmYikEcyszkPgAAAJIemSVz1apViW5fsWKFfo5bPvzwQ/2ldevWrfVNt8w4NmfOHE/lYlMcm3KxLY7pz6btmJXPA+644w61Zs0aVbVqVf17TEyM6tWrl+7L6sZseffdd586ceJEotuzZs2q2rZtq7ySj6kYxKHMTO4DAAAg6UmVKpU6depUottPnz6tn+OWZ599Vv33v/9VkyZNUitWrFBTp05VDRo08FQuNsWxKRfb4pj+bFov0jVjCM358+etKkIT+ZgqM+JQZrZ9PgEAgFnPPPOMHrNm2rRpzsmTJ/3r5bGsK1y4sNOlSxdXYh07dsxp0aKFkylTJueTTz5x2rRp46RLl84ZMWKEp3KxKY5NudgWx+RnMylgVj4PkBH+ly1bFjTS/1133aV/miLdPqUliFfyMVVmxKHMouHzCQAA7HThwgX1/PPPqzFjxqjLly+rlClT6vUXL15UsbGxqmPHjuq9995zpWVGvnz5VJEiRdSECRP0TzF58mT1zDPP6JbhM2fO9EQuNsWxKRfb4pj8bCYFVExFsbNnz6r//Oc/6quvvlLJkiXTXerEsWPHdEVR69at1ccff+za1K3t27dXI0aMUOnSpQtav2vXLvX444+rxYsXR30+psqMOJSZ6c8nAABIuqTL0MqVK4O+CKtcubLKmDGjazFeffVV9fLLL+vrmkD79u1THTp0UPPmzfNMLrbFsSkX2+KYysV2jDEVxZ577jm1fPlyNWvWLHX+/Hl16NAhvchjWSfb5DluWbt2rSpXrpweG8fn888/V+XLl1fZs2f3RD6myow4lJnpzycAAEi65Ca3Tp066v7779fXGj/++KNu2XT06FHXYvTt2zdepZTInz+/a5VSpnKxLY5NudgWx1Qu1ot0X0IkLnPmzM6SJUsS3f7LL7/o57g57f0LL7zgpEyZ0unTp4/z0EMP6ZkEpI+5V/IxVWbEocxMfz4BAEDSU7JkSefo0aP68Z49e/S4NTIG1J133ulkzZrVyZkzp7Njxw5XYi1btkzP1N27d2+9yGNZ57VcbIpjUy62xTH52UwKaDEVxa5evervq5oQ2SbPcUuKFCnUW2+9pXr37q3eeOMN9e2336q5c+eqp556yjP5mCoz4lBmpj+fAAAg6fnzzz/1+DWiT58+Km/evGr37t26Zbb8lN4O0v0uFIcPH1Z33323HkdKxsT56aef9CKPZZ1sk+d4IRfb4tiUi21xTOWSZES6ZgyJe/TRR52KFSs6q1atirdN1lWuXFnPluFmi6nu3bs7qVKlcl566SWnVq1aTu7cuZ2ZM2d6Jh9TZUYcysz05xMAACQ9MTExzqFDh/TjokWLOnPnzg3aLq23CxQoEFKMli1bOtWqVXP+/PPPeNtkXfXq1Z1///vfjhdysS2OTbnYFsdULkkFFVNRTKZsbdSokd7ppTlgiRIl9CKPkyVL5jRu3Ng5fvy4a/HKlSvnFC9e3Fm6dKn+/erVq84bb7yhK6o6d+7siXxMlRlxKDPTn08AAJD0yHXG4cOH9eO8efM669evD9q+a9cuJ3Xq1CHFkKE7EvqizWfFihX6OV7IxbY4NuViWxxTuSQVsZFusYXEZcmSRf3www9q06ZN6rfffgsa6b9atWqqRIkSrhbfHXfcod5//33/rHwxMTGqV69eqkGDBnpWPi/kY6rMiEOZmf58AgCApKlevXp6+nmZ/Wvz5s2qTJky/m3SZShbtmwhvb5MZy+vnZjTp0+7NuV9uHOxMY5NudgWx1QuSQEVUx5QsmRJvYTbZ599luD6ihUr6ikwvZSPqTIjDmVmah8AAABJT//+/YN+T58+fdDv33//vR4DKhStWrVS7dq102NKyY22b5p7udmeP3++6t69u2rdurXyQi62xbEpF9vimMolqYiRZlORfhNI3MWLF/Ug5EuXLg1qkVG9enX1wAMPXHPw5Zslr79s2bKgOHfddZf+6aV8TJUZcSgzk59PAACAcLhw4YJ6/vnn1ZgxY/Rgzr7rF7nOkdYgHTt21JVWbrWaAoC4qJiKYtu2bVMNGzZU+/fv1xVEuXLl0usPHTqkK5Dy58+vuxIVL148pDhnz55V//nPf9RXX32lkiVLprJmzarXHzt2TMYg09+QfPzxxypt2rRRn4+pMiMOZWZqHwAAADBBWkhJL4nAL9sqV67sb0EFAOFCxVQUu/fee/V4T+PHj493QpATR9u2bdU///yj5syZE1KcJ598Ui1atEh98MEHqn79+ip58uR6/ZUrV3Tz3a5du6patWqpTz/9NOrzMVVmxKHMTO0DAAAAJsmX1lOmTNFfwuXNm1c98sgjjJUDIKyomIpi0kJp+fLlQYOoBVq/fr1uqXHu3LmQB3GeOXOm7n6UkCVLlqimTZuq48ePR30+psqMOJSZqX0AAAAgnEqVKqV++eUX3Wti7969+gtpue6/7bbb1Pbt23V3PpnopUiRIvwhAIRFsvC8LNyQOXNmtWvXrkS3yzZ5TqiuXr16zbFwZJs8xwv5mCoz4lBmpvYBAACAcPrzzz/12FKiT58+upWUzCgmX8DJz3LlyqmXX36ZPwKAsGFWvigmXeykO1Dfvn31DBmBY9hIF7vXXntNd7MLlbSG6tSpk56VT2bgC7R69WrVuXNn1axZM0/kY6rMiEOZmdoHAAAATJEJXUaNGqUyZcrkn2ls4MCBujsfAISNzMqH6PXGG284efLkcWJiYpxkyZLpRR7LuqFDh7oS49ixY06jRo3062bNmtUpUaKEXuSxxGvcuLFz/Phxz+RjIgZxKDOT+wAAAEC4yLXL4cOH9eO8efM669evD9q+a9cuJ3Xq1PwBAIQNY0x5xM6dO4NmyAhHH+9Nmzbp/uOBcapVq6ZKlCjhyXxMxCAOZWZyHwAAAHCbzMotY2bKWFJbt25V48aNUy1btvRvl0mSHn30UbVv3z4KH0BYUDHlYTI4Yf/+/dWYMWOUDUzkY6rMiEOZ2fb5BAAAdpKueoGqVq2qGjZs6P+9Z8+eulLqq6++isC7A5AUUDHlYWvXrlWVKlVSV65cCfm1Ll68qL799lvdrzyw5YfM1PfAAw9cc3D0aMwnkjGIQ5mZ3AcAAAAAwMsY/DyKTZ8+/Zrbd+zY4Uqcbdu26W9F9u/fr6e39w3iLAOfy+CH+fPnVz/88IMqXrx41OdjqsyIQ5mZ2gcAAAAAwGa0mIry/t4xMTEyQH2iz5HtobbIuPfee1W6dOnU+PHjVcaMGYO2nTp1Ss889s8//6g5c+ZEfT6myow4lJmpfQAAAAAAbJYs0m8AicuTJ4+aNm2aunr1aoLLqlWrXCm+JUuW6Knt41ZKCVn36quvqsWLF3siH1NlRhzKzNQ+AAAAAAA2o2IqilWuXFmtXLky0e3Xa61xozJnzqx27dqV6HbZJs/xQj6myow4lJmpfQAAAAAAbMYYU1FMZsA4e/ZsottlzKcFCxaEHOfJJ5/U3fX69u2r6tWr5x9j6tChQ2r+/Pm6NVXXrl09kY+pMiMOZWZqHwAAAAAAmzHGFLShQ4eq4cOH6xn5pKWHkNYeMjPf888/r1588UVKCgAAAAAAuIqKKQTZuXOnrpwSUilVpEgRSggAAAAAAIQFFVO4rr1796r+/furMWPGUFoAAAAAAMA1VEzhutauXasqVarEtPcAAAAAAMBVDH4ONX369GuWwo4dOyglAAAAAADgOlpMQSVLluy6U9vL9itXrlBaAAAAAADANcnceyl4VZ48edS0adPU1atXE1xWrVoV6bcIAAAAAAAsRMUUVOXKldXKlSsTLYnrtaYCAAAAAAC4FYwxBdWzZ0919uzZREuiePHiasGCBZQUAAAAAABwFWNMAQAAAAAAICLoygcAAAAAAICIoGIKAAAAAAAAEUHFFAAAAAAAACKCiikAAAAAAABEBBVTAAAAAAAAiAgqpgAAAAAAABARVEwBAAAAAAAgIqiYAgAAAAAAgIqE/w+R78c7vzxy2gAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "\n", + "df = pd.DataFrame(results)\n", + "\n", + "metrics = [\"время_мс\", \"посещено_клеток\", \"длина_пути\"]\n", + "titles = [\"Время (мс)\", \"Посещено клеток\", \"Длина пути\"]\n", + "colors = {\"BFS\": \"#4C72B0\", \"DFS\": \"#DD8452\", \"A*\": \"#55A868\"}\n", + "\n", + "fig, axes = plt.subplots(len(metrics), 1, figsize=(12, 14))\n", + "\n", + "for ax, metric, title in zip(axes, metrics, titles):\n", + " for strategy_name in df[\"стратегия\"].unique():\n", + " subset = df[df[\"стратегия\"] == strategy_name].reset_index(drop=True)\n", + " ax.bar(\n", + " [\n", + " i + list(df[\"стратегия\"].unique()).index(strategy_name) * 0.25\n", + " for i in range(len(subset))\n", + " ],\n", + " subset[metric],\n", + " width=0.25,\n", + " label=strategy_name,\n", + " color=colors[strategy_name],\n", + " )\n", + " ax.set_title(title, fontsize=13)\n", + " ax.set_xticks([i + 0.25 for i in range(len(df[\"лабиринт\"].unique()))])\n", + " ax.set_xticklabels(df[\"лабиринт\"].unique(), rotation=90, ha=\"right\")\n", + " ax.legend()\n", + " ax.grid(axis=\"y\", alpha=0.3)\n", + "\n", + "plt.tight_layout()\n", + "plt.savefig(\"results.png\", dpi=150)\n", + "plt.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/skorohodovsa/task_2/practice/results.csv b/skorohodovsa/task_2/practice/results.csv new file mode 100644 index 0000000..8e373ed --- /dev/null +++ b/skorohodovsa/task_2/practice/results.csv @@ -0,0 +1,121 @@ +лабиринт,стратегия,время_мс,посещено_клеток,длина_пути +100x100_spaghetti_v1.txt,BFS,9.2513,205.0,205.0 +100x100_spaghetti_v1.txt,DFS,8.2451,2129.0,2129.0 +100x100_spaghetti_v1.txt,A*,7.1113,205.0,205.0 +100x100_spaghetti_v10.txt,BFS,9.2555,207.0,207.0 +100x100_spaghetti_v10.txt,DFS,8.1821,2489.0,2489.0 +100x100_spaghetti_v10.txt,A*,8.4803,207.0,207.0 +100x100_spaghetti_v2.txt,BFS,9.3921,217.0,217.0 +100x100_spaghetti_v2.txt,DFS,6.7196,2063.0,2063.0 +100x100_spaghetti_v2.txt,A*,10.5764,217.0,217.0 +100x100_spaghetti_v3.txt,BFS,8.4084,217.0,217.0 +100x100_spaghetti_v3.txt,DFS,5.7855,2107.0,2107.0 +100x100_spaghetti_v3.txt,A*,6.3385,217.0,217.0 +100x100_spaghetti_v4.txt,BFS,8.8661,205.0,205.0 +100x100_spaghetti_v4.txt,DFS,6.8166,2409.0,2409.0 +100x100_spaghetti_v4.txt,A*,6.1874,205.0,205.0 +100x100_spaghetti_v5.txt,BFS,8.3117,217.0,217.0 +100x100_spaghetti_v5.txt,DFS,6.3364,2071.0,2071.0 +100x100_spaghetti_v5.txt,A*,8.495,217.0,217.0 +100x100_spaghetti_v6.txt,BFS,8.212,243.0,243.0 +100x100_spaghetti_v6.txt,DFS,7.0348,1869.0,1869.0 +100x100_spaghetti_v6.txt,A*,12.8413,243.0,243.0 +100x100_spaghetti_v7.txt,BFS,8.3471,211.0,211.0 +100x100_spaghetti_v7.txt,DFS,6.1699,2283.0,2283.0 +100x100_spaghetti_v7.txt,A*,6.9637,211.0,211.0 +100x100_spaghetti_v8.txt,BFS,8.3499,221.0,221.0 +100x100_spaghetti_v8.txt,DFS,7.1166,2473.0,2473.0 +100x100_spaghetti_v8.txt,A*,9.5093,221.0,221.0 +100x100_spaghetti_v9.txt,BFS,8.5536,209.0,209.0 +100x100_spaghetti_v9.txt,DFS,5.4126,1939.0,1939.0 +100x100_spaghetti_v9.txt,A*,6.7365,209.0,209.0 +10x10_path_v1.txt,BFS,0.032,13.0,13.0 +10x10_path_v1.txt,DFS,0.0341,13.0,13.0 +10x10_path_v1.txt,A*,0.0399,13.0,13.0 +10x10_path_v10.txt,BFS,0.0323,13.0,13.0 +10x10_path_v10.txt,DFS,0.036,13.0,13.0 +10x10_path_v10.txt,A*,0.037,13.0,13.0 +10x10_path_v2.txt,BFS,0.0354,17.0,17.0 +10x10_path_v2.txt,DFS,0.0433,17.0,17.0 +10x10_path_v2.txt,A*,0.044,17.0,17.0 +10x10_path_v3.txt,BFS,0.0348,17.0,17.0 +10x10_path_v3.txt,DFS,0.0492,17.0,17.0 +10x10_path_v3.txt,A*,0.0439,17.0,17.0 +10x10_path_v4.txt,BFS,0.0476,29.0,29.0 +10x10_path_v4.txt,DFS,0.0475,29.0,29.0 +10x10_path_v4.txt,A*,0.0652,29.0,29.0 +10x10_path_v5.txt,BFS,0.0302,13.0,13.0 +10x10_path_v5.txt,DFS,0.0334,13.0,13.0 +10x10_path_v5.txt,A*,0.0371,13.0,13.0 +10x10_path_v6.txt,BFS,0.0307,13.0,13.0 +10x10_path_v6.txt,DFS,0.0339,13.0,13.0 +10x10_path_v6.txt,A*,0.0375,13.0,13.0 +10x10_path_v7.txt,BFS,0.0401,17.0,17.0 +10x10_path_v7.txt,DFS,0.0499,17.0,17.0 +10x10_path_v7.txt,A*,0.0489,17.0,17.0 +10x10_path_v8.txt,BFS,0.0615,29.0,29.0 +10x10_path_v8.txt,DFS,0.0536,29.0,29.0 +10x10_path_v8.txt,A*,0.0801,29.0,29.0 +10x10_path_v9.txt,BFS,0.0579,17.0,17.0 +10x10_path_v9.txt,DFS,0.046,17.0,17.0 +10x10_path_v9.txt,A*,0.0468,17.0,17.0 +30x30_empty_v1.txt,BFS,1.1046,55.0,55.0 +30x30_empty_v1.txt,DFS,0.7781,379.0,379.0 +30x30_empty_v1.txt,A*,1.9965,55.0,55.0 +30x30_empty_v10.txt,BFS,1.1246,55.0,55.0 +30x30_empty_v10.txt,DFS,0.7002,379.0,379.0 +30x30_empty_v10.txt,A*,2.0086,55.0,55.0 +30x30_empty_v2.txt,BFS,1.1401,55.0,55.0 +30x30_empty_v2.txt,DFS,0.7263,379.0,379.0 +30x30_empty_v2.txt,A*,2.0245,55.0,55.0 +30x30_empty_v3.txt,BFS,1.1038,55.0,55.0 +30x30_empty_v3.txt,DFS,0.7249,379.0,379.0 +30x30_empty_v3.txt,A*,2.007,55.0,55.0 +30x30_empty_v4.txt,BFS,1.1224,55.0,55.0 +30x30_empty_v4.txt,DFS,0.7053,379.0,379.0 +30x30_empty_v4.txt,A*,1.989,55.0,55.0 +30x30_empty_v5.txt,BFS,1.1294,55.0,55.0 +30x30_empty_v5.txt,DFS,0.7202,379.0,379.0 +30x30_empty_v5.txt,A*,2.1138,55.0,55.0 +30x30_empty_v6.txt,BFS,1.0843,55.0,55.0 +30x30_empty_v6.txt,DFS,0.7746,379.0,379.0 +30x30_empty_v6.txt,A*,2.009,55.0,55.0 +30x30_empty_v7.txt,BFS,1.1449,55.0,55.0 +30x30_empty_v7.txt,DFS,0.7076,379.0,379.0 +30x30_empty_v7.txt,A*,2.033,55.0,55.0 +30x30_empty_v8.txt,BFS,1.3196,55.0,55.0 +30x30_empty_v8.txt,DFS,0.7794,379.0,379.0 +30x30_empty_v8.txt,A*,1.9972,55.0,55.0 +30x30_empty_v9.txt,BFS,1.1088,55.0,55.0 +30x30_empty_v9.txt,DFS,0.7131,379.0,379.0 +30x30_empty_v9.txt,A*,2.0128,55.0,55.0 +50x50_deadends_v1.txt,BFS,1.7809,729.0,729.0 +50x50_deadends_v1.txt,DFS,1.7167,729.0,729.0 +50x50_deadends_v1.txt,A*,2.5217,729.0,729.0 +50x50_deadends_v10.txt,BFS,0.7362,261.0,261.0 +50x50_deadends_v10.txt,DFS,1.7627,261.0,261.0 +50x50_deadends_v10.txt,A*,0.9753,261.0,261.0 +50x50_deadends_v2.txt,BFS,0.9246,249.0,249.0 +50x50_deadends_v2.txt,DFS,1.7347,249.0,249.0 +50x50_deadends_v2.txt,A*,1.0804,249.0,249.0 +50x50_deadends_v3.txt,BFS,0.945,297.0,297.0 +50x50_deadends_v3.txt,DFS,1.7483,297.0,297.0 +50x50_deadends_v3.txt,A*,1.0832,297.0,297.0 +50x50_deadends_v4.txt,BFS,1.5487,413.0,413.0 +50x50_deadends_v4.txt,DFS,1.6526,413.0,413.0 +50x50_deadends_v4.txt,A*,1.9521,413.0,413.0 +50x50_deadends_v5.txt,BFS,0.9255,309.0,309.0 +50x50_deadends_v5.txt,DFS,1.7299,309.0,309.0 +50x50_deadends_v5.txt,A*,1.1469,309.0,309.0 +50x50_deadends_v6.txt,BFS,1.0637,337.0,337.0 +50x50_deadends_v6.txt,DFS,1.7728,337.0,337.0 +50x50_deadends_v6.txt,A*,1.3449,337.0,337.0 +50x50_deadends_v7.txt,BFS,0.7827,261.0,261.0 +50x50_deadends_v7.txt,DFS,1.6948,261.0,261.0 +50x50_deadends_v7.txt,A*,0.9527,261.0,261.0 +50x50_deadends_v8.txt,BFS,1.5551,565.0,565.0 +50x50_deadends_v8.txt,DFS,1.7707,565.0,565.0 +50x50_deadends_v8.txt,A*,2.3158,565.0,565.0 +50x50_deadends_v9.txt,BFS,0.6693,209.0,209.0 +50x50_deadends_v9.txt,DFS,1.052,209.0,209.0 +50x50_deadends_v9.txt,A*,0.7957,209.0,209.0 diff --git a/skorohodovsa/task_2/practice/results.png b/skorohodovsa/task_2/practice/results.png new file mode 100644 index 0000000..e94ceb8 Binary files /dev/null and b/skorohodovsa/task_2/practice/results.png differ diff --git a/skorohodovsa/task_2/requirements.txt b/skorohodovsa/task_2/requirements.txt new file mode 100644 index 0000000..20ae8cb --- /dev/null +++ b/skorohodovsa/task_2/requirements.txt @@ -0,0 +1,11 @@ +sphinx +myst-parser +sphinxawesome-theme +nbsphinx +myst-nb +tabulate +bibtexparser +pytest +sphinxcontrib-mermaid +matplotlib +pandas \ No newline at end of file diff --git a/skorohodovsa/task_2/source/models/base.py b/skorohodovsa/task_2/source/models/base.py new file mode 100644 index 0000000..1496091 --- /dev/null +++ b/skorohodovsa/task_2/source/models/base.py @@ -0,0 +1,257 @@ +from typing import Optional + +from source.settings import cell_mapping + + +class Cell: + """Представляет одну клетку поля лабиринта. + + Каждая клетка хранит свои координаты и один из четырёх возможных + типов: стена, старт, выход или пустая клетка. Типы взаимоисключают + друг друга: установка одного автоматически сбрасывает остальные. + """ + + def __init__( + self, + x: int, + y: int, + is_wall: bool = False, + is_start: bool = False, + is_exit: bool = False, + ): + """Инициализирует клетку с заданными координатами и типом. + + Args: + x: Координата клетки по оси X. + y: Координата клетки по оси Y. + is_wall: Если True — клетка является стеной. + is_start: Если True — клетка является стартом. + is_exit: Если True — клетка является выходом. + """ + self.x = x + self.y = y + self._is_wall = is_wall + self._is_start = is_start + self._is_exit = is_exit + + def is_possible(self) -> bool: + """Проверяет, можно ли переместиться в эту клетку. + + Returns: + True, если клетка не является стеной, иначе False. + """ + return not self._is_wall + + @property + def is_wall(self) -> bool: + """True, если клетка является стеной.""" + return self._is_wall + + @property + def is_start(self) -> bool: + """True, если клетка является стартовой позицией.""" + return self._is_start + + @property + def is_exit(self) -> bool: + """True, если клетка является выходом из лабиринта.""" + return self._is_exit + + def _clear_flags(self) -> None: + """Сбрасывает все флаги типа клетки в False.""" + self._is_wall = False + self._is_start = False + self._is_exit = False + + @is_wall.setter + def is_wall(self, value: bool) -> None: + """Устанавливает флаг стены, сбрасывая остальные типы при value=True. + + Args: + value: Новое значение флага стены. + """ + if value: + self._clear_flags() + self._is_wall = value + + @is_start.setter + def is_start(self, value: bool) -> None: + """Устанавливает флаг старта, сбрасывая остальные типы при value=True. + + Args: + value: Новое значение флага старта. + """ + if value: + self._clear_flags() + self._is_start = value + + @is_exit.setter + def is_exit(self, value: bool) -> None: + """Устанавливает флаг выхода, сбрасывая остальные типы при value=True. + + Args: + value: Новое значение флага выхода. + """ + if value: + self._clear_flags() + self._is_exit = value + + def _get_type_cell(self) -> str: + """Возвращает символ клетки согласно cell_mapping. + + Returns: + Строковый символ, соответствующий текущему типу клетки. + """ + if self._is_wall: + return cell_mapping["wall"] + if self._is_start: + return cell_mapping["start"] + if self._is_exit: + return cell_mapping["exit"] + return cell_mapping["empty"] + + def __str__(self) -> str: + return self._get_type_cell() + + def __repr__(self) -> str: + return f"Cell(x={self.x}, y={self.y}, '{self._get_type_cell()}')" + + +class Maze: + """Представляет двумерный лабиринт из клеток Cell. + + Лабиринт хранится как список списков клеток. Доступ к отдельным + клеткам и их изменение возможны через индексацию вида maze[row, col]. + """ + + def __init__(self, size: tuple[int, int] = (10, 10)): + """Создаёт пустой лабиринт заданного размера. + + Args: + size: Кортеж (width, height) — ширина и высота лабиринта в клетках. + """ + self._width, self._height = size + self._map: list[list[Cell]] = [ + [Cell(x, y) for x in range(self._width)] for y in range(self._height) + ] + + def _check_point_in_map(self, x: int, y: int) -> bool: + """Проверяет, находится ли точка в границах лабиринта. + + Args: + x: Координата по оси X. + y: Координата по оси Y. + + Returns: + True, если точка (x, y) находится внутри лабиринта. + """ + return 0 <= x < self._width and 0 <= y < self._height + + def get_cell(self, x: int, y: int) -> Optional[Cell]: + """Возвращает клетку по координатам или None, если координаты вне границ. + + Args: + x: Координата по оси X. + y: Координата по оси Y. + + Returns: + Объект Cell, если координаты корректны, иначе None. + """ + if not self._check_point_in_map(x, y): + return None + return self._map[y][x] + + def get_neighbors(self, x: int, y: int) -> Optional[list[Cell]]: + """Возвращает список проходимых соседей клетки (вверх, вправо, вниз, влево). + + Args: + x: Координата клетки по оси X. + y: Координата клетки по оси Y. + + Returns: + Список проходимых соседних клеток, или None если (x, y) вне границ. + """ + if not self._check_point_in_map(x, y): + return None + + deltas = ((0, 1), (1, 0), (0, -1), (-1, 0)) + neighbors = [] + + for dx, dy in deltas: + cell = self.get_cell(x + dx, y + dy) + if cell is not None and cell.is_possible(): + neighbors.append(cell) + + return neighbors + + @property + def start(self) -> Optional[Cell]: + for y in range(self._height): + for x in range(self._width): + if self[y, x].is_start: + return self[y, x] + return None + + @property + def exit(self) -> Optional[Cell]: + for y in range(self._height): + for x in range(self._width): + if self[y, x].is_exit: + return self[y, x] + return None + + @property + def shape(self) -> tuple[int, int]: + return self._height, self._width + + def __getitem__(self, index: tuple[int, int]) -> Cell: + """Возвращает клетку по индексу [row, col]. + + Args: + index: Кортеж (row, col) — строка и столбец. + + Returns: + Объект Cell в позиции (row, col). + + Raises: + IndexError: Если индекс выходит за пределы лабиринта. + """ + row, col = index + if not self._check_point_in_map(col, row): + raise IndexError(f"Индекс ({row}, {col}) выходит за пределы лабиринта") + return self._map[row][col] + + def __setitem__(self, index: tuple[int, int], value: str) -> None: + """Устанавливает тип клетки по индексу [row, col] через символ из cell_mapping. + + Args: + index: Кортеж (row, col) — строка и столбец. + value: Символ типа клетки согласно cell_mapping. + + Raises: + IndexError: Если индекс выходит за пределы лабиринта. + ValueError: Если символ не найден в cell_mapping. + """ + row, col = index + if not self._check_point_in_map(col, row): + raise IndexError(f"Индекс ({row}, {col}) выходит за пределы лабиринта") + + cell = self._map[row][col] + cell_type = next( + (t for t, s in cell_mapping.items() if s == value), + None, + ) + + if cell_type is None: + raise ValueError(f"Символ '{value}' не соответствует ни одному типу клетки") + + if cell_type == "empty": + cell._clear_flags() + else: + setattr(cell, f"is_{cell_type}", True) + + def __str__(self) -> str: + return "\n".join( + "".join(str(self._map[y][x]) for x in range(self._width)) + for y in range(self._height) + ) diff --git a/skorohodovsa/task_2/source/settings.py b/skorohodovsa/task_2/source/settings.py new file mode 100644 index 0000000..0a0f313 --- /dev/null +++ b/skorohodovsa/task_2/source/settings.py @@ -0,0 +1 @@ +cell_mapping = {"wall": "#", "empty": " ", "start": "S", "exit": "E"} diff --git a/skorohodovsa/task_2/source/strategy/__init__.py b/skorohodovsa/task_2/source/strategy/__init__.py new file mode 100644 index 0000000..72c56bf --- /dev/null +++ b/skorohodovsa/task_2/source/strategy/__init__.py @@ -0,0 +1,11 @@ +from source.strategy.algorithms import PathFindingStrategy +from source.strategy.astar import AStarStrategy +from source.strategy.bfs import BFSStrategy +from source.strategy.dfs import DFSStrategy + +__all__ = [ + "PathFindingStrategy", + "BFSStrategy", + "DFSStrategy", + "AStarStrategy", +] diff --git a/skorohodovsa/task_2/source/strategy/algorithms.py b/skorohodovsa/task_2/source/strategy/algorithms.py new file mode 100644 index 0000000..c8b1537 --- /dev/null +++ b/skorohodovsa/task_2/source/strategy/algorithms.py @@ -0,0 +1,68 @@ +from abc import ABC, abstractmethod +from typing import Optional + +from source.models.base import Maze, Cell + + +class PathFindingStrategy(ABC): + """Интерфейс стратегии поиска пути в лабиринте.""" + + @abstractmethod + def find_path( + self, maze: Maze, start: Cell = None, exit: Cell = None + ) -> list[Cell]: + """Найти путь от start до exit. + + Args: + maze: Объект лабиринта. + start: Стартовая клетка. + exit: Целевая клетка. + + Returns: + Список клеток пути (от start до exit включительно). + Пустой список, если путь не найден. + """ + + def _validate( + self, maze: Maze, start: Optional[Cell], exit: Optional[Cell] + ) -> tuple[Optional[Cell], Optional[Cell]]: + """Подставляет start/exit из лабиринта если не переданы явно. + + Raises: + ValueError: Если старт или выход не найдены. + """ + if start is None: + start = maze.start + if exit is None: + exit = maze.exit + + if start is None: + raise ValueError("Стартовая клетка не найдена в лабиринте") + if exit is None: + raise ValueError("Выходная клетка не найдена в лабиринте") + + return start, exit + + def _reconstruct_path( + self, came_from: dict[Cell, Optional[Cell]], end: Cell + ) -> list[Cell]: + """Восстанавливает путь от старта до end, идя по came_from в обратном порядке. + + Args: + came_from: Словарь {клетка: родитель}, где родитель старта = None. + end: Конечная клетка. + + Returns: + Список клеток пути от старта до end включительно. + Пустой список, если end отсутствует в came_from. + """ + if end not in came_from: + return [] + + path: list[Cell] = [] + current: Optional[Cell] = end + while current is not None: + path.append(current) + current = came_from[current] + path.reverse() + return path diff --git a/skorohodovsa/task_2/source/strategy/astar.py b/skorohodovsa/task_2/source/strategy/astar.py new file mode 100644 index 0000000..ead84f6 --- /dev/null +++ b/skorohodovsa/task_2/source/strategy/astar.py @@ -0,0 +1,50 @@ +import heapq +from typing import Optional + +from source.models.base import Cell, Maze +from source.strategy.algorithms import PathFindingStrategy + +# ---------------------------------------------------------------------------- # +# Моя азиатская жена называет меня расистом. Но как я могу # +# быть расистом, если я женился на женщине низшей расы?! # +# ---------------------------------------------------------------------------- # + + +def _manhattan(a: Cell, b: Cell) -> int: + """Манхэттенское расстояние между двумя клетками.""" + return abs(a.x - b.x) + abs(a.y - b.y) + + +class AStarStrategy(PathFindingStrategy): + """Алгоритм A* с манхэттенской эвристикой.""" + + def find_path( + self, maze: Maze, start: Optional[Cell] = None, exit: Optional[Cell] = None + ) -> list[Cell]: + start, exit = self._validate(maze, start, exit) + + g_score: dict[Cell, int] = {start: 0} + came_from: dict[Cell, Optional[Cell]] = {start: None} + + counter = 0 + open_heap: list[tuple[int, int, Cell]] = [ + (_manhattan(start, exit), counter, start) + ] + + while open_heap: + _, _, current = heapq.heappop(open_heap) + + if current is exit: + return self._reconstruct_path(came_from, exit) + + for neighbor in maze.get_neighbors(current.x, current.y): + tentative_g = g_score[current] + 1 + + if tentative_g < g_score.get(neighbor, float("inf")): + g_score[neighbor] = tentative_g + came_from[neighbor] = current + f = tentative_g + _manhattan(neighbor, exit) + counter += 1 + heapq.heappush(open_heap, (f, counter, neighbor)) + + return [] diff --git a/skorohodovsa/task_2/source/strategy/bfs.py b/skorohodovsa/task_2/source/strategy/bfs.py new file mode 100644 index 0000000..b0bba06 --- /dev/null +++ b/skorohodovsa/task_2/source/strategy/bfs.py @@ -0,0 +1,34 @@ +from collections import deque +from typing import Optional + +from source.models.base import Cell, Maze +from source.strategy.algorithms import PathFindingStrategy + + +class BFSStrategy(PathFindingStrategy): + """Поиск в ширину (Breadth-First Search). + + Гарантирует кратчайший путь по количеству шагов. + Сложность: O(V + E) по времени и памяти. + """ + + def find_path( + self, maze: Maze, start: Optional[Cell] = None, exit: Optional[Cell] = None + ) -> list[Cell]: + start, exit = self._validate(maze, start, exit) + + came_from: dict[Cell, Optional[Cell]] = {start: None} + queue: deque[Cell] = deque([start]) + + while queue: + current = queue.popleft() + + if current is exit: + return self._reconstruct_path(came_from, exit) + + for neighbor in maze.get_neighbors(current.x, current.y): + if neighbor not in came_from: + came_from[neighbor] = current + queue.append(neighbor) + + return [] diff --git a/skorohodovsa/task_2/source/strategy/dfs.py b/skorohodovsa/task_2/source/strategy/dfs.py new file mode 100644 index 0000000..8b41845 --- /dev/null +++ b/skorohodovsa/task_2/source/strategy/dfs.py @@ -0,0 +1,38 @@ +from typing import Optional + +from source.models.base import Maze, Cell +from source.strategy.algorithms import PathFindingStrategy + +# ---------------------------------------------------------------------------- # +# Как называется пресмыкающийся, который в прошлом был программистом? # +# ---------------------------------------------------------------------------- # +# крокодил # +# ---------------------------------------------------------------------------- # + + +class DFSStrategy(PathFindingStrategy): + """Поиск в глубину (Depth-First Search). + + Находит путь, но не гарантирует кратчайший. + """ + + def find_path( + self, maze: Maze, start: Optional[Cell] = None, exit: Optional[Cell] = None + ) -> list[Cell]: + start, exit = self._validate(maze, start, exit) + + came_from: dict[Cell, Optional[Cell]] = {start: None} + stack: list[Cell] = [start] + + while stack: + current = stack.pop() + + if current is exit: + return self._reconstruct_path(came_from, exit) + + for neighbor in maze.get_neighbors(current.x, current.y): + if neighbor not in came_from: + came_from[neighbor] = current + stack.append(neighbor) + + return [] diff --git a/skorohodovsa/task_2/source/strategy/solver.py b/skorohodovsa/task_2/source/strategy/solver.py new file mode 100644 index 0000000..ac1379f --- /dev/null +++ b/skorohodovsa/task_2/source/strategy/solver.py @@ -0,0 +1,94 @@ +import time +from dataclasses import dataclass + +from source.models.base import Maze, Cell +from source.strategy import PathFindingStrategy + + +@dataclass +class SearchStats: + """Статистика выполнения поиска пути. + + Attributes: + elapsed_ms: Время выполнения в миллисекундах. + visited_count: Количество посещённых клеток. + path_length: Длина найденного пути (0 если путь не найден). + path: Найденный путь — список клеток от старта до выхода. + """ + + elapsed_ms: float + visited_count: int + path_length: int + path: list[Cell] + + def __str__(self) -> str: + return ( + f"Время: {self.elapsed_ms:.3f} мс | " + f"Посещено клеток: {self.visited_count} | " + f"Длина пути: {self.path_length}" + ) + + +class MazeSolver: + """Оркестратор поиска пути в лабиринте. + + Принимает лабиринт и стратегию поиска, выполняет поиск + и возвращает результат вместе со статистикой выполнения. + + Example: + solver = MazeSolver(maze, BFSStrategy()) + stats = solver.solve() + print(stats) + + solver.set_strategy(AStarStrategy()) + stats = solver.solve() + """ + + def __init__(self, maze: Maze, strategy: PathFindingStrategy) -> None: + """Инициализирует солвер с лабиринтом и стратегией поиска. + + Args: + maze: Объект лабиринта. + strategy: Стратегия поиска пути. + """ + self._maze = maze + self._strategy = strategy + + def set_strategy(self, strategy: PathFindingStrategy) -> None: + """Заменяет текущую стратегию поиска. + + Args: + strategy: Новая стратегия поиска пути. + """ + self._strategy = strategy + + def solve( + self, + start: Cell = None, + exit: Cell = None, + ) -> SearchStats: + """Выполняет поиск пути и собирает статистику. + + Если start или exit не переданы явно, стратегия найдёт + их самостоятельно по флагам is_start / is_exit в лабиринте. + + Args: + start: Стартовая клетка (опционально). + exit: Конечная клетка (опционально). + + Returns: + Объект SearchStats с временем выполнения, количеством + посещённых клеток и длиной найденного пути. + """ + t_start = time.perf_counter() + path = self._strategy.find_path(self._maze, start, exit) + t_end = time.perf_counter() + + elapsed_ms = (t_end - t_start) * 1000 + + return SearchStats( + elapsed_ms=elapsed_ms, + visited_count=len(path), + path_length=len(path), + path=path, + ) diff --git a/skorohodovsa/task_2/source/templates/100x100_spaghetti_v1.txt b/skorohodovsa/task_2/source/templates/100x100_spaghetti_v1.txt new file mode 100644 index 0000000..84fca03 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/100x100_spaghetti_v1.txt @@ -0,0 +1,99 @@ +################################################################################################### +#S # # # # # # # # # # # +### ##### ### #### ### # ### # ### # ####### # ## # # # # # # # ###### ## # # # ### ## # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# ##### # # ## # # ### # ### # ## # # # ##### # ##### ### ## ####### # ##### # ### # # ### # +# # # # # # # # # # # # # # # # # # # # # +# ### # ## ###### # # # # # ### ##### # ## ##### ### # # #### # #### ### ##### ### ## ## # +# # # # # # # # # # # # # # # # # # # # # # # # +# # ### # # # ## # ## # ### # ##### # # # ## ##### ### # # ### # # # # ### # # # ## # # ##### +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# ### # # ##### # # # # ### # # # # # # # # # # ## # #### ### # ### # # ### ### # # # # ## # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# ##### ### # # # # # ##### ### # ## # ### # # # # # # ### ##### ####### # ### ### # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# # ### # # ### # ### # # # # ## # ### ### # ##### # # # ####### # ##### # # # # # ####### # # # +# # # # # # # # # # # # # # # # # # # # +# # ### # #### ### ####### ### ## # ## ## # ######## ####### # ### ######## ### # # ### # +# # # # # # # # # # # # # # # # # +# #### # # ## # ### #### # # #### ##### # # ## ### # # # # #### # ## ##### ### ### # +# # # # # # # # # # # # # # # # # # # # # # # # +# # # ### # # # # ## ### # # # ### # # # # ##### ##### # # ###### # # # ## ## ## #### ### # +# # # # # # # # # # # # # # # # # # # # # # # +### # # ########### # ####### # ## # # # #### # ### ### # # # ### # ### ### # # # # ## #### +# # # # # # # # # # # # # # # # # # # # +# # # # # # # # #### ### # # # # ### # ##### ## ## # # ######### ####### # # ## # ### # +# # # # # # # # # # # # # # # # # # # # # +# ##### # ### # # # ## # # # # ### ##### ## #### # # ###### ## # # # # # ### # ### # ##### # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # ### ######### # # ### # ##### # ####### # # # # # # # # # ##### # ##### # # #### # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # +##### # # ### ## # # ### # # ### # # ### # # # #### ### ##### ### # # # # # ########## # ### # # +# # # # # # # # # # # # # # # # # # # # # # # # +# ##### ### # # # ### # # # # # ### ### ##### # # # ## ########### # # #### # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# ### ### ##### # ### ##### # # # # # # # # ##### # # ####### ### # # # ## # # # # ##### # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # ## ### # ### # # # #### # ## # ##### # # # # # ### # #### ## ##### ####### # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # ## # ### ##### ##### # # # ##### # # # #### # # ###### ## # # # ### # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# # # ####### # # # #### # # # ## # ### # #### # # ##### ## ## # # # # # # ### # # # # ### # +# # # # # # # # # # # # # # # # # # # +### ### # # # # ## ###### ### # #### # # ##### ##### ### ## ## # ######### # # ### # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # ### #### # ### # ### # #### ### # ##### ### ## # ### # ##### ##### # # # ## # +# # # # # # # # # # # # # # # # # # # # # # +# ## # ##### # ## ## ####### ### # ##### ##### # ##### # # # # # ##### # ##### ## # ### # ### +# # # # # # # # # # # # # # # # # # # # +# # ##### ########### # # ######### # # ### # ##### ### ### ### ##### ### ######### ### # ## # +# # # # # # # # # # # # # # # # # # # # # # +# ## # ### # # ## #### # # ##### ## # ## # #### # # # # ### # # # # ### # # # # # # ##### +# # # # # # # # # # # # # # # # # # # # # # # # # # # # +### # ### # # ### # ##### ### # # # # # # ## ## # # # ## ###### # ## # ########## # ####### # +# # # # # # # # # # # # # # # # # # # # # # # +# ##### ##### # ####### ### # # # ### ## # ###### # ### # # ### # ### ### # ### ### ### # # +# # # # # # # # # # # # # # # # # # # # # # # +# # # # # ## ## #### # ## ## # # # # ######### ### # # # ### # # # # # # ## ## # ### ### # +# # # # # # # # # # # # # # # # # # # # # # +# ### # ##### ## ### ## ### # ### ##### # # # #### ##### # ### ######### # # #### # # # +# # # # # # # # # # # # # # # # # # # # # # +### # ## ## ### # # ## #### # ### # # # # # # ##### # ### ### ## #### ## ### ## # # # +# # # # # # # # # # # # # # # # # # # # # +# ### # # # ###### # # ####### ### ### # # ##### ##### ###### # # # ##### ##### # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # +# ### # ### ### ### ######### ### # # ## #### ### # #### ### ##### # ######## # ####### +# # # # # # # # # # # # # # # # # # # # +# # # ### ### # # # # ## ## # ### # ## # # ##### # ### ## ####### # ## ### # ### # ## # # +# # # # # # # # # # # # # # # # # # # # # # # # +### ### # # ######### # ### # ### ####### ## # # ### # # # # ###### # ### # # # ## # ##### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# ### # # # # # # ##### # ######### #### ## ## ##### ## ## ##### #### # # ### # ### # +# # # # # # # # # # # # # # # # # # +# ### ##### # # ##### ####### # # # ##### # ##### ### ## ## # ### ### # ### ## #### ####### ### +# # # # # # # # # # # # # # # # # # # # # # # # +# # ## # #### #### # # # ### # ## #### # ## ## # # ##### # ##### # # # # # ### ##### # # # # # +# # # # # # # # # # # # # # # # # # # # # # # +# ### ### # # ### # # ##### ### ### ## # # # ### # # # ####### # ### ##### ## # # # +# # # # # # # # # # # # # # # # # +# # ## ######## ### ####### ###### ##### # ############### # # ### # ##### # ### ### ## ## # # +# # # # # # # # # # # # # # # # # # +# # ### # ##### # # # # # ## # # ## # # # # ######### # # ##### # # ####### ####### # +# # # # # # # # # # # # # # # # # # # # # # # +### # # # ##### # # # ### # # # # ####### ### # ## ## ### ## ###### # ### # # # ## ## # +# # # # # # # # # # # # # # # # # # # # +# # ## ## ### ######### ####### # ######### ### # ### # # # # # # # # ### #### ## ### +# # # # # # # # # # # # # # # # # # # # # # # # +# ### # # # # ### ##### ########## ## ### # # ### ##### # ### #### ##### ####### # # # ### # +# # # # # # # # # # # # # # # # # # # # # # # +# # # # ### # # # ### # ###### # # ##### ## # ######## # ### # #### #### ### # # # ### # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # ### ## # # # # # ## # # ### # # # #### # # # # ## #### # # # # ## ### # ## ## # ### # +# # # # # # # # # # # # # # # # # # # # # # # +### # # ########## # ####### # ### ### # ### ##### # # # ## ### ### ### # # ##### ### # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # ### # # # ## # # # # # # # # ### ##### ##### # ### ## # ####### # ### # ### # # ### +# # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # ##### # #### # ## ## ##### # ######### # ## ### # #### ## # # # # # ######### # +# # # # # # # # # # E# +################################################################################################### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/100x100_spaghetti_v10.txt b/skorohodovsa/task_2/source/templates/100x100_spaghetti_v10.txt new file mode 100644 index 0000000..87f9068 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/100x100_spaghetti_v10.txt @@ -0,0 +1,99 @@ +################################################################################################### +#S# # # # # # # # # # # # # # # +# # # # # # # # ### # ### # # # # # ## ##### # # ## # # ### # # ### # # ## # ### ### # # # +# # # # # # # # # # # # # # # # # # # # # # # +##### ### ##### ## # # ### # # # # # # # # # # # # ## # # # #### ### # # ## ### ### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # +# ##### ###### ## # # ### ##### # # # ### ### ##### # # # # # ## # # ### ### # # ### # # # +# # # # # # # # # # # # # # # # # # # # # # # # +# # # ### # # ### ##### # ### ## # # # # # # ### ####### # ####### # # ## ## ###### #### ## ## # +# # # # # # # # # # # # # # # # # # # # # # # +### ##### # # # ### # # # # ### # #### # ### ####### # #### #### # ### # # # #### # ### +# # # # # # # # # # # # # # # # # # # # # # # +# ### # ## ### # ### # # ####### ### ### # ######### ### # # # ### # # ### # ### ######### # # # +# # # # # # # # # # # # # # # # # # # # # # # +# # # #### # # ### # # #### # # # # # ####### # # ## ## ### # # ### ##### # # ## ### ### # +# # # # # # # # # # # # # # # # # # # # # # +# ### # # ##### ### # # ###### ## # ## ## # # ### # # ### # # ####### # ### # ## # ### # ### # +# # # # # # # # # # # # # # # # # # # # +# # ## ##### # # # # ## ## # # ####### # ### ##### # # ####### ## # # # ###### #### # ### # # +# # # # # # # # # # # # # # # # # # +##### ### ##### ##### ### ##### # ## ## # # ##### # ##### ####### # #### ### # ## ## # # # # +# # # # # # # # # # # # # # # # # # # # # # # # +## # ####### ### # # # # # #### # # ### ##### #### # #### ## # # # # ## # ### ### # # ### +# # # # # # # # # # # # # # # # # # +# # # ## ####### ### # ## # # # ## # # ####### # # # ### ### # # # # # # ##### # # # ##### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # ### ##### #### ## # # # ######### # ##### ### ### # ## ## ### # # ###### # # +# # # # # # # # # # # # # # # # # # # # # +# ### #### # ### # # ### # # ##### ## ### # ## ######### ### #### ### # # # ##### # ##### # +# # # # # # # # # # # # # # # # # # # # # +# # ### # ##### ### ### # ####### ### # ## # ## # # # ##### # ### # #### #### ### ### ## #### +# # # # # # # # # # # # # # # # # # # # # # # +# ####### # # ### # # ##### # ### # # # #### ##### # # ## ##### # # # # # ### ### # # ### # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# # # ## # # # ### ### # # # # ### # ##### ### # # # ## # # # # # ########### ### ### # # # +# # # # # # # # # # # # # # # # # # # # # +# # ##### #### # ### # ######### ### ## ###### ######### ### ##### ## # ### # # ##### #### ##### +# # # # # # # # # # # # # # # # # +# # # ## ### ##### # # ### ### ### ### # # ####### ####### # # ##### # # ### ### ##### ## # # +# # # # # # # # # # # # # # # # # # # # # # +# ## ##### ### # ## # # # # ##### # # #### # ####### ##### # # ##### # ## ## # # ### ## # +# # # # # # # # # # # # # # # # # # # # # # # +### ## # ##### ##### # ## ### ## # # # # # # ### ### # ### ### # ### # # # # ## # ### ##### # +# # # # # # # # # # # # # # # # # # # # # # +# ### # ### ### ## # # # #### # #### # ####### ### # ### #### ## # # #### # ## # ### # # +# # # # # # # # # # # # # # # # # +### # ### # # # #### # ######### # # ### # # # # ### ##### ###### ## # # #### #### ### ## # +# # # # # # # # # # # # # # # # +# ### # ### ##### # # ######### # ### ### ### ### # ####### ##### ####### # # ##### # #### ## +# # # # # # # # # # # # # # # # +### # # # ########### ### # # # ##### ##### ## #### # ###### #### #### # #### # ######### # # +# # # # # # # # # # # # # # # # # # # # # +# ### ####### ##### # # # # ## ### #### ## # # ### ##### # ### # # # # ###### ## # # #### # # +# # # # # # # # # # # # # # # # # # # # # # # +# # ### ####### # # # ### ### ########### # # # # ## # ## # ###### ### # # ### # # # ## # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # ##### # ### ### # ### ### ### # # # ##### # # # # ### # ### ## ########### # ### # +# # # # # # # # # # # # # # # # # # # # # # +# # # # # ### # ### # ####### # # # # ## ### ### # ### # ### # ## # # # # # # ## ### ### # +# # # # # # # # # # # # # # # # # # # # # # # # # # +### ### # # # ##### ##### # # ### ##### ### # ### # ##### # ## ####### # # # # # # #### # +# # # # # # # # # # # # # # # # # # # # # # # # # +# ## ### ####### # ## ## ##### # # # ### ### ### ### # # # # # # ##### # ### ##### # # # ##### +# # # # # # # # # # # # # # # # # # # # # # # # # +# # ### # # ####### #### # ##### # ## # ## # #### #### # # ### ### ### # # # # ## ###### # +# # # # # # # # # # # # # # # # # # # # # +# ### # # ### # # # # ##### # ##### # # ######## ##### # # # ### # ##### ####### # ### # ## # # +# # # # # # # # # # # # # # # # # # # # # # +# #### # # # ## ## # ##### # ## # # # # # # # ##### ### #### # # # # ##### # # # ### # ### # # # +# # # # # # # # # # # # # # # # # # # # # # # +## ## # ### # # ### ### # ##### ### # # # ##### ## # # # ### ### # ##### ####### ##### #### # +# # # # # # # # # # # # # # # # # # # # # +# #### # ## ## #### ##### # ### # ### ##### # ## # # # # ## ### ####### ####### ### # ## +# # # # # # # # # # # # # # # # # # # # # +# # ### #### # # ## ##### ### # # # ### # # ### # ####### # ### # ## #### # ## # # # +# # # # # # # # # # # # # # # # # # # # # # # +# ## ### # # ####### # # ## # # ### ### # ### # # ##### ## # # ## # # # ## # # #### # # +# # # # # # # # # # # # # # # # # # # # # # +# ### # # ##### # # # # # # # ### ### ### ########## ##### ### # ##### # # ##### ####### #### # +# # # # # # # # # # # # # # # # # # # # # # +##### ## ## # # # ## ### # # # ####### # ## # # # ######## ### ## #### # ##### ##### # # # +# # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # ## ## ####### # ## ### ########### ### ### # # #### ### # # #### # ## # ### # # # +# # # # # # # # # # # # # # # # # # # +# ### # ##### # # ### ### # ### # # ### # ###### # ## ### # ## ## # # ### # ### ### # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # ### ### # # # # # # # # # # # # # # ### ##### # # ### ### ## #### # # ### #### # # ## ## ### +# # # # # # # # # # # # # # # # # # # # # # # # # +# # ##### ## #### ##### # ## ### # # # ##### # # # ##### # # # # ######### # # ### ##### # ### # +# # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # ##### # # ## # # ## # # # # ## # ## # # # # # # ### ## ### # #### # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# ##### # # # # ##### # ### ##### # # # # ### ### # ### # ##### # # # ### # # # ##### ##### ## # +# # # # # # # # # # # # # # # # # # # # # # # +### # ##### ### ### # # # # ### # # ## ### ####### ######## # # ### ## ### ### ####### # +# # # # # # # # # # # # # # # # # # # # # # # # +# ###### ## # # # # ## ###### ## ## ### ### ### # ### # # # # ####### # ### # ### #### # # +# # # # # # # # # E# +################################################################################################### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/100x100_spaghetti_v2.txt b/skorohodovsa/task_2/source/templates/100x100_spaghetti_v2.txt new file mode 100644 index 0000000..9e2fa3f --- /dev/null +++ b/skorohodovsa/task_2/source/templates/100x100_spaghetti_v2.txt @@ -0,0 +1,99 @@ +################################################################################################### +#S# # # # # # # # # # # +# # ### # # ### ### # ## ## ####### # ########### # ### # ## #### # ### # # ### # # ### # +# # # # # # # # # # # # # # # # # # # +# # ### ## # ## # # ###### #### ## ###### ### ##### ## #### ### # ## # # ####### # ##### ### +# # # # # # # # # # # # # # # # # # # # # +# # ### ### #### ## ### #### ### # # ### ### ##### #### # # ##### ### # # ### ####### # ##### # +# # # # # # # # # # # # # # # # # # # # # +##### ##### # # # # ### ### #### ##### # ####### ## ## ####### # ## # #### ## # ####### # # # +# # # # # # # # # # # # # # # # # # # # +# #### ## ## ########## # # # ### ##### # ######## ## ## ###### #### ## #### ### # # ### ## +# # # # # # # # # # # # # # # # +# ### ### # ####### # ## # ## # # # # # # # # # ###### # # # ### # # ##### # # # #### # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # ### # #### # #### # # #### # ##### # # # ### # # # # # ############# # # ## ##### # # # +# # # # # # # # # # # # # # # # # # # # +### # ## ### # ### ## ##### ### # ### # # ## #### # ####### # ### # ### # # # # ## # # +# # # # # # # # # # # # # # # # # # # # # # # # +# # #### ## ##### # ## # ##### # # ### # ### # # # # ####### ##### ### # # ## #### ##### +# # # # # # # # # # # # # # # # # # # # # # # # +# # ######### ## #### ### # ##### ################# # # # ### #### # # ### # ## # ### # +# # # # # # # # # # # # # # # # # # # # # # # +# ### ## # ## # # ### # # # ##### ### # ### # # ### # #### ## # # ### # ###### ### # ### # # +# # # # # # # # # # # # # # # # # # # # # # +# # ### ### # # # # ### # ## ## # ##### # # # # ### # ##### ########### # ######## ####### # +# # # # # # # # # # # # # # # # # # # +# ### # # # # ### ##### # # ### # ## # ### ### # # ## #### # # # # ##### # ### # # #### # # # # # +# # # # # # # # # # # # # # # # # +####### ## # ###### ### # # ## ### #### ### # #### ## # ### # # # ### # #### ### # ### # # # # # +# # # # # # # # # # # # # # # # # # # # +###### ## ## ### # ## # #### # # ##### # ## ## # ##### ### ### ##### # ### # # # # ### # # +# # # # # # # # # # # # # # # # # # # +# ####### # # # ## # # ### # ####### ## ### ## ### # # # # ## # # ### # ####### # # # # +# # # # # # # # # # # # # # # # # # # +# # ## ### # ## ## # # ##### ### ## ## #### # # ##### # # # ### ## ## ### ## # ### ## # ##### # +# # # # # # # # # # # # # # # # # # # # # # +# # # ######### ### # # ## # # ### ### ## ### ### # ##### # ## # ### # # ############ ### # ### +# # # # # # # # # # # # # # # # # # # +# # #### ## # ### #### ### ## #### ## # # ### # # ## # # ### ### ## # ###### ### ### # # +# # # # # # # # # # # # # # # # # # # # # +# ### ## #### # # ## ########### # # # # # ####### ## # ##### # # # # # #### # # # ## # +# # # # # # # # # # # # # # # # # # # # +# ### ### # # ##### # # ### # ## ## # # ### ### ### ### # ### # # # #### ## # #### # ##### ### # +# # # # # # # # # # # # # # # # # # +# # ### # ## ## # # ### ####### ### # # ## ## # # ########### # # ########### # # # ### ##### # +# # # # # # # # # # # # # # # # # # # +# ##### ## #### # #### ####### # # ## ### ##### # ##### ##### ### # ######### # ###### # # ##### +# # # # # # # # # # # # # # # +# # # ### ### ##### # ##### ### #### ## ## # # ##### ######### # ############# # ##### ### +# # # # # # # # # # # # # # # # # # # # +##### # ######### # ### # # ### # # # ###### # ### ### # # ## ########## #### # ## ##### ## # +# # # # # # # # # # # # # # # # # # # # # +### ###### # ### # ### # ## # #### ### # ### # # ### ### # # ### # # ### ####### ### # # +# # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # ####### # ### ##### # # # ##### ### ## ##### ##### ### ### ### # # # ### # ## # +# # # # # # # # # # # # # # # # # # # # # +# ### # # ### ###### ### ###### ####### # # # ### ####### # #### ### # ##### ## # ####### ### +# # # # # # # # # # # # # # # # # # # # # # +# ##### # # ### # # ### # ## # # # # ##### # ### ### ####### # ###### #### ## # #### ### # # # # +# # # # # # # # # # # # # # # # # # # # # # +### # # # # # ## #### ### # # # ##### ### ## #### ### # # ### # ### ##### ## # # ####### # +# # # # # # # # # # # # # # # # # # # # # # +# # # ## # ### # ##### # # # # # ##### ################### # # ##### ## # # ### # # # ### ####### +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # ### # # ### # # # # # ### ### # ##### # ## # ##### # ## # # # ## ## # ### # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# # # ### # # ## ### ## # ## # # # # # # ##### ######### # ## ## # ### ##### ### # +# # # # # # # # # # # # # # # # # # # # +# ### # # # ## ## # ## ### # ###### ## ## # ####### # # ### #### # # # ### ### #### # # ##### +# # # # # # # # # # # # # # # # +# #### # ##### ### ## # #### ### ### # ### ######## # ## # # # # # # # ### ### ######## ## # +# # # # # # # # # # # # # # # # # # # # # # +# ## ### # # # ### # #### # ####### # # ## # ### ####### ### # ### # # # # ### # ### # # # +# # # # # # # # # # # # # # # # # # # # # # # +# # ### ##### ##### #### # # ######### # # # ### ####### # # ###### # # ####### ## # # ### # +# # # # # # # # # # # # # # # # # # # # # +# ## ### # ### # # # ### # # ### #### ######## # # ### # ### # # # # ### # ### ### ##### ##### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # #### # # ### ##### # ### #### ### # ### # # ### # ## # ### # ######### # ## #### +# # # # # # # # # # # # # # # # # # # # # # # +# # ### # ### # ######### # # ### # # # # # ### #### ## # # # #### #### ### # ###### # # # +# # # # # # # # # # # # # # # # # # # # # # # # +# ##### ### # # ### ### ##### # # # ######## # # # # # ####### # ##### # ### ### # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # ##### ##### ### ## ## # # # # # # ## # ### ### # ########### # ##### ## ### ### # # +# # # # # # # # # # # # # # # # # # # # # +# # # # ##### # # ## # #### # # ###### # ## #### ### ### # # # # # ### # # # # # # ### ### # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # ##### ## ## # # #### # # # ##### # ### # # # ### # # # ## #### # # ### # ### ### # +# # # # # # # # # # # # # # # # # # # # # # # # +# ### #### ### ## ### # ### ### #### # # # ## #### # ### # # ## ### ########## ## ### # # # ## +# # # # # # # # # # # # # # # # # # # # # +# # # # # # # ### ### # ### # ## # ## #### # ### # ### # ### # ### ### # # # ### ## # ## ## +# # # # # # # # # # # # # # # # # # # # # # +# #### # # # # #### # ### # ####### # # ### #### # # # ## ### # # ### # ## # ### # ### # +# # # # # # # # # # # # # # # # # # # # # # # # # +# ### ### # ##### # ### # # # ### # # # ## # # # ### # # # # ###### #### ### ## ### # ### ##### # +# # # # # # # # # E# +################################################################################################### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/100x100_spaghetti_v3.txt b/skorohodovsa/task_2/source/templates/100x100_spaghetti_v3.txt new file mode 100644 index 0000000..38c0034 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/100x100_spaghetti_v3.txt @@ -0,0 +1,99 @@ +################################################################################################### +#S # # # # # # # # # # # # +##### # # ### ####### ### # # ### ## # # # # ### # # # ## # # # # ##### # # # ### ## ### # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # ###### # # # ###### # # # # #### # # # ### # # # # # # ### ##### # # ############# # # # +# # # # # # # # # # # # # # # # # # # # +# ### ## # #### ### # # # # # # # # ##### # #### ##### # # # # ### ### ### # ## ### ##### # +# # # # # # # # # # # # # # # # # # # # # # +# # ### ##### ## ### # # #### #### # # ## ###### ### # # # # ##### ### # ## ####### # # ### # +# # # # # # # # # # # # # # # # # # # # # # # # +##### # # ######### # ##### ##### # ## ### # ##### ####### # # # # # # # #### ## # # # ### # # +# # # # # # # # # # # # # # # # # # # # # +# # # ### # ## # ## ####### # # # ##### # ### ####### ## ### ##### # ## # # ###### # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# ## # # # ### # #### # ### ##### # ### ### # # ##### # # ### ######### ## ### # # ##### # +# # # # # # # # # # # # # # # # # # # # # # # +# # # ## # # ### ####### ### # # ## ##### ## ## # ## # # # # ## # # # # ## ### ### +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # +### # ### ### # # ### ### # # ### # #### ### # # ##### ### ######### # # # # # # # ## # # # +# # # # # # # # # # # # # # # # # # # # # # # +# # # # # # ####### # # #### # # ## # ####### # ### ### # # # ####### ### ### ## # # ##### # +# # # # # # # # # # # # # # # # # # # # # +# # #### ## ## ### # ### # # #### ## #### # ##### ##### ### # # ## ## # ### # ##### # # +# # # # # # # # # # # # # # # # # # # # # # +# ####### # # ## # ### ### # # # ## # ## # ## ## # ##### ### ## # ### ### # ### ###### # ### +# # # # # # # # # # # # # # # # # # # +# # # ######### ##### # # # # # #### ## ##### ####### # ##### # ## ##### ### ### ####### # +# # # # # # # # # # # # # # # # # +# # # # # ### ######### ### # ## # ### # # ### # ####### ##### ### ### ### # # ### # ### # +# # # # # # # # # # # # # # # # # # # # # # # # # +## # # ### ### # ### # ##### ## ## # # ########### # # # ####### # # ## # # # # ### ##### # ## # +# # # # # # # # # # # # # # # # # # # # # # +##### # ### ####### # ### ## ##### # # # ##### # # # ## # # ### # #### # ######## # +# # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # ### ##### # # # # # ## # # # # # # # #### ### # # ### # ### ### ##### # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# ### # # # ##### # # ## ##### ######### # # ##### # ### # # ##### #### ## # ####### # # # +# # # # # # # # # # # # # # # # # # # # +# # ## ## # ## # ### # # # # ##### ##### # ### ## ### # ### # # # ##### # # ##### # # ## # ### # +# # # # # # # # # # # # # # # # # # # # # +# #### # # #### ### # # # ### # ### ### ## # # ######### # ## # ##### # ##### ## # # # #### # +# # # # # # # # # # # # # # # # # # # # # # # # +# # ## ## ## # ## # ##### # ### ### ## ## # # ### # ### # # # ### # ### # # ####### # ### +# # # # # # # # # # # # # # # # # # # # # # +# ## ## # # # ####### ### # ### # ####### # #### # # # # # # # # ### # ### # ##### # # # ### # +# # # # # # # # # # # # # # # # # # # +## #### ########### # # # # # ##### # #### ## ########### # # # ### # ### ### ## ## ##### # # # +# # # # # # # # # # # # # # # # # +# # # ### # ### # # # ### # ## ## ### # ### ### # # ### ### #### ## ### # ### # # ### # +# # # # # # # # # # # # # # # # # # # # # # +# # ### ##### # # ### # ####### # # # # # # # # ### ### # ## # ##### ### ### #### # # # ### +# # # # # # # # # # # # # # # # # # # +# ### # # # # ## ## ### # # ##### ########### # ### ##### ### # ### # # ### ## ## # # ### # +# # # # # # # # # # # # # # # # # # # # # # +# # ## # # # # ### # # # #### ## ############# # ### # # # # ### ### # ### # # # # ### # # # # # +# # # # # # # # # # # # # # # # # # # # # # +# # # # ### ### ### # ### # # #### ## # ### # ### # ## ### # #### #### ### ### ##### ### # +# # # # # # # # # # # # # # # # # # # # # # # +# # # ####### ### ##### # # # ### # ## ### # # # ### ##### ##### # ### # # ### # # ####### # +# # # # # # # # # # # # # # # # # # # # # # # +## # # # # # # # # ### ### ## ### # ##### # # ## ###### ## ## # ##### ### # ####### # # +# # # # # # # # # # # # # # # # # # # # # # +# ###### ### #### ## ####### ## ## ### ### # ### ### # # ##### # # # # ### # ### ### # ### # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# ### # # # # ## ### ### # ### ######## ## # # # # ## ### ###### ## ####### # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# ##### # ### # ## # ###### # # # # # # #### ## # ### # # ### ### # ###### ## ### # +# # # # # # # # # # # # # # # # # # # # # # # # +# ####### # ### # ##### ##### # ############# # ############ # # # ### # # # ## # ## # # # +# # # # # # # # # # # # # # # # # # # +# ############ # # # # ##### # ### # ## ##### ### # ##### #### # # ##### ### ##### # # # # +# # # # # # # # # # # # # # # # # # # # # # +# ### # ### # # # # # ## ### # # # # # # # # # ### # ## #### # #### # #### # ### #### ### # +# # # # # # # # # # # # # # # # # # # # # # # # # +# ###### ### # ### # # # # # ### ### # #### ### ## # ####### # #### # ##### ##### # # #### # +# # # # # # # # # # # # # # # # # # # # # # +### # # ## ######### # ## # # ##### # ### # # # # # # # # #### ## # #### # ## # ### # # # +# # # # # # # # # # # # # # # # # # # # # # # +# ### ### # ### ### # ## ## ### # # ########### ####### #### # ###### ##### # # # ### ## ## +# # # # # # # # # # # # # # # # # # +# ### # # ##### ### ## ####### ### ####### ### ##### ### ###### ###### # # # # ### # # ## # +# # # # # # # # # # # # # # # # # # # +### ### ##### # # # ######### # # ### # # ## ### ### # ### # # ##### # ##### ### # ### # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# ##### # # # # ## ##### # ####### # ### # ###### # # # # # # ## #### # # # # ##### # ## # +# # # # # # # # # # # # # # # # # # # # # # # # +# ### ### ##### # # # # # # # #### ## ### ##### ### # ## # ##### ## # ## ##### # # # ### # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # ### # # # # # # ### # ###### # ### #### # ### # ##### # ##### ### # # # ### ### ### # # # +# # # # # # # # # # # # # # # # # # # # # # # # +### # # ### ## # # # ### # ##### # ## #### ### ###### # # ## # ###### ## ### # # ### ### # # # # +# # # # # # # # # # # # # # # # # # # # # # # +# ###### ### # # ### # ## ##### # # ### # # # ## # # ##### ########### ##### ##### # # ## ## ### +# # # # # # # # # # # # # # # # # # # # # # +# # # ### # # ######### # ## ## ### # # # ####### #### #### ## ######## ####### ### ### # ### # +# # # # # # # # # # # # # # # # # # # +### ## # # ### # ## ### ###### # # ### ##### ### # #### # ## # # # ####### # ### # ## ## # +# # # # # # # # # # # E# +################################################################################################### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/100x100_spaghetti_v4.txt b/skorohodovsa/task_2/source/templates/100x100_spaghetti_v4.txt new file mode 100644 index 0000000..ce3c46b --- /dev/null +++ b/skorohodovsa/task_2/source/templates/100x100_spaghetti_v4.txt @@ -0,0 +1,99 @@ +################################################################################################### +#S # # # # # # # # # +# ######## # ##### ### ## #### # # ### ##### # ### # ### # ### ##### # # # ##### # ##### # ### # +# # # # # # # # # # # # # # # # # # # # # # # # +# ####### ##### # ### ## ## ##### ####### ### ##### # # ##### # # # # # ### ## ##### ### +# # # # # # # # # # # # # # # # # # # # # # +# # # # ## ######## # ######## ### # # ### # ### # # ##### # # ##### ### ### # ## # ### # +# # # # # # # # # # # # # # # # # # # +# ### # # ## ## ### # # # # #### #### # # # # # ####### # # ##### ########### # ### # # # ## # +# # # # # # # # # # # # # # # # # # # # # +# ##### ## # # # ##### # # ####### ### #### # ### # # # # # ### ## ######## # # # # # ## # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# ### ### # # ##### # ######### # # # ## ##### # # # # #### ###### # ## # ## # ### # # ### +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # ## ## ## #### ####### ##### # # # # # # # # ######### ##### # # # ### # # # # # ### # +# # # # # # # # # # # # # # # # # # # # # # # # +# ### ### ### # ### ## # # # # # ###### ## # # ### ### # ### # # # ### # # ## ## ### # +# # # # # # # # # # # # # # # # # # # # # # +# ##### ######### ### ## ###### ## # # ##### ##### # ### ##### # ### ## # ##### # # ## +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # #### # ## # # # # # # ### ### # # # # # ####### ### # ### ## #### ### # # ## ########## # +# # # # # # # # # # # # # # # # # # # # # # # # +# # # ######### ### # # ### ##### ## # #### ## # ##### # # ### ##### ### # ##### # ##### ### # +# # # # # # # # # # # # # # # # # # # +### ####### # # ##### # ##### # #### ########### # # # # #### ## # ### # # #### # # # ### # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # ####### # # # ### # ## ### # ##### ### # # # ### # # # ### ##### #### # ### # # # # +# # # # # # # # # # # # # # # # # # # # # # # +# # ### ######### ################# ## ### # # ### # ######## ### # ##### ## # ## ## # # +# # # # # # # # # # # # # # # # # # # +# ### ### # ####### ### #### # # # ### # ### # ### ## ### # ### ### ### # # # # ### # # # +# # # # # # # # # # # # # # # # # # # +### # ####### # # # # # ### ##### # ### # #### # ## ######### ### ####### #### ### # ##### +# # # # # # # # # # # # # # # # # # # # # # # +# # # # ##### ### # ## # # # ## ### ### ## ###### ### # # # ### # # # ## # ### # # # +# # # # # # # # # # # # # # # # # # # # # # # # +# ## ####### # # # # # # # ### ### ## ## # ######## # # ### # ## ## ##### # # # ### # ### # # +# # # # # # # # # # # # # # # # # # # # # # # +# ######### # ### # ### # # #### ## # #### #### ## # ##### ## ######### # ### # ### ### ### # +# # # # # # # # # # # # # # # # # +## ### # ## ##### ##### ##### # # ## ## # # # ##### ##### # # ####### # # # # ## ## #### ### +# # # # # # # # # # # # # # # # # # # +# ### # ### # ##### ##### # ## # # # ##### # # # # ## ### ### ## # ### ### ## # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # ######### # # ### # # # ## ## # # ## ## ## ##### ## ############# # ##### # ### # +# # # # # # # # # # # # # # # # # # # # # +# # # #### ### # # ####### # ### ###### # #### # # # # #### # # # ###### ## ##### # ### # # # +# # # # # # # # # # # # # # # # # # # +# # # # # # # ### #### # ### # # ######## ### ### ### ### ## # # ### # ### ##### # # ### # +# # # # # # # # # # # # # # # # # # # # # # +# #### # ### # ## ## # # # # ##### # ##### # ### # ### # # # ### ### # ### ##### ## ### # # +# # # # # # # # # # # # # # # # # # # +# ##### ## # # # # # ## ### # ## ## # # # # ### # # ### ### ### ######### # # ###### # # # +# # # # # # # # # # # # # # # # # # # # # # +# ### ### ### ##### ##### ## ##### # # ### ##### ##### # ### #### # ### ##### ### ### # ##### # # +# # # # # # # # # # # # # # # # # # # # # +## ### # ## # # # # ## # # ## ##### # # ## ######### # # # ### ## #### # # # ## # ## ## +# # # # # # # # # # # # # # # # # # # # # # # +# ### ### # ## ## ### # # ### # ########### # # # ##### # # # ### # # ### ##### ### # # ### # # +# # # # # # # # # # # # # # # # # # # # # # # +### ##### ## # # ## #### ### # ### # # # # ## ## # # # ###### # # ### # ## ## ## ### ## # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# ### ### # ### ##### ##### # ###### # # ###### # #### ## ### # # # # ### ## #### ##### # +# # # # # # # # # # # # # # # # # # # # # +# ### ### # # # # # ### # # # ##### # # ###### #### ##### # # ## # ### # ##### # ### ##### +# # # # # # # # # # # # # # # # +# ### ########### ##### # # ## ## ### # ### # ### ## ### ### # ######### # ### ####### ### # # +# # # # # # # # # # # # # # # # +# ### # ##### # # # ### ### ### ### ##### ### ### ###### ###### #### ## # # ### # ### # ###### # +# # # # # # # # # # # # # # # # # # # +# # #### # # ##### # #### ### # # ##### # ### ##### ####### # # ## # ####### ######## ## # # +# # # # # # # # # # # # # # # # # # # # +# # ####### ### ##### ### # # # ### ######## ## # ### # ###### # ####### ### ### # ## # ### +# # # # # # # # # # # # # # # # # # # # # # +####### ##### # # ##### ####### ########## # ### # # # # ### ####### ## # # ### # # ##### # # # +# # # # # # # # # # # # # # # # # # # # # +# ####### # ### # ### #### # ### ###### # ### # # ### ### # ### ############# ### # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # +# ### # #### ##### # # # ######### ### ### # ####### # # # # ##### ## ## ## # # ## # ## # # # # +# # # # # # # # # # # # # # # # # # # # # # # +# # # ## ### ##### ### # # ### # ## #### ### # ##### ### # # # ### # #### # ## ### # # # ### # +# # # # # # # # # # # # # # # # # # # # # # # # +### ## # # # ## # # # # ####### ### ##### # ### # ##### # # # # # # # # # # # # ### # ### +# # # # # # # # # # # # # # # # # # # # # # # # # # +# ## # # ## # # ## # ## ### # ### # #### #### # # ####### ### # ### # ##### ##### # # +# # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # ### ### ## # # # # # # ## # ####### ## # # # # #### # # ##### ### ### # ### # +# # # # # # # # # # # # # # # # # # # # # # # # +####### # #### ##### # # # # ## ### # # # ### ### ## # # # ### # ## ##### # # # # ####### # +# # # # # # # # # # # # # # # # # # # # # +# # # ### ## # ## #### # # # ## # # ### # # # # # ####### ## ## ### # # ### # # # ### # +# # # # # # # # # # # # # # # # # # # # # # # # # +# ##### ### # # ## ## # ####### # # # # # # #### ## # # ### # ## # # # # ## ## # # # # +# # # # # # # # # # # # # # # # # # +# ### # # # ## ####### # # ### # # # # # # # #### ## ### # ## #### ##### # ## ## ### # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # ### ### ### # # ####### # # # ## ### # # ### # # # ### ### ######### ### # # ###### # +# # # # # # # # # # # E# +################################################################################################### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/100x100_spaghetti_v5.txt b/skorohodovsa/task_2/source/templates/100x100_spaghetti_v5.txt new file mode 100644 index 0000000..26576ef --- /dev/null +++ b/skorohodovsa/task_2/source/templates/100x100_spaghetti_v5.txt @@ -0,0 +1,99 @@ +################################################################################################### +#S # # # # # # # # +##### # ### #### ## ### ###### ### ### # # ## # # # ## ## ### ## # # # ### ##### # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # ### ### # ### ### # #### # ### ### # # ### ### # # ##### ### # # ## # # #### ## # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# #### # ### ##### # # ## ## # # # ### ##### # ##### # ### # ##### # ###### #### # # # # ### # # +# # # # # # # # # # # # # # # # # # # # # +# # # ## ## #### ####### # ### # #### # ## # ## #### ## # ### ####### # # ##### ##### +# # # # # # # # # # # # # # # # # # # # # # # +# # # # # # ### # # # ## # # # # ### ## # ### # ######## ##### ### # ## # # ##### # # # # ##### +# # # # # # # # # # # # # # # # # # # # +# ##### # ### ############# #### ### ##### # # # ### # ## # # # ## #### ### # # # ##### ## # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# # # ### # # # # ##### # #### # # # # ### # ### # ## ## ## # # #### ### # # # # ### # +# # # # # # # # # # # # # # # # # # # # # # # # +# # # ### # # ####### ####### # ### # ## # # # ## ##### # ####### ### # ### ### # # ####### # +# # # # # # # # # # # # # # # # # # # # # # # +# # # # ## ## #### # # ### ##### ### ### ### # #### # #### # # ##### ### # ##### # # # +# # # # # # # # # # # # # # # # # # # # # # # +#### ## ## # ### ## # # ## #### # ### # # # # ##### # # # # ### ## ###### ##### # ### # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # ### # ### # # # ### # # # ## ### # ### # # ### # ## # # ## #### #### ## ## # # # # +# # # # # # # # # # # # # # # # # # # # # # # # +### ## # # ### ### ### ### # # # ####### ### ### # # ### # ### # ### # # # # ##### #### ### # ### +# # # # # # # # # # # # # # # # # # # # # +# # # # ## ##### ### ### ##### # # # ## #### ##### ## # ### # ### ######### # # ##### # +# # # # # # # # # # # # # # # # # # # # # # # # # +# #### ##### ### ##### # # # # # ## # # # # ### # # ### ##### ### # # ### # # # ## ####### # +# # # # # # # # # # # # # # # # # # # # # # # # +# ## ### #### # # # # ### # ##### # ### # ### # # # # ## # ##### # # # ######### ###### # # +# # # # # # # # # # # # # # # # # # # # # +# # # # ##### ##### ###### ###### # # # ### ### ### ## # ## # # # # ##### ### # ### # ### # +# # # # # # # # # # # # # # # # # # # # # # # # +# # ### # ### # # # # # ### ####### # ###### ## # ### ### ## ## ### # # ## # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# ## # # # ### # ### # # # # ## ########## ### ##### ## # # ### # ## # # # ## # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# # ## # ## # ### # # # # ##### ### # ###### # # ## ### # #### ## ### # ### ### # ## #### # +# # # # # # # # # # # # # # # # # # # # # # # # +### # ## # # # # ####### ##### ### ### # # ##### ### ### # # ## #### # ### #### # ### # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # #### # ### # ### # ### # ##### # # # ## #### # # # # ### ### ### ###### ## # +# # # # # # # # # # # # # # # # # # # # # # # # # +# ### # # ## ## # ## ### # ##### # # ##### # #### ##### ## ## ##### # ### ### # ### # ## +# # # # # # # # # # # # # # # # # # # # # # +# # ####### ## # ## ### ### ### # ## ## ##### # # # ### ## ## ##### ###### # ####### # ### # # +# # # # # # # # # # # # # # # # # # # # # # +# # ### # ## #### ## # ## # # ### # ### # # # # #### # # # # # # ##### ### ### ### # # # +# # # # # # # # # # # # # # # # # # # # # # # # +# # # ##### ### ## # # # # # # # # # # ## # ##### ##### ### # # # ### ####### ####### # ## # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# ### # # # ### ### ##### ####### # # # ### # #### ### # # # ####### ### ## ## ####### ##### # +# # # # # # # # # # # # # # # # # # # # # # +### ######### # ##### # # # #### # ### # ### # # # # # ## ##### # ### ### # # # ### # # # +# # # # # # # # # # # # # # # # # # # # # # # # +# ### ############# ### ### # # ## # ## ###### # ### # ## # # ### ### ####### # # # # # ## ## # +# # # # # # # # # # # # # # # # # # # +# #### ##### ##### ##### ##### # # # ### # # ## ## # # # ### ### ### ### ### # ##### ##### # +# # # # # # # # # # # # # # # # +### ## #### ###### ##### ## ## # ## # # # # # ####### # # # ####### # ## ### ## ##### # +# # # # # # # # # # # # # # # # # # +# # ### ## ### ## #### ##### # ## # # ##### # # ##### # # ### ####### # ### ## # ### # # # ## # +# # # # # # # # # # # # # # # # # # # +# # ### #### # # ##### # # #### ### ## # ### # ### # # # ######### ### # # ### ### # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# # ### ## ## # ### # # # # ## # ### # ##### ### # # ######### ### # ### # ##### # #### # +# # # # # # # # # # # # # # # # # # # # # # # # +# ### ##### ## ###### # # # ## ##### # ### ### ### ### # # # ### # # ### # ######### # # # ## +# # # # # # # # # # # # # # # # # # # # # # +# # # # # # # #### ## ### # ##### ### # ##### ### ### # # ### ## # # # ##### # #### # # +# # # # # # # # # # # # # # # # # # # # # # # # +# ##### # # #### # ### ########## ## # # ### # # ####### ##### # ### # ### # ##### # ##### # # # +# # # # # # # # # # # # # # # # # # # # +# ###### ### #### # ######### # ##### ### ### ######## ## ### # # # # ####### # # ##### ### +# # # # # # # # # # # # # # # # +# # ###### ###### ### ### # ### # # ##### ### # ### # # ##### # #### # ###### ## # ### ### ### # # +# # # # # # # # # # # # # # # # +# # # ### # ##### ## ######## ### ##### ### ### ###### ########### # ####### ## ## # # ### # +# # # # # # # # # # # # # # # # # # # +# # #### ##### ## ######## # ########### ### ### ## # # ### # ####### # # ### ### # ### # ### # +# # # # # # # # # # # # # # # # # # # # +# ##### # ##### # # ############# # # # # ### ##### # # ######## # ##### # # ### ### ## ### +# # # # # # # # # # # # # # # # # # # # # # # # # # # +### # ## ## # # # # # ## # ##### # ## ###### ## ### # ### # # ### ##### # ##### ## ## ## # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# # ######### ### # # ######### # # # # # # ### ### # ## ## ## ## # # ### # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# ### # # ##### # # ### # # # ## ## ### ## ## # ### # # ##### # # # # # #### ## # ### +# # # # # # # # # # # # # # # # # # # # # # # # # +# ### ##### # # ### # # # # #### # # # ### # # ####### # # ##### # ## ### ### ###### ##### # +# # # # # # # # # # # # # # # # # # +# # ### # # ### # #### ### # ### # # #### # ######### ### ### ##### # # ### ##### ### # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# ## ### ### ### # # # #### ### ## # # ##### # ### # ## # ## # ## #### ####### # ### # +# # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # #### ##### # ####### # ## # # # # ### # ##### # ### ##### # ### # ####### # # +# # # # # # # # # # # E# +################################################################################################### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/100x100_spaghetti_v6.txt b/skorohodovsa/task_2/source/templates/100x100_spaghetti_v6.txt new file mode 100644 index 0000000..52595aa --- /dev/null +++ b/skorohodovsa/task_2/source/templates/100x100_spaghetti_v6.txt @@ -0,0 +1,99 @@ +################################################################################################### +#S# # # # # # # # # # # # +# ### ### ## ##### ### # ### ### # # ### ## # # ##### ### # ### # # # ######### # ## ## # +# # # # # # # # # # # # # # # # # # # # # # +# ### # # ## ### # # # ### # # ### # # # # # # # # #### # # # # # # ##### # # ########## # +# # # # # # # # # # # # # # # # # # # # # # +##### ###### ### ## ## # ### # # # ### # ############# # # # # # # ##### ####### # ### ### # # +# # # # # # # # # # # # # # # # # # # # # # # +# # ### # ### # # # # ### # ### # # ### # # # ######## ### # ## # # ### ##### # # # # +# # # # # # # # # # # # # # # # # # # # # # # # +# # # # # ##### # # # ### # # ### ### # # # # ## # # #### #### ## # # # ##### # ### ## ### # +# # # # # # # # # # # # # # # # # # # # # # # +##### # # ### # # # # ## # # ### #### # # # ### # ### ### ### ### # # # ##### # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # ##### # # ### ## # ### ### ### # # # # # #### ### ### # ## ### # # ############ # ## +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # ##### ### # # # # # # ### # # ### # # # ### ##### ##### # # ### ### ## # # # ##### # +# # # # # # # # # # # # # # # # # # # # # # # # # +# ### # ### ## ## # ## #### ## # # # ## # # ### ### ### # ######### # # ### # ### # # ### # # +# # # # # # # # # # # # # # # # # # # # # # +# # # ### ###### ##### # # ########### # # ####### # # ### ### # # ### # ### # # # # ##### # # +# # # # # # # # # # # # # # # # # # # # # # +# ##### # ####### # # # ####### # #### ## # ## # ## # ### # # # ## ## # ### ## # #### # +# # # # # # # # # # # # # # # # # # # # # # # +##### # # ################# ### ### # ### # # # # # # ### # # # ## # ## # ### ### # ##### # ### +# # # # # # # # # # # # # # # # # # # # # # +# # #### ### # # # ##### # ##### ####### # # # # # # # ####### # ### ## # # ### ### # # #### # # +# # # # # # # # # # # # # # # # # # # # # +### # # ### # ### ### # ## ## ##### ### # ## #### ### # #### ## ## ### # # ####### # # ##### # +# # # # # # # # # # # # # # # # # # # # # # +# # # # # ### # ### # ## ##### # ##### # # ######### ### ### # # #### # #### ######## # ### +# # # # # # # # # # # # # # # # # # # # # # # # # # +# ### ### # # ## # # # ### # ##### # # # # # # # ## # # #### # # ### # ### # # ### # ## # +# # # # # # # # # # # # # # # # # # # # # # # # +### # # ## ### # ### # # # ### # # # # ####### ########### # # # ### # ###### ## ### # # ## # +# # # # # # # # # # # # # # # # # # # # # # +# # ##### ### # # # # # # # # ####### # # ######## ###### #### ### ## ## # # # ### ## # # +# # # # # # # # # # # # # # # # # # # +# ### # #### # # # # # ####### # # # ### ###### #### ## # ## #### # # ######### ### # ### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# ###### #### # # # # ### # ### # # ### ### ### # ### # # ### # # # #### # ### ## # # ### +# # # # # # # # # # # # # # # # # # # # # # # +####### ### ##### # ## # ## ## # ### # ### # ## # ### ####### # # # ### # # # ###### ## ### # +# # # # # # # # # # # # # # # # # # # # # # # +# # # # # ## ##### ####### ####### ### # ### # ##### ##### ### ### # # # # # # ### ## # # +# # # # # # # # # # # # # # # # # +# # ### ### ####### ######### ######## ##### # # # ############# ####### # ### ### # ### +# # # # # # # # # # # # # # # # # # # +# ### # ### ##### ### ##### # # # ### # ## # # # ### # ### # # # # ### # ##### ## ### ### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +## # # # # # # ### # ##### # # ### ##### #### ############ ### ### # ###### ### # ### ### # # +# # # # # # # # # # # # # # # # # # # # # # +##### # ## # # # #### ### ## # ##### ### # # #### ### ### # ##### # # ### ## ### # # # ## # +# # # # # # # # # # # # # # # # # # # # # # +# ####### ##### ### # # ## # # # ##### # ####### ##### ### # ### ##### # # # ## # ## ###### ## +# # # # # # # # # # # # # # # # # # # # # +## # ##### ####### # ### # # # # # ##### ### # ### #### ### # ### ## # # ### # # ### ##### # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # #### # # # ### ### ##### ##### # ### # # # ### # ##### # # ### # ### # ### # # # ### #### +# # # # # # # # # # # # # # # # # # # # # # # +# ##### # # # # # ### # # ### # # ### # ############# # # ##### ## # ## ### # ###### # # ### # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # ### # # ### ### ### # # #### ## # # # ##### # # # ## # # ## # # # # ### ## ##### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# ### ##### ### ## # # ### # ##### # ####### # ## ## # # ### # # # # ## # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # +# ### # # # ####### # ######### ######## #### # # ## # ## ## # # # # # # # #### ##### # # # ### +# # # # # # # # # # # # # # # # # # # # +## # # #### # # # ## # # ### ##### # # ### # # ### # ### # ## # ########### # # # ## ###### # +# # # # # # # # # # # # # # # # # # # # # # +# # ###### ####### # ## ########## # # ####### ## ## ### # # # ## # # # ####### # ## # # ##### +# # # # # # # # # # # # # # # # # # # # # # # # +# # # ### # # # # ## # ### # ## ## ## # # # # # #### ### # ### # # ## ## # ### #### ### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# ## ## # ####### ##### # ##### # ### # # # # # # # # # ##### # ## # ### ##### # ##### # # +# # # # # # # # # # # # # # # # # # # # # # # +# ## ### # # ##### ### ## ## ### # # # # ######### ##### #### # ### ### ## ### # ### ##### # +# # # # # # # # # # # # # # # # # # # # # # # +### # # #### # # ## # # ### ####### # # ## ## #### ### # # # # # # ### # ########## # # +# # # # # # # # # # # # # # # # # # # # # # # +# ##### ##### # ### # # # ### # ####### # # # ##### # # # # # # ##### # ## ## ### # ### ### # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +##### # # # ##### # # ### # ##### ### # ### ### # ### # ####### # # ####### # ### # ## ### ## # +# # # # # # # # # # # # # # # # # # # +# #### # # # ## #### # ### ## # ## # ### # ### #### # ### # ####### ######### # ##### # ### # +# # # # # # # # # # # # # # # # # # # # # # +# # # ####### ###### # # ## ## # ## ## #### # # ## ## #### ####### # ####### # # ####### +# # # # # # # # # # # # # # # # # # # # # +# ### # # # # ###### ## # #### ### # # #### #### #### ### ### # # ## ## # ### ##### +# # # # # # # # # # # # # # +### # ## ## ##### ######### #### # # # # # ## # ######### ######### #### #### ### ## # +# # # # # # # # # # # # # # # # # # # # # # # +# # # ### ## ##### # ##### ### # # # # # ##### # ### ## # # ### ### # # # ####### # # # # +# # # # # # # # # # # # # # # # # # # # # # # +# # ### # ############# # # # # ##### # # # # ### # ###### # # # ## # ####### # # ### # # +# # # # # # # # # # # # # # # # # # # # # # # # +# ### ##### # # # ###### ## ### # # ##### # # ### # ### ### # # # # ### ## ## ### # # # # # ### +# # # # # # # # # E# +################################################################################################### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/100x100_spaghetti_v7.txt b/skorohodovsa/task_2/source/templates/100x100_spaghetti_v7.txt new file mode 100644 index 0000000..5d863ab --- /dev/null +++ b/skorohodovsa/task_2/source/templates/100x100_spaghetti_v7.txt @@ -0,0 +1,99 @@ +################################################################################################### +#S # # # # # # # # # # # # # # +# # # # ### # # #### ## # ##### # # # # # # # # ### ### # # ### # ##### # ### # # ## # ## # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +######### # ### # ##### # ## # # ##### # ######## ### ##### # # # # # ### # # ### ## # # # # # +# # # # # # # # # # # # # # # # # # # # # # +# ##### ####### # # ### # ### ### # ##### ### ## # ## ### ### # # # # ## #### #### ## # ## +# # # # # # # # # # # # # # # # # # # # # +##### ##### ### ###### # # # ### # # # # ### # ### # ##### ####### # ### ##### # # ##### # ####### +# # # # # # # # # # # # # # # +# ####### # # # ### # # # # ### # # ### #### # # ######### ## ## ### ### ## ## ### # # # +# # # # # # # # # # # # # # # # # # # # # +# ##### ### ### # ########### # # ### # ####### ##### # #### ###### # ### # # # ### # ### ## # # +# # # # # # # # # # # # # # # # # # # # +### # #### #### # ####### # ####### # ## # # # # # # ##### ## ## # ##### ### ##### # ### # +# # # # # # # # # # # # # # # # # # # # # # # # +# ## ### # # # # # ## ### # ### ### # # ### ## ## ##### # # # ### ## ######## ### ######### # +# # # # # # # # # # # # # # # # # # +# # ##### ##### # ### # # # # # # # ## # ### ## ## # # # ## ####### # # # # # ### ###### # +# # # # # # # # # # # # # # # # # # # # # # # # # +# #### #### # # ## ### # ##### # # ### ### # # ###### ##### ### # ###### # ## ## ### # ## # +# # # # # # # # # # # # # # # # # # # +## #### # ### # # ##### # # # # # ### ############ # ####### ###### ## ####### # # ## ## # # +# # # # # # # # # # # # # # # # # # # # # # +# # # ##### # ### # ### ####### # # # ## ##### # ### # ######### #### # # # # # # ##### # ### # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # ### # # # # # ### # # # # # ### # # # # ##### ### # # # # # ####### ## ## +# # # # # # # # # # # # # # # # # # # # # # # # # # +# # ## ##### ## # # ### # ## ## ## # #### # ### ##### # ####### # # # ### # # # ## # +# # # # # # # # # # # # # # # # # # # # # # # # +# ###### # ### ## # ##### # # # # #### # # # # ### ######### ##### ##### ###### ## # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# # ###### # ### # # ##### # ### ##### # ### # # # # ## # ## ### # # ## # # # # # +# # # # # # # # # # # # # # # # # # # # +#### ### # # # # ######### ### ### # # # # ##### # ##### #### # # # ### ### ####### # # ## # +# # # # # # # # # # # # # # # # # # # # # # # +# ## ## ##### #### # # #### ## # ###### #### ## # # ####### ## # ## ### ### ## ##### # # +# # # # # # # # # # # # # # # +## # ############ ##### ### # # ### # ###### ## # # ## # # # ### # # ### ## ## +# # # # # # # # # # # # # # # # # # # # # # # # # +# ####### ## ## ##### ## ####### ##### ### # # ### # # #### # ### # ### # #### #### # ##### # # +# # # # # # # # # # # # # # # # # # # # # # +# ### # # # # # # # # ### ### ## ###### # ##### # ##### # ### # ### # # ######## ##### # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # ## # # # # # # # ### # # # #### # ### ### ### # ## ### # # ### ### ### ### # +# # # # # # # # # # # # # # # # # # # # # # # # +# # ##### # ## ### ## # # # # # ### # # # ######## ###### # ##### ### # # # ## # # # ### # # +# # # # # # # # # # # # # # # # # # # # +# # ### ## # # # ######### # # ## #### ############## # # ## # # ## # # ############# ### # +# # # # # # # # # # # # # # # # # # # +# # # ###### ## ######### # # # # # # ## # ########### ## # ## ###### ##### # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +######### # ### # # # #### ## ### # ### ### # ##### # # # ### # # # # ### # # # ### ## ## ## # +# # # # # # # # # # # # # # # # +# ##### ## # # ##### ## # ##### ##### # ### #### ###### ####### # # ### ########## #### # # +# # # # # # # # # # # # # # # # # # # # +# # # # # ### ### # ## # # ### # # ### ##### # ####### ### ## # # ### ### # # ### ## #### +# # # # # # # # # # # # # # # # # # # # # # +# ### # # # ### ##### ## ##### ### ## ####### ## ### # # ### # # ######### # ######### ### # +# # # # # # # # # # # # # # # # # # # # # +# ### ### ## ###### # #### # # ####### # ### #### # ####### # # ##### # # ##### # ### ####### # +# # # # # # # # # # # # # # # # # +### ### # # # #### ############ ### ### ### # ### # # ### ### ### ##### # ####### # ## #### # # +# # # # # # # # # # # # # # # # # # # # +# ### ##### # ### # ## # ########### #### ## # ## # ## ###### ## # # # # ### # ###### # +# # # # # # # # # # # # # # # # # # # # # # # # # +### # ### # # # # # # ## # #### ##### # # ### ###### # ##### # # # # ## #### # # # ### +# # # # # # # # # # # # # # # # # # # # # # # +# # # ## ## #### ### ## # # ### ##### ##### ### ## ###### #### ### ## # ### # ### # ### # +# # # # # # # # # # # # # # # # # # # # # # # # +# # # # ## ## # ### # # ### # # ### # ### # # # ## # #### #### # ### # # ### #### ## ###### +# # # # # # # # # # # # # # # # # # # # # # # # +# ## ###### ## # # # ### ### ####### ##### # ### # # ### # ### # # # # ## ## # # # # ##### # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # ### # # # #### # # ### ### ### # # # ### # # ### # # # # ##### ####### # # ##### ### # # # +# # # # # # # # # # # # # # # # # # # # # # # +# # ## ##### # ## ##### ## ## ### ### ### ### # # ##### #### ## # # ## ### ##### # +# # # # # # # # # # # # # # # # # # # # # # +# # # ### # ##### # ## #### # ####### # # ### ### # # ## ## # # ########### # ### # # # ##### +# # # # # # # # # # # # # # # # # # # # # # +# ##### ## ## # ## # #### ####### ## ### ## #### ###### # # ## #### # # ####### # ##### # +# # # # # # # # # # # # # # # # # # # +### # ########## #### # ##### # # ##### # ### ##### # #### ## # # # # ##### ### ## ### # # # #### +# # # # # # # # # # # # # # # # # # # # # # +# # ### # # ##### # # # ####### ## ## ## ## ### # ##### # # ### ##### ### ##### ### ### # +# # # # # # # # # # # # # # # # # # # # +# # #### ## # ##### # ## ##### # ### # #### #### ##### # ## ####### # # ######### ## # # +# # # # # # # # # # # # # # # # # # # # # +# ### # # # ##### # ####### # # # # ### ### # # ### ## # ### # ## # ### # ##### # ##### # # # +# # # # # # # # # # # # # # # # # # # # # # # +# #### # ## # # ## ## # ### ## ## ## # ##### ### ####### ### ####### # # ## # ### # # ### +# # # # # # # # # # # # # # # # # # # # # # +# ### # #### #### # # # ## # #### #### ## ######## #### # # # ### # # # # # # # # ## ## # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # ## ## # ## ### ## # ### # ### # ## # # # ### # ### # # ## ####### # # ### ###### # ## # +# # # # # # # # # # # # # # # # # # # # # +## # ### ## # ## ### # # # ##### ### # ##### ##### # ## # ######## # # ## ### ### ### ## # # # +# # # # # # # # # E# +################################################################################################### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/100x100_spaghetti_v8.txt b/skorohodovsa/task_2/source/templates/100x100_spaghetti_v8.txt new file mode 100644 index 0000000..393318e --- /dev/null +++ b/skorohodovsa/task_2/source/templates/100x100_spaghetti_v8.txt @@ -0,0 +1,99 @@ +################################################################################################### +#S# # # # # # # # # +# ### # ##### ### # # # ## ### ######### ############# # # ########### # # # # ##### # #### # ### +# # # # # # # # # # # # # # # # # # # # +### # ### # ### # # ##### ####### ####### # ### # ## # ### ## ## # ### ##### ##### ## ##### ## # +# # # # # # # # # # # # # # # # # # # +# ## ## ### # ### #### ## # # ## ##### ### ## # # ### # # ### ##### # # ##### # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# ### # # # # ### # # # ## ## #### #### ## ## # # ## # # # ### ### ## ##### ####### ### ### +# # # # # # # # # # # # # # # +# # ##### # ## ## #### # #### ## # # ### ### ####### ### ## ###### # ##### # # ##### ### ## # +# # # # # # # # # # # # # # # # # # # +## # ### # # # ### ##### # # ##### ### # ### ### # # # # # ### ### # ### ###### # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # ## # # # ####### ### ### ## # ### # # # ### # # # ### ### # ### # ### # # # # +# # # # # # # # # # # # # # # # # # # # # # # +# ### ###### # # # ####### ##### #### # ###### # # ### # # ### # # # # ##### # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # +# # # # ####### # # # ### # # # ### ### # # # # ##### # ##### # # # # ## # ### # # # ## # +# # # # # # # # # # # # # # # # # # # # # # # # +# ### # #### ## # # ### ## ## # # ## # ### ###### ## # ### # ### # # # ##### ####### # ### # ### +# # # # # # # # # # # # # # # # # # # # # +# #### #### ####### # ##### ## # ### ## ##### # ### ##### # ### # # ## # # # #### ### # +# # # # # # # # # # # # # # # # # # # # # +#### # ## # # # # # # ### ### ### # # # ### ####### # ##### # ##### ##### ####### # # # ### # # # +# # # # # # # # # # # # # # # # # # # # # +# # # # # ### ## # ### ### ## ### # # # #### ## # ### #### # ##### # ### # ##### ## ## ##### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # ### ### ### # ## ## ## # # ### ##### # ### # # # ## ## # # # ### ####### # ### # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# ####### # ### # # ### ## # ### # # # ### # # ### # ##### # ##### ###### ### # ### ##### # +# # # # # # # # # # # # # # # # # # # # # # # # +# ### # # # ## ### ### # ## # # ### # # ## ##### # ##### # # # ### # ### ### # # ##### # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# # ### ### # ### # # # # ### # # # ##### ### # ## ## ####### # # ## ## ##### ### ## # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # ### # # ### # ### # ## #### # # # # # # # ### ## # # # ### ### ### ### # # ### +# # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # ######## ### ### ### # # #### ### ## # # # ##### # # # # #### ### # # ### # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +### # # # ## ### # # # # # #### # ### # ## ##### # # # # # ####### # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # ## # ### # # # ### #### ### ## # # # #### ##### # # # ##### # #### ## ### # # #### # # +# # # # # # # # # # # # # # # # # # # # # # # # +# # ### ##### # # # ## # # ##### ### # ### # ## #### # ### ## # # ### # ### ### # # ##### # +# # # # # # # # # # # # # # # # # # # # # # # # +# ## # # ##### # ###### # ########### # ##### ### # ### # # ########## ### ##### # # # # # # # +# # # # # # # # # # # # # # # # +# ####### ######### # # ##### # # # ## # # # ### # # # # ### ### ### ####### # ## ##### # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # ### # # ### # ## ## # # # # # ### # ##### # # ### # # # # # # # # # ### # # ## # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # +##### # # ##### # #### #### ### # # # ############# # # ### # ### ## #### # # #### # # #### +# # # # # # # # # # # # # # # # # # # +# # # ##### #### ### # #### # # # ### # #### ## # # # # ### # # ### ###### # ## # # # # # +# # # # # # # # # # # # # # # # # # # # # # +# ##### # # # ### # # ############# # # ##### # # # # ### ### # ## # ### # #### ## # ### # +# # # # # # # # # # # # # # # # # # # # # # # # +## # # ### ## # ## # ## # ##### # # # ###### # ## ### # # # ### ### # ###### ### # ### # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # ## # # ##### ##### # # # ##### # # # ### ### # # ### # # # # # ### ### ###### # +# # # # # # # # # # # # # # # # # # # # # # # +# # ### # ##### # ## # # ### ### ####### # # ### ### # # # ##### # # ############ # # # # ### +# # # # # # # # # # # # # # # # # # # # # # +# ## ### # ### ### # # ### ### ### ##### # ##### ##### # ## ## # # # ### ### ###### ###### # +# # # # # # # # # # # # # # # # # # # # # # +# # ######## ## ##### ##### ### ##### ##### # # # # # # # # # # ### ### # # # # # #### # # # +# # # # # # # # # # # # # # # # # # # # +# ### ### ### # # # # ### ##### # # # # # ### ### # # # # ##### ##### # ## # ## # # ### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +### # # # # # # ### # ##### # ### ### ### ## # # ### # # ### ###### ##### # # ### ## ## # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# ### ## # # # # ## #### ### ###### # # # ###### ## # ## ## # ##### # # # ### ####### ### # +# # # # # # # # # # # # # # # # # # # # # +# ##### ### ### ### # # ### ####### # ## ## ### # # # # ########### # ### ##### ### # ## ##### # +# # # # # # # # # # # # # # # # # # # # +# ### ### ## ####### # ##### # ### # ## ## ######## # ### # #### # # ####### ### ####### # +# # # # # # # # # # # # # # # # # # # +# ##### ##### # # ### # # ## ###### ## # ##### # #### # ### # # # # ##### # # ### # # ### # +# # # # # # # # # # # # # # # # # # # # # # +# # # ## # # # # ### # ## # # # ##### # ##### # ########### # ##### # ####### # ### # ### +# # # # # # # # # # # # # # # # # # # # # +# # # #### ### ##### # ##### # ##### # # ### # ### # # # ####### # # # # ##### # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# ### # ### ####### ### # # ### # # ## # #### # ##### # # ### # ### ## ## # #### ## ## ##### +# # # # # # # # # # # # # # # # # +# # ###### # ### # ##### # ## ######## # # # # # ######## ##### ####### # #### # ####### # +# # # # # # # # # # # # # # # # # # +##### # ### # # ### # ### ### ### ### ##### ## # # # # # ## ### # ### # ### # ## # ## # ### +# # # # # # # # # # # # # # # # # # # # # # # +# # ### # # # # ### # ### # # ### ### ### # # ####### #### # ### # # # ##### # ### # ### #### # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# # ### # ### ### # # # ### # # ### # ######### ### ## ### ## ### # ### # ## ##### ### # +# # # # # # # # # # # # # # # # # # # # # # # +# ### # # # ####### # ### ## # # ### ##### ## ### ##### # # # ## #### # ### # # ### ##### ### +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# # ### # ### ## # # ### # ##### # ##### # ### ## ##### ### # # ######### # # ### # # # # # # +# # # # # # # # # # # E# +################################################################################################### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/100x100_spaghetti_v9.txt b/skorohodovsa/task_2/source/templates/100x100_spaghetti_v9.txt new file mode 100644 index 0000000..850393f --- /dev/null +++ b/skorohodovsa/task_2/source/templates/100x100_spaghetti_v9.txt @@ -0,0 +1,99 @@ +################################################################################################### +#S # # # # # # # # +### # # ### # ### #### ## ##### # ### # ### ### #### # ### ### # ####### # # # ### # ### ### # +# # # # # # # # # # # # # # # # # # # # # # +# # # # # ############### # ### ###### ### # ### ######## ### # # ##### # # # # # ### # # # # # # +# # # # # # # # # # # # # # # # # # # # # +# # ## # # ##### ## ## ### ### # ### #### #### # ### ### ### # ##### # ### ### ### # # +# # # # # # # # # # # # # # # # # # # # # +# ### ### ### # # ##### ###### ######## ## #### # # # # # # ## # # # ### ### ### # # ## # +# # # # # # # # # # # # # # # # # # # # # # # +# # # # ####### ## # ############### ### # ##### # #### # # # # ### ## # # ### # ### # # +# # # # # # # # # # # # # # # # # # # # # +# ##### # ## ## ##### ### # ## ## #### # # # ##### ####### ### # ## ## # #### # ##### # # +# # # # # # # # # # # # # # # # # # # # +### ### ### # ## #### # ### # # # ###### ### # ####### # # ## # # # # ### # ## ## # # ### # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# ### ### # # # # ####### # ### ##### ####### # # # # ####### # # #### ###### # # ### # ## ### # # +# # # # # # # # # # # # # # # # # # # +# #### # # ## ######## ### ##### # # ##### ###### # ##### #### ## ## ## # ### # #### # +# # # # # # # # # # # # # # # # # # # # # # # # +# ###### ##### ### # # # # # # ### # ## ## ### # #### # ### # # # # ### # # # ### ###### # ### +# # # # # # # # # # # # # # # # # # # # # # # +# ### # # ##### # #### #### ####### #### # ### ### # ## ###### # ## # # # # # # # # ### # ### # +# # # # # # # # # # # # # # # # # # # # # # # +# # # # ## # # ######## ## ## ### ### ### ####### # ### # # ### # ### #### # ## # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # +# ### # ### # # # # # # ## ### # # ## # # ### ### ### # # # # # ##### # # # # ### ### # #### # +# # # # # # # # # # # # # # # # # # # # # # # +# # # ### # # # ## # ### ### ##### # ### # # ####### # ####### ### # ###### # # ##### # # # # # +# # # # # # # # # # # # # # # # # # # # # # +# # # ### # ## ## # ##### # # # ## ####### # ## ### ######### # # ##### # #### ## ## # ### # # # +# # # # # # # # # # # # # # # # # # # # # # +# ## # # #### #### #### ### # ####### # # # ## ##### # # ##### # ######### ### # ### # # # # +# # # # # # # # # # # # # # # # # # # # # +## # # # # # ## ### # # # # # ### ## ### # ### # # # # ####### # ## # ### ## # ### ### # ### +# # # # # # # # # # # # # # # # # # # # +# ##### # # # # ##### # # ##### ### # ##### # ##### # ## #### # ##### # # ####### # ### ### # +# # # # # # # # # # # # # # # # # # # # # +# ## # # # ### # # ### # # ## ## # # ### # #### # # # # ####### ### #### # ### ## ###### +# # # # # # # # # # # # # # # # # # # # +# # # #### ### #### ### # # ### ####### ###### # # # ##### # ################# # # ##### # +# # # # # # # # # # # # # # # # # # # # # +#### # # ### # # # # ### # # ### # # ####### ### ### # # ## ## # # # # #### # ## ## ### # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # +# ## # # # # # # ## ## # # # ### # ### # # ### ### ### ## # ### ######### # ## ## # ## ## +# # # # # # # # # # # # # # # # # # # # # # # # # # # # +# ### ### # # ### ### # ### ## # ## # ## # ## ### # # # # # ### # # # # # ### ### # ### # # # +# # # # # # # # # # # # # # # # # # # # # # # # +# # ### ## ### # # ### # # ##### # # ## ###### #### # ##### # # # # # # ### # ## # #### # # +# # # # # # # # # # # # # # # # # # # # # # # +# ####### ## ## ### # # # ### # # # ## # # ## #### # # ####### # # # # ##### ## # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # +####### ### # ### # ### ### # ########### # ### ### # # ###### ## # # # ####### ####### # # # # +# # # # # # # # # # # # # # # # # # # # # +# ### ## # # #### #### # # #### # # ##### # # # # # # # # # # # ##### # # #### # # # # ## +# # # # # # # # # # # # # # # # # # # # +# # # ## # # ####### ### ##### # # ####### # ### # # # # # # # ## ## #### #### ####### ### # +# # # # # # # # # # # # # # # # # # # # # # # +# # # ##### # ## ## ### ### ### # ##### # # ##### # # ###### #### # ## ## # ### # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# # # ### # ### ### # # # # ### #### ########## # # ##### ### # # ## # # # ### # # ### ##### # # +# # # # # # # # # # # # # # # # # # # # # # # +### # ### # # # # # ####### ## # ### ### ##### # # ##### ## # # ##### # # ##### ####### # # +# # # # # # # # # # # # # # # # # # # # # # # +# # # # # # ###### # ### #### ## # ### ### # # ## # ### #### ### # # # ### #### # ## ### # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # ## # #### ###### # ## # ## ### # ### # # ### ## # # ##### ### ######### # # ## # # ### +# # # # # # # # # # # # # # # # # # +# # # ######## ### # ### ### ### ##### # # # # # ### # ########## # ######### # # ## # +# # # # # # # # # # # # # # # # # # # # # # +# #### ## # # ## ## ##### # # # # # ### # ##### # # ### # ### # # # # ### ### ### ## ## # ##### # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # ##### # # # ## # # # # # # #### ############## ## # # # # ### # # ##### ##### # # +# # # # # # # # # # # # # # # # # # # # # # # +# # ## # ### ####### # ### # # #### # # ### ######### # ##### ####### # ### # ### ## # ### ### +# # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # ## #### ## ## ### # # # ##### # # # ## # #### #### # ## ## ## # #### ### # +# # # # # # # # # # # # # # # # # # # # # # # # +# # # # ### ### ##### # # ##### # ### # ##### # # ### # ### # # ### # # # #### # ## #### # ### # +# # # # # # # # # # # # # # # # # # # # # +# ### # ##### # # # # ### # ### ### ### # # # # # # # # ### ###### ## # # # ######### # # ## +# # # # # # # # # # # # # # +# # # # #### ###### #### # #### ###### # ## #### # ## # ### ## # ###### ##### ############# # +# # # # # # # # # # # # # # # # # # +# # # ###### # ####### # # ### # # # # # #### #### ##### # ### ##### # # ### # ## # +# # # # # # # # # # # # # # # # # # # # # # # # # +# ### ## # # # # ##### # # # ### ##### # # # #### #### ####### # ### ### ### ### # #### # # # +# # # # # # # # # # # # # # # # # # # +# # ### ##### ### # #### ### # # # # ### ####### ##### ##### # #### ### ### ########## ##### +# # # # # # # # # # # # # # # # # # # # # # # +# #### ##### ### ### # ###### # ### # # ### # # ### ## # ### ### ###### ##### ### # # ### # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# # ### ##### # # # # # # # ### ##### # ### ## # ### # ######## # ## ## # # ##### # # +# # # # # # # # # # # # # # # # # # # +# ### ## # ## # ####### ##### # # #### # # ##### ### # ###### # # ######## # # # # # ### ### # +# # # # # # # # # # # # # # # # # +####### # # ### ########## # # # ## ## # # ### ########### ##### ### ### # # ### # # # # # +# # # # # # # # E# +################################################################################################### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/10x10_path_v1.txt b/skorohodovsa/task_2/source/templates/10x10_path_v1.txt new file mode 100644 index 0000000..1a113a8 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/10x10_path_v1.txt @@ -0,0 +1,9 @@ +######### +#S# # +# # # ### +# # # # +# ##### # +# # # +##### # # +# E# +######### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/10x10_path_v10.txt b/skorohodovsa/task_2/source/templates/10x10_path_v10.txt new file mode 100644 index 0000000..b4b07d4 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/10x10_path_v10.txt @@ -0,0 +1,9 @@ +######### +#S # # +### ### # +# # # # +# ### # # +# # # +# ##### # +# E# +######### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/10x10_path_v2.txt b/skorohodovsa/task_2/source/templates/10x10_path_v2.txt new file mode 100644 index 0000000..5d64685 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/10x10_path_v2.txt @@ -0,0 +1,9 @@ +######### +#S# # # +# # # # # +# # # # +##### # # +# # # # +# ### # # +# E# +######### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/10x10_path_v3.txt b/skorohodovsa/task_2/source/templates/10x10_path_v3.txt new file mode 100644 index 0000000..2757357 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/10x10_path_v3.txt @@ -0,0 +1,9 @@ +######### +#S # +####### # +# # # +# # ### # +# # # # +# # # ### +# # E# +######### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/10x10_path_v4.txt b/skorohodovsa/task_2/source/templates/10x10_path_v4.txt new file mode 100644 index 0000000..37e64e2 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/10x10_path_v4.txt @@ -0,0 +1,9 @@ +######### +#S# # +# # ### # +# # # # +# ### # # +# # # # +# # ### # +# # E# +######### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/10x10_path_v5.txt b/skorohodovsa/task_2/source/templates/10x10_path_v5.txt new file mode 100644 index 0000000..47d7392 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/10x10_path_v5.txt @@ -0,0 +1,9 @@ +######### +#S # # +##### # # +# # # # +# # # # # +# # # # +# ##### # +# E# +######### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/10x10_path_v6.txt b/skorohodovsa/task_2/source/templates/10x10_path_v6.txt new file mode 100644 index 0000000..47d7392 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/10x10_path_v6.txt @@ -0,0 +1,9 @@ +######### +#S # # +##### # # +# # # # +# # # # # +# # # # +# ##### # +# E# +######### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/10x10_path_v7.txt b/skorohodovsa/task_2/source/templates/10x10_path_v7.txt new file mode 100644 index 0000000..99c6fe3 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/10x10_path_v7.txt @@ -0,0 +1,9 @@ +######### +#S# # +# ### ### +# # # +### ### # +# # # # +# # # # # +# #E# +######### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/10x10_path_v8.txt b/skorohodovsa/task_2/source/templates/10x10_path_v8.txt new file mode 100644 index 0000000..6fc4a3a --- /dev/null +++ b/skorohodovsa/task_2/source/templates/10x10_path_v8.txt @@ -0,0 +1,9 @@ +######### +#S # # +### # # # +# # # # # +# # # # # +# # # # +# ### # # +# #E# +######### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/10x10_path_v9.txt b/skorohodovsa/task_2/source/templates/10x10_path_v9.txt new file mode 100644 index 0000000..7c37230 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/10x10_path_v9.txt @@ -0,0 +1,9 @@ +######### +#S# # +# ##### # +# # # +### # # # +# # # # +# ##### # +# E# +######### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/20x20_noexit_v1.txt b/skorohodovsa/task_2/source/templates/20x20_noexit_v1.txt new file mode 100644 index 0000000..724541a --- /dev/null +++ b/skorohodovsa/task_2/source/templates/20x20_noexit_v1.txt @@ -0,0 +1,19 @@ +################### +#S # # # +##### # ##### ### # +# # # # # # +# ### # # ##### # # +# # # # # # # +# # ######### # # # +# # # # # +# ######### # ##### +# # # # # +# ### # # # ##### # +# # # # # # # +# # ### ####### # # +# # # # # # # +# # # ### ### ### # +# # # # # +# ### # ### ##### # +# # # # +################### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/20x20_noexit_v10.txt b/skorohodovsa/task_2/source/templates/20x20_noexit_v10.txt new file mode 100644 index 0000000..242bcc7 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/20x20_noexit_v10.txt @@ -0,0 +1,19 @@ +################### +#S # # # +##### # # # ##### # +# # # # # # # # +# # # # # ### # # # +# # # # # # # +# ##### ### ### # # +# # # # # +# ### ####### ### # +# # # # # # +### ##### # ### # # +# # # # # # +# ######### # ##### +# # # # # +# # ##### # ##### # +# # # # # # +# ### # # # # ### # +# # # # # +################### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/20x20_noexit_v2.txt b/skorohodovsa/task_2/source/templates/20x20_noexit_v2.txt new file mode 100644 index 0000000..a5dd21d --- /dev/null +++ b/skorohodovsa/task_2/source/templates/20x20_noexit_v2.txt @@ -0,0 +1,19 @@ +################### +#S # # # +######### # # # # # +# # # # # # +### ##### ##### ### +# # # # # # +# # # # # # # ### # +# # # # # # # # +# ### ##### ### # # +# # # # # # +# # ####### # ### # +# # # # # # +# ### # ##### ### # +# # # # # # +# ####### # ### # # +# # # # # # # +# # # ### # ### # # +# # # # +################### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/20x20_noexit_v3.txt b/skorohodovsa/task_2/source/templates/20x20_noexit_v3.txt new file mode 100644 index 0000000..585f187 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/20x20_noexit_v3.txt @@ -0,0 +1,19 @@ +################### +#S # # # # +### # # # ### # # # +# # # # # # # # +# # ##### # # # # # +# # # # # # # # +# # # # ### ####### +# # # # # # +# # # ### ####### # +# # # # +# ### # ######### # +# # # # # +# # ##### ### ##### +# # # # # # +# ### # ##### ### # +# # # # # # +####### # # ### # # +# # # # +################### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/20x20_noexit_v4.txt b/skorohodovsa/task_2/source/templates/20x20_noexit_v4.txt new file mode 100644 index 0000000..7eeb7da --- /dev/null +++ b/skorohodovsa/task_2/source/templates/20x20_noexit_v4.txt @@ -0,0 +1,19 @@ +################### +#S # # # +### ####### # # ### +# # # # # +# ### ### ####### # +# # # # # +####### ####### # # +# # # # +### ### # ####### # +# # # # # +# ######### ### # # +# # # # # +### # # # ####### # +# # # # # # # +# ### # ### # # # # +# # # # # # # +# # # # ##### ##### +# # # # +################### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/20x20_noexit_v5.txt b/skorohodovsa/task_2/source/templates/20x20_noexit_v5.txt new file mode 100644 index 0000000..2f8ad60 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/20x20_noexit_v5.txt @@ -0,0 +1,19 @@ +################### +#S# # # +# # ### # ####### # +# # # # # # # # +# # # # # ### ### # +# # # # # # +##### # # # # ##### +# # # # # # # +# # # # ### ##### # +# # # # # # +# ####### ### ### # +# # # # # +# # ##### # # # ### +# # # # # # # # +# ### ####### # # # +# # # # # # +# # ### # ##### # # +# # # # +################### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/20x20_noexit_v6.txt b/skorohodovsa/task_2/source/templates/20x20_noexit_v6.txt new file mode 100644 index 0000000..ffcf274 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/20x20_noexit_v6.txt @@ -0,0 +1,19 @@ +################### +#S # # # +### # # ### ### ### +# # # # # +# ### ########### # +# # # # # # +### # # ### ### # # +# # # # # # # # +# # # # # ### ### # +# # # # # # +# ####### # ##### # +# # # # # # +# ### # # # # ##### +# # # # # +########### ##### # +# # # # +# # ########### # # +# # # +################### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/20x20_noexit_v7.txt b/skorohodovsa/task_2/source/templates/20x20_noexit_v7.txt new file mode 100644 index 0000000..04d6167 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/20x20_noexit_v7.txt @@ -0,0 +1,19 @@ +################### +#S # # # +##### # ### ### # # +# # # # # # # +# # # ##### ### # # +# # # # # # +# ####### # # ### # +# # # # # # +# # ########### # # +# # # # # +# ####### ### # ### +# # # # # +########### # ### # +# # # # # +# # # ### # ##### # +# # # # # # +# # ####### ##### # +# # # +################### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/20x20_noexit_v8.txt b/skorohodovsa/task_2/source/templates/20x20_noexit_v8.txt new file mode 100644 index 0000000..18a670d --- /dev/null +++ b/skorohodovsa/task_2/source/templates/20x20_noexit_v8.txt @@ -0,0 +1,19 @@ +################### +#S # # +### # ### ####### # +# # # # # # # +# ### ##### # # # # +# # # # # +# ### ####### ##### +# # # # +### ####### ##### # +# # # # # # +# # # # ##### ### # +# # # # # # +# # # ######### # # +# # # # # # +# # ########### ### +# # # # +# ########### ### # +# # +################### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/20x20_noexit_v9.txt b/skorohodovsa/task_2/source/templates/20x20_noexit_v9.txt new file mode 100644 index 0000000..7a11e8f --- /dev/null +++ b/skorohodovsa/task_2/source/templates/20x20_noexit_v9.txt @@ -0,0 +1,19 @@ +################### +#S # # # +### # ##### # ### # +# # # # # # # +# # # # ####### # # +# # # # # +# ####### ####### # +# # # # # +####### ##### # # # +# # # # # +# ### ### # ##### # +# # # # # # +### ### # ### # ### +# # # # # # # # +# ### # # # ### # # +# # # # # # +# ### ####### ### # +# # # +################### \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/30x30_empty_v1.txt b/skorohodovsa/task_2/source/templates/30x30_empty_v1.txt new file mode 100644 index 0000000..2828878 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/30x30_empty_v1.txt @@ -0,0 +1,30 @@ +############################## +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +############################## \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/30x30_empty_v10.txt b/skorohodovsa/task_2/source/templates/30x30_empty_v10.txt new file mode 100644 index 0000000..2828878 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/30x30_empty_v10.txt @@ -0,0 +1,30 @@ +############################## +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +############################## \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/30x30_empty_v2.txt b/skorohodovsa/task_2/source/templates/30x30_empty_v2.txt new file mode 100644 index 0000000..2828878 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/30x30_empty_v2.txt @@ -0,0 +1,30 @@ +############################## +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +############################## \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/30x30_empty_v3.txt b/skorohodovsa/task_2/source/templates/30x30_empty_v3.txt new file mode 100644 index 0000000..2828878 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/30x30_empty_v3.txt @@ -0,0 +1,30 @@ +############################## +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +############################## \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/30x30_empty_v4.txt b/skorohodovsa/task_2/source/templates/30x30_empty_v4.txt new file mode 100644 index 0000000..2828878 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/30x30_empty_v4.txt @@ -0,0 +1,30 @@ +############################## +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +############################## \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/30x30_empty_v5.txt b/skorohodovsa/task_2/source/templates/30x30_empty_v5.txt new file mode 100644 index 0000000..2828878 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/30x30_empty_v5.txt @@ -0,0 +1,30 @@ +############################## +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +############################## \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/30x30_empty_v6.txt b/skorohodovsa/task_2/source/templates/30x30_empty_v6.txt new file mode 100644 index 0000000..2828878 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/30x30_empty_v6.txt @@ -0,0 +1,30 @@ +############################## +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +############################## \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/30x30_empty_v7.txt b/skorohodovsa/task_2/source/templates/30x30_empty_v7.txt new file mode 100644 index 0000000..2828878 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/30x30_empty_v7.txt @@ -0,0 +1,30 @@ +############################## +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +############################## \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/30x30_empty_v8.txt b/skorohodovsa/task_2/source/templates/30x30_empty_v8.txt new file mode 100644 index 0000000..2828878 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/30x30_empty_v8.txt @@ -0,0 +1,30 @@ +############################## +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +############################## \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/30x30_empty_v9.txt b/skorohodovsa/task_2/source/templates/30x30_empty_v9.txt new file mode 100644 index 0000000..2828878 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/30x30_empty_v9.txt @@ -0,0 +1,30 @@ +############################## +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +############################## \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/50x50_deadends_v1.txt b/skorohodovsa/task_2/source/templates/50x50_deadends_v1.txt new file mode 100644 index 0000000..5c91bce --- /dev/null +++ b/skorohodovsa/task_2/source/templates/50x50_deadends_v1.txt @@ -0,0 +1,49 @@ +################################################# +#S # # # # # # # # +##### # ### # ### # # # # ### ##### # # ####### # +# # # # # # # # # # # +### ##### ### # ################# ############# # +# # # # # # # # # +# ##### # # # ########### ##### # ##### # # ##### +# # # # # # # # # # # # +# ### ### # ### ########### ######### ### ### # # +# # # # # # # # # # # # +# # ####### ### # ##### ##### # # ##### # # ### # +# # # # # # # # # # # # +##### # ##### ### ### ######### # ### ### ##### # +# # # # # # # # # # # +# ####### # # ##### ##### ### ### # ### ######### +# # # # # # # # # # # # # +# ####### # # # # # # # ############# # # ##### # +# # # # # # # # # # # # # +### # # ####### # # ### ### # # ####### ### # # # +# # # # # # # # # # # # # # # # +# ########### ### ### # # ##### ### # # ##### ### +# # # # # # # # # # # # +# # ##### # ####### # # ######### ######### ### # +# # # # # # # # # # # # +# ### # ##### ##### # # # # # # # # ######### # # +# # # # # # # # # # # # # # # +# # # ########### # ##### # ### # # # ######### # +# # # # # # # # # # # # # +# ##### # ### ### # # # # ########### # ### ##### +# # # # # # # # # # # # # # +# ### # # # ### # ######### # # ##### ### ### # # +# # # # # # # # # # # # # # +### # # ### # # ### # ### # ##### # ### ####### # +# # # # # # # # # # # # # # # # +# ####### ### # # # # # ####### # ### ####### # # +# # # # # # # # # # # # # # +##### # # ##### ### ### ### # ### # # # # # ### # +# # # # # # # # # # # # # # # +### # ### # # ### ### ####### # ##### # ##### # # +# # # # # # # # # # # # # # +# # ### # # ### # ####### ##### ### # # ### # # # +# # # # # # # # # # # # # # # # # +# ##### ### # ##### # # ##### ### ### ### ### # # +# # # # # # # # # # # # # # # +### # ### ### # # # # ### # ### ### ### ### ### # +# # # # # # # # # # # # # # # # # +# ######### # # # # # # ### # ### ### # # # ##### +# # # # # # E# +################################################# \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/50x50_deadends_v10.txt b/skorohodovsa/task_2/source/templates/50x50_deadends_v10.txt new file mode 100644 index 0000000..72fd3d8 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/50x50_deadends_v10.txt @@ -0,0 +1,49 @@ +################################################# +#S# # # # # +# # ##### # # ### ########### ####### ### ####### +# # # # # # # # # # # # +##### # # ### # ### ### # ##### ####### # # ### # +# # # # # # # # # # # # # +# ### # ### # ####### # # ####### ######### # ### +# # # # # # # # # # # # +# # ### # ### # # ####### # # # ############### # +# # # # # # # # # # # # # # +# ### ### ##### # # # ########### ####### # # # # +# # # # # # # # # # # # +### ### # # ##### ######### ### # # ### ### ### # +# # # # # # # # # # # # # # # +# ### # # # # ##### ##### # # ### # # ### ##### # +# # # # # # # # # # # # # # # # # +# ### # ##### ##### ### ##### # # ### # ### # ### +# # # # # # # # # # # # +# # ##### ### # ##### ####### # ### ######### # # +# # # # # # # # # # # # # +# ### # ### # ##### # # # ##### # ### ##### ### # +# # # # # # # # # # # # # # +####### # # ######### ####### # ### ### ##### # # +# # # # # # # # # # # # +### ### # ### # ### # # ### ############# # ### # +# # # # # # # # # # # # # # +# ### ### # # # # # ##### ##### # ##### # # ### # +# # # # # # # # # # # # # # +# # # # # ### ##### # # ### ######### # # ##### # +# # # # # # # # # # # # # # +# # ### ### ### ##### ### ### # ##### # ### ### # +# # # # # # # # # # # # # # # # # +# ### # # ### # # # # # ####### # # ##### ### # # +# # # # # # # # # # # # # # # +# # ##### ### # # ##### ##### # # ##### # # ### # +# # # # # # # # # # # # # # # # +# ##### ### ##### # # ### # ### ##### ##### # # # +# # # # # # # # # # # # +####### # ######### # # ####### # ### # ### ##### +# # # # # # # # # # # # +### ##### ####### ####### ##### # # ### # ##### # +# # # # # # # # # # # # +# ### ##### ### # # # ##### # # # # ########### # +# # # # # # # # # # # +# # ### # ##### ####### ############# # ####### # +# # # # # # # # # # # # +# ### # ##### ### ### ##### # # # ####### # # ### +# # # # # # # E# +################################################# \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/50x50_deadends_v2.txt b/skorohodovsa/task_2/source/templates/50x50_deadends_v2.txt new file mode 100644 index 0000000..167abe4 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/50x50_deadends_v2.txt @@ -0,0 +1,49 @@ +################################################# +#S # # # # # # # # # +### ### ##### ### # # # # # # # ### # ### ### # # +# # # # # # # # # # # # # # # # # # # +# # # ### # ### # # # # # # ##### ##### ### # # # +# # # # # # # # # # # # # # # +# ##### ### ####### # # ####### ### ##### # # # # +# # # # # # # # # # # +# # ####### # ####### ### ### ### ### ######### # +# # # # # # # # # # # # # # +# # # ### # # ##### # # # ##### ### ### # ### ### +# # # # # # # # # # # # # +####### ######### # ####### # ### # ########### # +# # # # # # # # # # +# ### # # ##### # ########### # # ####### # ### # +# # # # # # # # # # # # # # +### ####### # # ### # ### # ### ##### ####### # # +# # # # # # # # # # # # +# ### # ##### ### # ### ### ####### ### ### # # # +# # # # # # # # # # # # +# # ########### # # # ### ### ### ############# # +# # # # # # # # # # # # # # +# ### # ######### # # # ### ### ### # # # ### # # +# # # # # # # # # # # # # # # # +####### # ####### # # ##### # ### ##### # # # ### +# # # # # # # # # +# ####### ######### ### ##### # ### ########### # +# # # # # # # # # # # # # +# ### # # ### # # ### ##### ##### ### ####### # # +# # # # # # # # # # # # # +# # # ##### ##### # # # # ####### # ### ####### # +# # # # # # # # # # # # # # # # +### ### ##### # # # # # # ### # ##### # ### ### # +# # # # # # # # # # # # # # # +# ### ######### ### # # ### # # ##### ### ##### # +# # # # # # # # # # # +# # ########### # # ##### ### ### ##### ### # ### +# # # # # # # # # # # # # +# ######### # ##### # ##### ### # # # ##### ### # +# # # # # # # # # # # +# # ### ### ########### ######### # ######### ### +# # # # # # # # # # # # +# ### # # # # ##### ####### # ####### # # # ### # +# # # # # # # # # # # # # # # +### # # ######### ### # # # # # ### ##### ### # # +# # # # # # # # # # # # # # # +# ### ### # ### # # ##### # ##### ##### ### ### # +# # # # # # E# +################################################# \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/50x50_deadends_v3.txt b/skorohodovsa/task_2/source/templates/50x50_deadends_v3.txt new file mode 100644 index 0000000..343eefa --- /dev/null +++ b/skorohodovsa/task_2/source/templates/50x50_deadends_v3.txt @@ -0,0 +1,49 @@ +################################################# +#S# # # # # # +# ##### # # # ####### # ##### ##### ### ##### # # +# # # # # # # # # # # # # +### # ### # ##### ##### # # ######### ### # ### # +# # # # # # # # # # # # # +# ##### ##### # # # ######### ##### # ##### # ### +# # # # # # # # # # # # # # # # +# # # ### # # # ##### # # # # # ##### ### # ### # +# # # # # # # # # # # # # # +# ##### # # ### # # ########### # ####### ### ### +# # # # # # # # # # # # +# # ########### ### # ######### # # ########### # +# # # # # # # # # # # # +# ##### # # # ### ### ####### # ##### # ### # # # +# # # # # # # # # # # # # +##### ########### # ####### ##### # # ### # ##### +# # # # # # # # # # +### ### # ######### ############### # # ####### # +# # # # # # # # # # +# ######### ### ##### # ### ### # ### # # ##### # +# # # # # # # # # # # # +# ##### # # # ### ### ### # # ### ##### # ### ### +# # # # # # # # # # # # # # # +##### # ##### # ### ##### # # # ### ##### # ### # +# # # # # # # # # # # # # +# # # ### # ########### ### # ######### ##### # # +# # # # # # # # # # # # # # +# ### # # # # ####### # ### # # ######### ####### +# # # # # # # # # # # # # # +# # ##### ### ##### ##### ### ### # # # ### ### # +# # # # # # # # # # # # # # # # +# # # # # # ### # ### # ##### # # # # # # ### # # +# # # # # # # # # # # # # # # +##### ### ####### # ##### # ######### ####### # # +# # # # # # # # # # # +# ##### ### ### # ######### # # ####### # ##### # +# # # # # # # # # # # # +### # ##### # ##### ### ##### ##### # ##### ### # +# # # # # # # # # # # # # +# # ### # ### # ### ##### # ####### # # ### # ### +# # # # # # # # # # # # # # +# ### ##### ##### ### ##### # ######### # ##### # +# # # # # # # # # +# ##### ##### # ### ### ####### ################# +# # # # # # # # # # +##### ### ####### ### ####### ### # ##### ### # # +# # # # #E# +################################################# \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/50x50_deadends_v4.txt b/skorohodovsa/task_2/source/templates/50x50_deadends_v4.txt new file mode 100644 index 0000000..d3d86bc --- /dev/null +++ b/skorohodovsa/task_2/source/templates/50x50_deadends_v4.txt @@ -0,0 +1,49 @@ +################################################# +#S# # # # # # # +# ### # # ### ##### ##### ### ### # # # # ####### +# # # # # # # # # # # # # +####### ### ##### ### ##### # # # ### # ####### # +# # # # # # # # # # # # # +# # ##### ### # ### ### ### ### # # ### # ### # # +# # # # # # # # # # # # # # # # +# # # # ### # ### ### # ####### # ####### ##### # +# # # # # # # # # # # # # # +### # ####### # # # ### # ############# ### # # # +# # # # # # # # # # # # +# # ####### ### # ######### ### # ### ### ##### # +# # # # # # # # # # # # # # +# ##### # ### # # # # # ########### ### # # # ### +# # # # # # # # # # # # # # # +# ### ### # ##### # # # # # ##### ### # ####### # +# # # # # # # # # # # # # # +# # ### ####### ### ##### # # # # # ######### # # +# # # # # # # # # # # # # # +##### ### # ##### ######### # # ####### ### ### # +# # # # # # # # # # # # # +# ### # ### # # # # # # ##### ##### # ### ### # # +# # # # # # # # # # # # # # # # +# # ### # ##### # # # ##### ### ##### # # # ### # +# # # # # # # # # # # # # # # # # # +# # # # # # # ##### ### # ### # # # ##### # # ### +# # # # # # # # # # # # # # # # +# # # # ##### ### # # ### # ### # ##### ####### # +# # # # # # # # # # # # # # # +# # # ######### # ### # ### ##### # # # # ####### +# # # # # # # # # # # # # +# # ##### # # # ### ### ### ######### ##### # ### +# # # # # # # # # # # # # +# ######### # ### ### ### # ##### # ### ### ### # +# # # # # # # # # # # # # +# # # ####### # # # ######### # ##### ### ### # # +# # # # # # # # # # # # # # # # +### # ####### # # # # # # # ### # # ### # ### # # +# # # # # # # # # # # # # # # # # # +# ####### # ### # # # # ### # ### # # ### # ### # +# # # # # # # # # # # # # +# # ### ##### ####### ####### # # ####### # ### # +# # # # # # # # # # # # # +# ### ####### # # # ####### ### ### # # ##### ### +# # # # # # # # # # # # # # # # +# # # # # ##### ####### # ##### # # # ##### ### # +# # # # # # E# +################################################# \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/50x50_deadends_v5.txt b/skorohodovsa/task_2/source/templates/50x50_deadends_v5.txt new file mode 100644 index 0000000..3a8fffb --- /dev/null +++ b/skorohodovsa/task_2/source/templates/50x50_deadends_v5.txt @@ -0,0 +1,49 @@ +################################################# +#S# # # # # # # +# ### # # # # # # ### # ##### ####### ### # ### # +# # # # # # # # # # # # # # # +# # ####### # ######### # # ##### # ### ### # ### +# # # # # # # # # # # # # # +##### # # # # ##### # ### # ####### # # # ##### # +# # # # # # # # # # # # # # # # +# # ### # # # ### ####### # # # # ### ##### ##### +# # # # # # # # # # # +# ### ##### ####### # # ########### # ### ##### # +# # # # # # # # # # # # # +# # ### ##### ##### # ### ### ######### ##### ### +# # # # # # # # # # +# ####### ### ####### # ############# # ####### # +# # # # # # # # # # # +# # # # ### # ### # # ### # ############### # ### +# # # # # # # # # # # # # +# # # ### ##### ### ### # ### # ########### ### # +# # # # # # # # # # # # # # # # +####### ### # # # ### # ### # ### # # ### # # # # +# # # # # # # # # # # # # # # +### # ### # ####### # ### ##### # ##### ### # # # +# # # # # # # # # # # # # # +# # # ### ### ### # # ##### ##### # # ### ####### +# # # # # # # # # # # # # +# ##### ### ### ### ### # # # ######### ####### # +# # # # # # # # # # # # # +# # ##### # # ##### # ### ####### # # # # # ##### +# # # # # # # # # # # # # # +# ### ### # ##### ##### ### # ##### # ### ##### # +# # # # # # # # # # # # # # # +# ### # ##### ##### # # # # ### ####### # # # ### +# # # # # # # # # # # # # # +# # ########### # ### ### ####### ### # # # ### # +# # # # # # # # # # # # +# ##### ########### ### # ### ##### # # # ##### # +# # # # # # # # # # # # # +### # # # # ####### ##### # ####### ####### ##### +# # # # # # # # # # # +# ### # # # # ### ############### # ##### ##### # +# # # # # # # # # # # # +# # # ##### # # ######### # # ####### # ####### # +# # # # # # # # # # # # # # +# # # # # # # ##### # # ####### ### ####### ### # +# # # # # # # # # # # # # # # +# # # # # ##### # # # # ##### ### # # ### ####### +# # # # # # # # E# +################################################# \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/50x50_deadends_v6.txt b/skorohodovsa/task_2/source/templates/50x50_deadends_v6.txt new file mode 100644 index 0000000..97806a1 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/50x50_deadends_v6.txt @@ -0,0 +1,49 @@ +################################################# +#S # # # # # # # +### # ### ######### ####### ##### ### # # # # # # +# # # # # # # # # # # # # +# ##### # # ##### ##### # ### # # # ### ### ##### +# # # # # # # # # # # # +############# ####### ####### ####### # ##### # # +# # # # # # # # # # +### # # ### ### # # # # ####### ######### ##### # +# # # # # # # # # # # # # # +# ### # # ##### # ##### # ### ### # ### ### ### # +# # # # # # # # # # # # # # +# ##### # ######### # # ### # # ### # ### # # ### +# # # # # # # # # # # # # # # # # +# # # ### # # # ### ####### ### ### # ####### # # +# # # # # # # # # # # # # +# # ### ######### ### ### # # # # ##### ### ### # +# # # # # # # # # # # # # # # +### # ######### # # ### ### # # # # # ### # # ### +# # # # # # # # # # # # # # # # +# ##### # # # # # ### # # # ####### ### ####### # +# # # # # # # # # # # # # +# # ##### # ### ####### # ### ### ########### # # +# # # # # # # # # # # # # +# ### ### # # # # ### # ##### # ##### # ####### # +# # # # # # # # # # # # # # # # # +# ### # # # # # ### # ##### ####### # ### # # ### +# # # # # # # # # # # # # # +# # ### # # ####### ### # ####### # ####### ### # +# # # # # # # # # # # # # # +##### # # ##### # # # ####### # # # ### ### # # # +# # # # # # # # # # # # # # +# ### ### # # # ######### ### # ##### ### ####### +# # # # # # # # # # # +# # ### ### # ############# ####### ### ####### # +# # # # # # # # # # # +# ### ####### ##### # # ######### ### ##### # # # +# # # # # # # # # # # # # # +# # # # ### ##### # ### # ### # ### ### ######### +# # # # # # # # # # # # # +### # ### ### ### ### ####### # ##### ### # # # # +# # # # # # # # # # # # # # +##### ##### ####### # # # ##### # # ####### ### # +# # # # # # # # # # +# ##### ##### ### ##### ##### ####### # ##### ### +# # # # # # # # # # # # # # # +# ### ### ####### # # # # # ### # # ### # ### # # +# # # # # # # E# +################################################# \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/50x50_deadends_v7.txt b/skorohodovsa/task_2/source/templates/50x50_deadends_v7.txt new file mode 100644 index 0000000..cf354c0 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/50x50_deadends_v7.txt @@ -0,0 +1,49 @@ +################################################# +#S# # # # # # +# ##### # ##### # ### ####### # # ### # ##### ### +# # # # # # # # # # # # # # # +# # # ##### ### # # ### ### # ##### # ##### ### # +# # # # # # # # # # # # # # +##### # ##### ### ### ### # ### ######### # # # # +# # # # # # # # # # # # # +# ##### # ##### ######### # ##### # # # # ##### # +# # # # # # # # # # # +### # # ##### # ### ####### # ####### ########### +# # # # # # # # # # # +# ####### # ##### ####### # ### ### # # ####### # +# # # # # # # # # # # # # # +### # ######### ### # # ### # ### # ### # ##### # +# # # # # # # # # # # # # # +# ##### # # # ### # # ####### ### ####### # # # # +# # # # # # # # # # # # # # +##### ##### # # ####### # ##### ##### # ### # ### +# # # # # # # # # # # # # # # +# ##### # ### ### # # # # # ### # # # ### # ### # +# # # # # # # # # # # # # # # # # +# # # ####### ### # ##### ### # # ##### ### # # # +# # # # # # # # # # # +# ### ##### ### ### ########### ### # ### ### # # +# # # # # # # # # # # # # # +### ### # ### ### ##### # # ##### # ### ### # # # +# # # # # # # # # # # # # # # # # +# # # # ### ### ### # # ##### ##### ### ### ### # +# # # # # # # # # # # # # # +# ### ####### ####### ### # ### # ### ### ### # # +# # # # # # # # # # +####### # ##### # # ####### ####### ### # # ### # +# # # # # # # # # # # # # # # +# # # ##### # # ### # # ##### # # ### ### ### ### +# # # # # # # # # # # # # +# ####### ### ### ### ### # ########### ### ### # +# # # # # # # # # # # # # # +# ### # ##### # ### ### # ### ####### # # # # ### +# # # # # # # # # # # # # +########### # # # ### # ####### # # # # ####### # +# # # # # # # # # # # # # +# ### ### # ##### # # ##### ##### # # ### ##### # +# # # # # # # # # # # # # # +### # ####### # ### ### # ################# ### # +# # # # # # # # # # # +# ### # # ####### # # ### # ##### # ### ##### ### +# # # # # # # E# +################################################# \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/50x50_deadends_v8.txt b/skorohodovsa/task_2/source/templates/50x50_deadends_v8.txt new file mode 100644 index 0000000..dc4c4d8 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/50x50_deadends_v8.txt @@ -0,0 +1,49 @@ +################################################# +#S# # # # # # +# # ####### # # ####### ### # # ##### ### # ##### +# # # # # # # # # # # # # +####### ############# # # ##### # # ### ####### # +# # # # # # # # # # +### # ### ##### ####### # # # # ######### ####### +# # # # # # # # # # # # +# ### # # # # # # # ############# ##### ##### # # +# # # # # # # # # # # # # # +### ##### # ##### ### ####### # ### # ### # ### # +# # # # # # # # # # # # # # +# ##### # ##### # # ### ### # # # ### # # ##### # +# # # # # # # # # # # # # # # # +# # # ##### # ##### # ### # ##### # ##### # ##### +# # # # # # # # # # # # # # +# # # # # ### # ##### # ######### # # ####### # # +# # # # # # # # # # # # # +# # ############# # ##### # ### # ##### ### ### # +# # # # # # # # # # # # +# ##### # ##### # ########### ####### # # ### ### +# # # # # # # # # # # +# ####### # # ############# # ### ######### # # # +# # # # # # # # # # # +##### # ### ### ############# # ### # # # ##### # +# # # # # # # # # # # # # +# ##### # # # ### ##### ### ### ####### # # ##### +# # # # # # # # # # # # # # +### # ### ### # ### # ### # # # # ########### # # +# # # # # # # # # # # # +# ##### ######### ########### ##### ##### # ### # +# # # # # # # # # # # +### ### # # ######### ### # ### ##### ##### # # # +# # # # # # # # # # # # +# ### ########### # ##### ######### # # ####### # +# # # # # # # # # # +# ##### ### # # ### # ### # ######### ##### ### # +# # # # # # # # # # # # +######### ########### # ##### # # ##### ##### ### +# # # # # # # # # # # +### # ##### # # ####### ### ##### # ##### # ### # +# # # # # # # # # # # # # +# # ### # # # # # ### ############### # # ####### +# # # # # # # # # # # # # # # +# ### ##### # # # # ####### # ### # # ### ##### # +# # # # # # # # # # # # # +# ##### ##### ##### # ### # ### ########### # # # +# # # # # E# +################################################# \ No newline at end of file diff --git a/skorohodovsa/task_2/source/templates/50x50_deadends_v9.txt b/skorohodovsa/task_2/source/templates/50x50_deadends_v9.txt new file mode 100644 index 0000000..82a5537 --- /dev/null +++ b/skorohodovsa/task_2/source/templates/50x50_deadends_v9.txt @@ -0,0 +1,49 @@ +################################################# +#S # # # # # # # +##### # # ##### # # # # # # ### # # ##### ##### # +# # # # # # # # # # # # # # # +# ####### # # # # # # ##### # ####### ######### # +# # # # # # # # # # # # # # +########### ### # ### # # ######### ### # # ##### +# # # # # # # # # # # # +# ### # # # ##### # ### # # ######### ### ##### # +# # # # # # # # # # # # # # # +# # ##### # # ### ##### # # # ### # # # ##### # # +# # # # # # # # # # # # # # +# # ####### ### # # ##### # ### ####### ####### # +# # # # # # # # # # # # # +# ####### ### # ####### # ### ##### ##### # # # # +# # # # # # # # # # # # # # +# # # # # ####### ### # # # ######### # # # # # # +# # # # # # # # # # # # # # # # +# ##### ########### ### # # # # # # ### ##### # # +# # # # # # # # # # # # # +##### # # ##### ##### ### ##### ##### ##### # ### +# # # # # # # # # # # +# ### ##### ##### # # # ######### ### # ####### # +# # # # # # # # # # # # # +# # # # ##### ### ##### # # ####### ### # ####### +# # # # # # # # # # # # # # +# # # ##### ######### ### ### # # # # ### ##### # +# # # # # # # # # # # # +# ### ############# ### # # # # ####### ####### # +# # # # # # # # # # # # # +### ### ### ### # # # ### ####### ### # # ### # # +# # # # # # # # # # # # # # +# ### ### ### ####### # ### # # ### ### ### ##### +# # # # # # # # # # # # # +# ######### ##### # ### # ### ### ####### # ### # +# # # # # # # # # # # # # +# ### # ### # # # # ### ### ### # # # ########### +# # # # # # # # # # # # # # +### # # # # ### # ########### # ######### ##### # +# # # # # # # # # # +# ### # ####### ####################### # # # # # +# # # # # # # # # # +# ####### ### # # ### ######### # ######### # ### +# # # # # # # # # # # # # +# # ####### ##### # ### ### ####### # # ####### # +# # # # # # # # # # # # # # +# ####### ##### # ### # # ### # # # ####### # # # +# # # # # # E# +################################################# \ No newline at end of file diff --git a/skorohodovsa/task_2/source/view/__init__.py b/skorohodovsa/task_2/source/view/__init__.py new file mode 100644 index 0000000..cad01a4 --- /dev/null +++ b/skorohodovsa/task_2/source/view/__init__.py @@ -0,0 +1,13 @@ +from source.view.observer import Observer, ConsoleView, Event +from source.view.command import Player, Command, MoveCommand, CommandHistory, DIRECTIONS + +__all__ = [ + "Observer", + "ConsoleView", + "Event", + "Player", + "Command", + "MoveCommand", + "CommandHistory", + "DIRECTIONS", +] diff --git a/skorohodovsa/task_2/source/view/command.py b/skorohodovsa/task_2/source/view/command.py new file mode 100644 index 0000000..fe4c304 --- /dev/null +++ b/skorohodovsa/task_2/source/view/command.py @@ -0,0 +1,158 @@ +from abc import ABC, abstractmethod +from typing import Optional + +from source.models.base import Maze, Cell + + +# ---------------------------------------------------------------------------- # +# Игрок # +# ---------------------------------------------------------------------------- # + + +class Player: + """Хранит текущее положение игрока в лабиринте. + + Attributes: + cell: Текущая клетка игрока. + """ + + def __init__(self, cell: Cell) -> None: + """Инициализирует игрока на заданной клетке. + + Args: + cell: Начальная клетка игрока. + """ + self.cell = cell + + def __repr__(self) -> str: + return f"Player(x={self.cell.x}, y={self.cell.y})" + + +# ---------------------------------------------------------------------------- # +# Интерфейс команды # +# ---------------------------------------------------------------------------- # + + +class Command(ABC): + """Интерфейс команды с поддержкой отмены.""" + + @abstractmethod + def execute(self) -> bool: + """Выполняет команду. + + Returns: + True если команда выполнена успешно, False иначе. + """ + + @abstractmethod + def undo(self) -> None: + """Отменяет команду, восстанавливая предыдущее состояние.""" + + +# ---------------------------------------------------------------------------- # +# Команда перемещения # +# ---------------------------------------------------------------------------- # + +DIRECTIONS = { + "w": (0, -1), + "s": (0, 1), + "a": (-1, 0), + "d": (1, 0), +} + + +class MoveCommand(Command): + """Перемещает игрока в заданном направлении. + + Сохраняет предыдущую клетку для возможности отмены хода. + """ + + def __init__(self, player: Player, direction: str, maze: Maze) -> None: + """Инициализирует команду перемещения. + + Args: + player: Объект игрока. + direction: Направление ('w', 'a', 's', 'd'). + maze: Объект лабиринта для проверки проходимости. + + Raises: + ValueError: Если направление не распознано. + """ + if direction not in DIRECTIONS: + raise ValueError( + f"Неизвестное направление '{direction}'. Используй: w/a/s/d" + ) + + self._player = player + self._direction = direction + self._maze = maze + self._prev_cell: Optional[Cell] = None + + def execute(self) -> bool: + """Перемещает игрока если целевая клетка проходима. + + Returns: + True если перемещение выполнено, False если клетка непроходима. + """ + dx, dy = DIRECTIONS[self._direction] + target = self._maze.get_cell( + self._player.cell.x + dx, + self._player.cell.y + dy, + ) + + if target is None or not target.is_possible(): + return False + + self._prev_cell = self._player.cell + self._player.cell = target + return True + + def undo(self) -> None: + """Возвращает игрока на предыдущую клетку.""" + if self._prev_cell is not None: + self._player.cell = self._prev_cell + + +# ---------------------------------------------------------------------------- # +# История команд # +# ---------------------------------------------------------------------------- # + + +class CommandHistory: + """Хранит историю выполненных команд и позволяет отменять их. + + Example: + history = CommandHistory() + cmd = MoveCommand(player, 'w', maze) + if cmd.execute(): + history.push(cmd) + + history.undo() # отменяет последний успешный ход + """ + + def __init__(self) -> None: + self._history: list[Command] = [] + + def push(self, command: Command) -> None: + """Добавляет выполненную команду в историю. + + Args: + command: Успешно выполненная команда. + """ + self._history.append(command) + + def undo(self) -> bool: + """Отменяет последнюю команду из истории. + + Returns: + True если отмена выполнена, False если история пуста. + """ + if not self._history: + print("Нечего отменять.") + return False + self._history.pop().undo() + return True + + def clear(self) -> None: + """Очищает историю команд.""" + self._history.clear() diff --git a/skorohodovsa/task_2/source/view/observer.py b/skorohodovsa/task_2/source/view/observer.py new file mode 100644 index 0000000..c5df3f7 --- /dev/null +++ b/skorohodovsa/task_2/source/view/observer.py @@ -0,0 +1,115 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Optional + +from source.models.base import Maze, Cell +from source.settings import cell_mapping + + +# ---------------------------------------------------------------------------- # +# События # +# ---------------------------------------------------------------------------- # + + +@dataclass +class Event: + """Событие, передаваемое наблюдателям. + + Attributes: + type: Тип события ('maze_loaded', 'path_found', 'move', 'no_path'). + payload: Дополнительные данные события. + """ + + type: str + payload: dict = None + + +# ---------------------------------------------------------------------------- # +# Интерфейс наблюдателя # +# ---------------------------------------------------------------------------- # + + +class Observer(ABC): + """Интерфейс наблюдателя за событиями лабиринта.""" + + @abstractmethod + def update(self, event: Event) -> None: + """Обрабатывает входящее событие. + + Args: + event: Объект события с типом и данными. + """ + + +# --------------------------------------------------------------------------- +# Консольный наблюдатель +# --------------------------------------------------------------------------- + + +class ConsoleView(Observer): + """Отображает состояние лабиринта и события в консоли.""" + + # Символ игрока на карте + PLAYER_SYMBOL = "P" + PATH_SYMBOL = "·" + + def update(self, event: Event) -> None: + """Реагирует на события и выводит информацию в консоль. + + Args: + event: Объект события. + """ + match event.type: + case "maze_loaded": + print("Лабиринт загружен.") + self.render(event.payload["maze"]) + case "path_found": + print(f"Путь найден! Длина: {event.payload['length']} шагов.") + self.render( + event.payload["maze"], + path=event.payload["path"], + ) + case "no_path": + print("Путь не найден.") + case "move": + print(f"Ход: {event.payload['direction']}") + self.render( + event.payload["maze"], + player=event.payload["player_cell"], + path=event.payload.get("path"), + ) + case _: + print(f"[событие] {event.type}") + + def render( + self, + maze: Maze, + player: Optional[Cell] = None, + path: Optional[list[Cell]] = None, + ) -> None: + """Рисует лабиринт в консоли. + + Путь отмечается символом '·', позиция игрока — 'P'. + + Args: + maze: Объект лабиринта. + player: Текущая клетка игрока (опционально). + path: Список клеток найденного пути (опционально). + """ + path_set = set(path) if path else set() + rows, cols = maze.shape + + print("+" + "─" * cols + "+") + for y in range(rows): + row_str = "|" + for x in range(cols): + cell = maze[y, x] + if player and cell is player: + row_str += self.PLAYER_SYMBOL + elif cell in path_set: + row_str += self.PATH_SYMBOL + else: + row_str += str(cell) + row_str += "|" + print(row_str) + print("+" + "─" * cols + "+") diff --git a/skorohodovsa/task_2/test/play.py b/skorohodovsa/task_2/test/play.py new file mode 100644 index 0000000..d46a16c --- /dev/null +++ b/skorohodovsa/task_2/test/play.py @@ -0,0 +1,62 @@ +from source.build.builder import TextFileBuilder +from source.models.base import Maze +from source.view.observer import ConsoleView, Event +from source.view.command import Player, MoveCommand, CommandHistory + + +maze: Maze = TextFileBuilder().build_from_file("source/templates/10x10_path_v1.txt") + +rows, cols = maze.shape + +start = None +for y in range(rows): + for x in range(cols): + if maze[y, x].is_start: + start = maze[y, x] + break + +if start is None: + print("Стартовая клетка не найдена!") + exit() + +player = Player(start) +history = CommandHistory() +view = ConsoleView() + +view.update(Event("maze_loaded", {"maze": maze})) +print("Управление: w/a/s/d — движение, z — отмена, q — выход\n") + +while True: + key = input(">>> ").strip().lower() + + if key == "q": + print("Выход.") + break + + elif key == "z": + if history.undo(): + print("Ход отменён.") + view.render(maze, player=player.cell) + + elif key in ("w", "a", "s", "d"): + cmd = MoveCommand(player, key, maze) + if cmd.execute(): + history.push(cmd) + view.update( + Event( + "move", + { + "maze": maze, + "player_cell": player.cell, + "direction": key, + }, + ) + ) + if player.cell.is_exit: + print("Выход найден! Победа!") + break + else: + print("Туда нельзя — стена или граница.") + + else: + print("Неизвестная команда. Используй: w/a/s/d, z, q") diff --git a/skorohodovsa/task_2/test/test_base.py b/skorohodovsa/task_2/test/test_base.py new file mode 100644 index 0000000..56e1e3d --- /dev/null +++ b/skorohodovsa/task_2/test/test_base.py @@ -0,0 +1,217 @@ +import pytest +import random + +random.seed("РФ СЛФ!") + +from source.models.base import Cell, Maze +from source.settings import cell_mapping + + +class TestCellCreation: + """Тесты создания клетки и начальных значений.""" + + def test_coordinates_are_set(self): + cell = Cell(3, 7) + assert cell.x == 3 + assert cell.y == 7 + + def test_default_flags_are_false(self): + cell = Cell(0, 0) + assert cell.is_wall is False + assert cell.is_start is False + assert cell.is_exit is False + + def test_create_wall(self): + cell = Cell(0, 0, is_wall=True) + assert cell.is_wall is True + + def test_create_start(self): + cell = Cell(0, 0, is_start=True) + assert cell.is_start is True + + def test_create_exit(self): + cell = Cell(0, 0, is_exit=True) + assert cell.is_exit is True + + +class TestCellIsPassable: + """Тесты метода is_possible.""" + + def test_empty_cell_is_passable(self): + cell = Cell(0, 0) + assert cell.is_possible() is True + + def test_wall_is_not_passable(self): + cell = Cell(0, 0, is_wall=True) + assert cell.is_possible() is False + + def test_start_cell_is_passable(self): + cell = Cell(0, 0, is_start=True) + assert cell.is_possible() is True + + def test_exit_cell_is_passable(self): + cell = Cell(0, 0, is_exit=True) + assert cell.is_possible() is True + + +class TestCellFlagsAreMutuallyExclusive: + """Тесты взаимного исключения флагов.""" + + def test_set_wall_clears_start(self): + cell = Cell(0, 0, is_start=True) + cell.is_wall = True + assert cell.is_start is False + assert cell.is_wall is True + + def test_set_wall_clears_exit(self): + cell = Cell(0, 0, is_exit=True) + cell.is_wall = True + assert cell.is_exit is False + assert cell.is_wall is True + + def test_set_start_clears_wall(self): + cell = Cell(0, 0, is_wall=True) + cell.is_start = True + assert cell.is_wall is False + assert cell.is_start is True + + def test_set_start_clears_exit(self): + cell = Cell(0, 0, is_exit=True) + cell.is_start = True + assert cell.is_exit is False + assert cell.is_start is True + + def test_set_exit_clears_wall(self): + cell = Cell(0, 0, is_wall=True) + cell.is_exit = True + assert cell.is_wall is False + assert cell.is_exit is True + + def test_set_exit_clears_start(self): + cell = Cell(0, 0, is_start=True) + cell.is_exit = True + assert cell.is_start is False + assert cell.is_exit is True + + def test_unset_wall_does_not_clear_others(self): + # снятие флага (False) не должно трогать остальные + cell = Cell(0, 0, is_wall=True) + cell.is_wall = False + assert cell.is_start is False + assert cell.is_exit is False + + +class TestCellStr: + """Тесты строкового представления клетки.""" + + def test_str_returns_string(self): + cell = Cell(0, 0) + assert isinstance(str(cell), str) + + def test_repr_contains_coordinates(self): + cell = Cell(4, 9) + assert "4" in repr(cell) + assert "9" in repr(cell) + + +class TestMaze: + def test_default_size(self): + """Проверка размеров лабиринта со значениями по умолчанию""" + maze = Maze() + row, col = maze.shape + assert row == 10 + assert col == 10 + + def test_custom_size(self): + """Проверка размеров лабиринта с заданными размерами""" + maze = Maze(size=(7, 3)) + assert maze._width == 7 + assert maze._height == 3 + + def test_all_cells_empty_on_init(self): + """Проверка создания пустого лабиринта с заданными размерами""" + maze = Maze(size=(3, 3)) + for y in range(3): + for x in range(3): + cell = maze.get_cell(x, y) + assert not cell.is_wall + assert not cell.is_start + assert not cell.is_exit + + def test_get_cell_valid(self): + """Проверка получения объекта Cell из лабиринта функцией `get_cell()`""" + maze = Maze(size=(5, 5)) + assert isinstance(maze.get_cell(2, 3), Cell) + + def test_get_cell_out_of_bounds(self): + """Проверка неправильных указанных индексов лабиринта""" + maze = Maze(size=(5, 5)) + assert maze.get_cell(-1, 0) is None + assert maze.get_cell(0, -1) is None + assert maze.get_cell(5, 0) is None + assert maze.get_cell(0, 5) is None + + def test_center_has_four_neighbors(self): + """Проверка нахождения соседей""" + maze = Maze(size=(5, 5)) + assert len(maze.get_neighbors(2, 2)) == 4 + + def test_corner_has_two_neighbors(self): + """Проверка нахождения соседей, когда указанное поле в углу лабиринта""" + maze = Maze(size=(5, 5)) + assert len(maze.get_neighbors(0, 0)) == 2 + + def test_wall_excluded_from_neighbors(self): + """Проверка что стена не попадает в список соседей""" + maze = Maze(size=(5, 5)) + maze[1, 2] = cell_mapping["wall"] + assert all(not n.is_wall for n in maze.get_neighbors(2, 2)) + + def test_setitem_wall(self): + """Проверка установки стены через оператор []""" + maze = Maze(size=(5, 5)) + maze[0, 0] = cell_mapping["wall"] + assert maze[0, 0].is_wall is True + + def test_setitem_start(self): + """Проверка установки старта через оператор []""" + maze = Maze(size=(5, 5)) + maze[0, 0] = cell_mapping["start"] + assert maze[0, 0].is_start is True + + def test_setitem_exit(self): + """Проверка установки выхода через оператор []""" + maze = Maze(size=(5, 5)) + maze[0, 0] = cell_mapping["exit"] + assert maze[0, 0].is_exit is True + + def test_setitem_empty_clears_flags(self): + """Проверка сброса флагов клетки при установке пустого типа""" + maze = Maze(size=(5, 5)) + maze[0, 0] = cell_mapping["wall"] + maze[0, 0] = cell_mapping["empty"] + assert not maze[0, 0].is_wall + + def test_getitem_out_of_bounds_raises(self): + """Проверка выброса IndexError при обращении к клетке вне границ лабиринта""" + maze = Maze(size=(5, 5)) + with pytest.raises(IndexError): + _ = maze[10, 10] + + def test_setitem_invalid_symbol_raises(self): + """Проверка выброса ValueError при установке неизвестного символа""" + maze = Maze(size=(5, 5)) + with pytest.raises(ValueError): + maze[0, 0] = "?" + + def test_str_lines_match_height(self): + """Проверка что количество строк в строковом представлении совпадает с высотой""" + maze = Maze(size=(4, 6)) + print(str(maze).splitlines()) + assert len(str(maze).splitlines()) == 6 + + def test_str_line_length_matches_width(self): + """Проверка что длина каждой строки в строковом представлении совпадает с шириной""" + maze = Maze(size=(5, 3)) + for line in str(maze).strip().splitlines(): + assert len(line) == 5 diff --git a/skorohodovsa/task_2/test/test_strategies.py b/skorohodovsa/task_2/test/test_strategies.py new file mode 100644 index 0000000..81b211c --- /dev/null +++ b/skorohodovsa/task_2/test/test_strategies.py @@ -0,0 +1,162 @@ +import pytest + +from source.models.base import Maze, Cell +from source.settings import cell_mapping +from source.strategy import BFSStrategy, DFSStrategy, AStarStrategy + + +def make_open_maze(width: int = 5, height: int = 5) -> Maze: + """Открытый лабиринт без внутренних стен, S в углу, E в противоположном.""" + maze = Maze(size=(width, height)) + maze[0, 0] = cell_mapping["start"] + maze[height - 1, width - 1] = cell_mapping["exit"] + return maze + + +def make_blocked_maze() -> Maze: + """Лабиринт где S и E разделены сплошной стеной — пути нет.""" + maze = Maze(size=(5, 5)) + maze[0, 0] = cell_mapping["start"] + maze[4, 4] = cell_mapping["exit"] + for col in range(5): + maze[2, col] = cell_mapping["wall"] + return maze + + +def make_corridor_maze() -> Maze: + """Узкий коридор 1×5: S → . → . → . → E.""" + maze = Maze(size=(5, 1)) + maze[0, 0] = cell_mapping["start"] + maze[0, 4] = cell_mapping["exit"] + return maze + + +STRATEGIES = [BFSStrategy, DFSStrategy, AStarStrategy] +STRATEGY_IDS = ["BFS", "DFS", "A*"] + + +# ---------------------------------------------------------------------------- # +# Общие тесты для всех стратегий # +# ---------------------------------------------------------------------------- # + + +class TestAllStrategies: + @pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS) + def test_returns_list(self, StrategyClass): + """find_path всегда возвращает список.""" + maze = make_open_maze() + result = StrategyClass().find_path(maze) + assert isinstance(result, list) + + @pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS) + def test_path_starts_with_start(self, StrategyClass): + """Первая клетка пути — старт.""" + maze = make_open_maze() + path = StrategyClass().find_path(maze) + assert path[0] is maze.start + + @pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS) + def test_path_ends_with_exit(self, StrategyClass): + """Последняя клетка пути — выход.""" + maze = make_open_maze() + path = StrategyClass().find_path(maze) + assert path[-1] is maze.exit + + @pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS) + def test_path_cells_are_passable(self, StrategyClass): + """Все клетки пути проходимы.""" + maze = make_open_maze() + path = StrategyClass().find_path(maze) + assert all(cell.is_possible() for cell in path) + + @pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS) + def test_path_cells_are_neighbors(self, StrategyClass): + """Каждая следующая клетка пути — сосед предыдущей.""" + maze = make_open_maze() + path = StrategyClass().find_path(maze) + for a, b in zip(path, path[1:]): + assert abs(a.x - b.x) + abs(a.y - b.y) == 1 + + @pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS) + def test_no_path_returns_empty(self, StrategyClass): + """Если пути нет — возвращает пустой список.""" + maze = make_blocked_maze() + path = StrategyClass().find_path(maze) + assert path == [] + + @pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS) + def test_corridor_path_length(self, StrategyClass): + """В коридоре 1×5 путь содержит ровно 5 клеток.""" + maze = make_corridor_maze() + path = StrategyClass().find_path(maze) + assert len(path) == 5 + + @pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS) + def test_maze_not_modified(self, StrategyClass): + """Алгоритм не изменяет состояние лабиринта.""" + maze = make_open_maze() + before = str(maze) + StrategyClass().find_path(maze) + assert str(maze) == before + + +# ---------------------------------------------------------------------------- # +# Тесты специфичные для BFS и A* (оптимальность пути) # +# ---------------------------------------------------------------------------- # + + +class TestOptimalStrategies: + @pytest.mark.parametrize( + "StrategyClass", [BFSStrategy, AStarStrategy], ids=["BFS", "A*"] + ) + def test_shortest_path_in_corridor(self, StrategyClass): + """BFS и A* находят кратчайший путь в коридоре.""" + maze = make_corridor_maze() + path = StrategyClass().find_path(maze) + assert len(path) == 5 + + @pytest.mark.parametrize( + "StrategyClass", [BFSStrategy, AStarStrategy], ids=["BFS", "A*"] + ) + def test_bfs_and_astar_same_length(self, StrategyClass): + """BFS и A* возвращают путь одинаковой длины на открытом лабиринте.""" + maze = make_open_maze(7, 7) + bfs_len = len(BFSStrategy().find_path(maze)) + astar_len = len(AStarStrategy().find_path(maze)) + assert bfs_len == astar_len + + +# ---------------------------------------------------------------------------- # +# Тесты с явной передачей start / exit # +# ---------------------------------------------------------------------------- # + + +class TestExplicitStartExit: + @pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS) + def test_explicit_start_and_exit(self, StrategyClass): + """find_path работает с явно переданными start и exit.""" + maze = Maze(size=(5, 5)) + start = maze.get_cell(0, 0) + exit = maze.get_cell(4, 4) + maze[0, 0] = cell_mapping["start"] + maze[4, 4] = cell_mapping["exit"] + + path = StrategyClass().find_path(maze, start=start, exit=exit) + assert path[0] is start + assert path[-1] is exit + + +class TestEdgeCases: + def test_no_start_raises(self): + """Если нет старта — ValueError.""" + maze = Maze(size=(5, 5)) + maze[4, 4] = cell_mapping["exit"] + with pytest.raises(ValueError): + BFSStrategy().find_path(maze) + + def test_no_exit_raises(self): + """Если нет выхода — ValueError.""" + maze = Maze(size=(5, 5)) + maze[0, 0] = cell_mapping["start"] + with pytest.raises(ValueError): + BFSStrategy().find_path(maze) diff --git a/skorohodovsa/task_2/test/test_text_builder.py b/skorohodovsa/task_2/test/test_text_builder.py new file mode 100644 index 0000000..e69de29