diff --git a/petryainiyas/426.md b/petryainiyas/426.md new file mode 100644 index 0000000..e69de29 diff --git a/petryainiyas/task1/bst.py b/petryainiyas/task1/bst.py new file mode 100644 index 0000000..221fd67 --- /dev/null +++ b/petryainiyas/task1/bst.py @@ -0,0 +1,118 @@ +from typing import Any, Dict, List, Optional + + +Node = Dict[str, Any] + + +def _make_node(name: str, phone: str) -> Node: + return {"name": name, "phone": phone, "left": None, "right": None} + + +def bst_insert(root: Optional[Node], name: str, phone: str) -> Node: + new_node = _make_node(name, phone) + + if root is None: + return new_node + + current = root + parent = None + + while current is not None: + parent = current + if name < current["name"]: + current = current["left"] + elif name > current["name"]: + current = current["right"] + else: + current["phone"] = phone + return root + + if name < parent["name"]: + parent["left"] = new_node + else: + parent["right"] = new_node + + return root + + +def bst_find(root: Optional[Node], name: str) -> Optional[str]: + current = root + while current is not None: + if name < current["name"]: + current = current["left"] + elif name > current["name"]: + current = current["right"] + else: + return current["phone"] + return None + + +def _find_min_node(node: Node) -> Node: + current = node + while current["left"] is not None: + current = current["left"] + return current + + +def bst_delete(root: Optional[Node], name: str) -> Optional[Node]: + if root is None: + return None + + parent = None + current = root + + while current is not None and current["name"] != name: + parent = current + if name < current["name"]: + current = current["left"] + else: + current = current["right"] + + if current is None: + return root + + if current["left"] is None or current["right"] is None: + child = current["left"] if current["left"] is not None else current["right"] + + if parent is None: + return child + + if parent["left"] is current: + parent["left"] = child + else: + parent["right"] = child + return root + + succ_parent = current + successor = current["right"] + while successor["left"] is not None: + succ_parent = successor + successor = successor["left"] + + current["name"] = successor["name"] + current["phone"] = successor["phone"] + + successor_child = successor["right"] + if succ_parent["left"] is successor: + succ_parent["left"] = successor_child + else: + succ_parent["right"] = successor_child + + return root + + +def bst_list_all(root: Optional[Node]) -> List[Dict[str, str]]: + result: List[Dict[str, str]] = [] + stack: List[Node] = [] + current = root + + while current is not None or stack: + while current is not None: + stack.append(current) + current = current["left"] + + current = stack.pop() + result.append({"name": current["name"], "phone": current["phone"]}) + current = current["right"] + + return result diff --git a/petryainiyas/task1/docs/data/delete.png b/petryainiyas/task1/docs/data/delete.png new file mode 100644 index 0000000..1ec4942 Binary files /dev/null and b/petryainiyas/task1/docs/data/delete.png differ diff --git a/petryainiyas/task1/docs/data/find.png b/petryainiyas/task1/docs/data/find.png new file mode 100644 index 0000000..2868e6c Binary files /dev/null and b/petryainiyas/task1/docs/data/find.png differ diff --git a/petryainiyas/task1/docs/data/insert.png b/petryainiyas/task1/docs/data/insert.png new file mode 100644 index 0000000..5c0e4ff Binary files /dev/null and b/petryainiyas/task1/docs/data/insert.png differ diff --git a/petryainiyas/task1/docs/data/results.csv b/petryainiyas/task1/docs/data/results.csv new file mode 100644 index 0000000..4ccfb69 --- /dev/null +++ b/petryainiyas/task1/docs/data/results.csv @@ -0,0 +1,109 @@ +Структура,Режим,Операция,Замер,Время (сек) +LinkedList,случайный,insert,1,4.2622492010 +LinkedList,случайный,find,1,0.0314994130 +LinkedList,случайный,delete,1,0.0149069000 +LinkedList,случайный,insert,2,4.0154580330 +LinkedList,случайный,find,2,0.0393284500 +LinkedList,случайный,delete,2,0.0210732100 +LinkedList,случайный,insert,3,4.0436019780 +LinkedList,случайный,find,3,0.0344933660 +LinkedList,случайный,delete,3,0.0152639850 +LinkedList,случайный,insert,4,3.7182993220 +LinkedList,случайный,find,4,0.0327698850 +LinkedList,случайный,delete,4,0.0149959540 +LinkedList,случайный,insert,5,3.7082228200 +LinkedList,случайный,find,5,0.0303762490 +LinkedList,случайный,delete,5,0.0141406560 +LinkedList,случайный,insert,среднее,3.9495662708 +LinkedList,случайный,find,среднее,0.0336934726 +LinkedList,случайный,delete,среднее,0.0160761410 +HashTable,случайный,insert,1,0.2059865770 +HashTable,случайный,find,1,0.0014966100 +HashTable,случайный,delete,1,0.0006891700 +HashTable,случайный,insert,2,0.2024331460 +HashTable,случайный,find,2,0.0015934880 +HashTable,случайный,delete,2,0.0007212620 +HashTable,случайный,insert,3,0.2126128040 +HashTable,случайный,find,3,0.0016566220 +HashTable,случайный,delete,3,0.0008358420 +HashTable,случайный,insert,4,0.2157934910 +HashTable,случайный,find,4,0.0015542810 +HashTable,случайный,delete,4,0.0007269120 +HashTable,случайный,insert,5,0.2079924580 +HashTable,случайный,find,5,0.0013696990 +HashTable,случайный,delete,5,0.0006616050 +HashTable,случайный,insert,среднее,0.2089636952 +HashTable,случайный,find,среднее,0.0015341400 +HashTable,случайный,delete,среднее,0.0007269582 +BST,случайный,insert,1,0.0166981280 +BST,случайный,find,1,0.0001569360 +BST,случайный,delete,1,0.0000917280 +BST,случайный,insert,2,0.0184119040 +BST,случайный,find,2,0.0001517110 +BST,случайный,delete,2,0.0001163770 +BST,случайный,insert,3,0.0174662270 +BST,случайный,find,3,0.0001582930 +BST,случайный,delete,3,0.0000892660 +BST,случайный,insert,4,0.0191369100 +BST,случайный,find,4,0.0002087170 +BST,случайный,delete,4,0.0001067050 +BST,случайный,insert,5,0.0184276900 +BST,случайный,find,5,0.0002767720 +BST,случайный,delete,5,0.0001067660 +BST,случайный,insert,среднее,0.0180281718 +BST,случайный,find,среднее,0.0001904858 +BST,случайный,delete,среднее,0.0001021684 +LinkedList,отсортированный,insert,1,2.9875078340 +LinkedList,отсортированный,find,1,0.0237300610 +LinkedList,отсортированный,delete,1,0.0111698260 +LinkedList,отсортированный,insert,2,3.0573987940 +LinkedList,отсортированный,find,2,0.0243270360 +LinkedList,отсортированный,delete,2,0.0115366030 +LinkedList,отсортированный,insert,3,2.9641987260 +LinkedList,отсортированный,find,3,0.0236313330 +LinkedList,отсортированный,delete,3,0.0112848510 +LinkedList,отсортированный,insert,4,3.0345914950 +LinkedList,отсортированный,find,4,0.0240271220 +LinkedList,отсортированный,delete,4,0.0112117310 +LinkedList,отсортированный,insert,5,2.9481954700 +LinkedList,отсортированный,find,5,0.0239006100 +LinkedList,отсортированный,delete,5,0.0110857710 +LinkedList,отсортированный,insert,среднее,2.9983784638 +LinkedList,отсортированный,find,среднее,0.0239232324 +LinkedList,отсортированный,delete,среднее,0.0112577564 +HashTable,отсортированный,insert,1,0.1997087560 +HashTable,отсортированный,find,1,0.0017550400 +HashTable,отсортированный,delete,1,0.0008407980 +HashTable,отсортированный,insert,2,0.1968675190 +HashTable,отсортированный,find,2,0.0019886760 +HashTable,отсортированный,delete,2,0.0008920910 +HashTable,отсортированный,insert,3,0.1907563580 +HashTable,отсортированный,find,3,0.0018447440 +HashTable,отсортированный,delete,3,0.0008684640 +HashTable,отсортированный,insert,4,0.2625327630 +HashTable,отсортированный,find,4,0.0016053140 +HashTable,отсортированный,delete,4,0.0008098670 +HashTable,отсортированный,insert,5,0.1936840590 +HashTable,отсортированный,find,5,0.0019015160 +HashTable,отсортированный,delete,5,0.0009053780 +HashTable,отсортированный,insert,среднее,0.2087098910 +HashTable,отсортированный,find,среднее,0.0018190580 +HashTable,отсортированный,delete,среднее,0.0008633196 +BST,отсортированный,insert,1,4.2195800190 +BST,отсортированный,find,1,0.0389314570 +BST,отсортированный,delete,1,0.0190308920 +BST,отсортированный,insert,2,4.1356184250 +BST,отсортированный,find,2,0.0383339310 +BST,отсортированный,delete,2,0.0194247740 +BST,отсортированный,insert,3,4.1204731890 +BST,отсортированный,find,3,0.0388593320 +BST,отсортированный,delete,3,0.0215428460 +BST,отсортированный,insert,4,4.2120902370 +BST,отсортированный,find,4,0.0378190250 +BST,отсортированный,delete,4,0.0188528460 +BST,отсортированный,insert,5,4.1304951260 +BST,отсортированный,find,5,0.0359927840 +BST,отсортированный,delete,5,0.0179617110 +BST,отсортированный,insert,среднее,4.1636513992 +BST,отсортированный,find,среднее,0.0379873058 +BST,отсортированный,delete,среднее,0.0193626138 diff --git a/petryainiyas/task1/docs/otchet1.docx b/petryainiyas/task1/docs/otchet1.docx new file mode 100644 index 0000000..d9863d0 Binary files /dev/null and b/petryainiyas/task1/docs/otchet1.docx differ diff --git a/petryainiyas/task1/experiments.py b/petryainiyas/task1/experiments.py new file mode 100644 index 0000000..f5face1 --- /dev/null +++ b/petryainiyas/task1/experiments.py @@ -0,0 +1,172 @@ +from __future__ import annotations + +import csv +import random +import time +from pathlib import Path +from typing import Dict, List, Tuple + +from linked_list import ll_insert, ll_find, ll_delete +from hash_table import ht_insert, ht_find, ht_delete +from bst import bst_insert, bst_find, bst_delete +from utils import generate_records, prepare_records_variants + + +Record = Tuple[str, str] + + +def make_missing_names(count: int = 10) -> List[str]: + return [f"None_{i}" for i in range(count)] + + +def pick_existing_names(records: List[Record], count: int, seed: int = 42) -> List[str]: + rng = random.Random(seed) + unique_names = list(dict.fromkeys(name for name, _ in records)) + if len(unique_names) < count: + raise ValueError(f"Not enough unique names: need {count}, got {len(unique_names)}") + return rng.sample(unique_names, count) + + +def pick_delete_names(records: List[Record], count: int = 50, seed: int = 43) -> List[str]: + rng = random.Random(seed) + unique_names = list(dict.fromkeys(name for name, _ in records)) + if len(unique_names) < count: + raise ValueError(f"Not enough unique names: need {count}, got {len(unique_names)}") + return rng.sample(unique_names, count) + + +def build_structure(structure_name: str, records: List[Record], buckets_count: int = 2048): + if structure_name == "linked_list": + structure = None + for name, phone in records: + structure = ll_insert(structure, name, phone) + return structure + + if structure_name == "hash_table": + buckets = [None] * buckets_count + for name, phone in records: + buckets = ht_insert(buckets, name, phone) + return buckets + + if structure_name == "bst": + root = None + for name, phone in records: + root = bst_insert(root, name, phone) + return root + + raise ValueError(f"Unknown structure: {structure_name}") + + +def do_find(structure_name: str, structure: object, existing_names: List[str], missing_names: List[str]) -> None: + if structure_name == "linked_list": + for name in existing_names: + ll_find(structure, name) + for name in missing_names: + ll_find(structure, name) + return + + if structure_name == "hash_table": + for name in existing_names: + ht_find(structure, name) + for name in missing_names: + ht_find(structure, name) + return + + if structure_name == "bst": + for name in existing_names: + bst_find(structure, name) + for name in missing_names: + bst_find(structure, name) + return + + raise ValueError(f"Unknown structure: {structure_name}") + + +def do_delete(structure_name: str, structure: object, delete_names: List[str]): + if structure_name == "linked_list": + for name in delete_names: + structure = ll_delete(structure, name) + return structure + + if structure_name == "hash_table": + for name in delete_names: + structure = ht_delete(structure, name) + return structure + + if structure_name == "bst": + for name in delete_names: + structure = bst_delete(structure, name) + return structure + + raise ValueError(f"Unknown structure: {structure_name}") + + +def measure_once(structure_name: str, records: List[Record], buckets_count: int = 2048) -> Dict[str, float]: + existing_names = pick_existing_names(records, 100, seed=42) + missing_names = make_missing_names(10) + delete_names = pick_delete_names(records, 50, seed=43) + + start = time.perf_counter() + structure = build_structure(structure_name, records, buckets_count=buckets_count) + insert_time = time.perf_counter() - start + + start = time.perf_counter() + do_find(structure_name, structure, existing_names, missing_names) + find_time = time.perf_counter() - start + + start = time.perf_counter() + structure = do_delete(structure_name, structure, delete_names) + delete_time = time.perf_counter() - start + + return {"insert": insert_time, "find": find_time, "delete": delete_time} + + +def run_experiments(n: int = 10000, buckets_count: int = 2048, repeats: int = 5): + records = generate_records(n, repeat_names=False) + records_shuffled, records_sorted = prepare_records_variants(records) + + datasets = [ + ("случайный", records_shuffled), + ("отсортированный", records_sorted), + ] + structures = [ + ("LinkedList", "linked_list"), + ("HashTable", "hash_table"), + ("BST", "bst"), + ] + operations = ("insert", "find", "delete") + + rows = [["Структура", "Режим", "Операция", "Замер", "Время (сек)"]] + + for mode_name, dataset_records in datasets: + for human_name, structure_name in structures: + times_by_op = {op: [] for op in operations} + + for attempt in range(1, repeats + 1): + result = measure_once(structure_name, dataset_records, buckets_count=buckets_count) + for op_name in operations: + elapsed = result[op_name] + times_by_op[op_name].append(elapsed) + rows.append([human_name, mode_name, op_name, attempt, f"{elapsed:.10f}"]) + + for op_name in operations: + avg_time = sum(times_by_op[op_name]) / len(times_by_op[op_name]) + rows.append([human_name, mode_name, op_name, "среднее", f"{avg_time:.10f}"]) + + return rows + + +def save_results_csv(rows, filename: str = "results.csv"): + with open(filename, "w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerows(rows) + + +def main(): + rows = run_experiments(n=10000, buckets_count=2048, repeats=5) + save_results_csv(rows, "results.csv") + print("Saved results.csv") + + +if __name__ == "__main__": + main() diff --git a/petryainiyas/task1/hash_table.py b/petryainiyas/task1/hash_table.py new file mode 100644 index 0000000..9aa720d --- /dev/null +++ b/petryainiyas/task1/hash_table.py @@ -0,0 +1,44 @@ + + +from typing import Any, Dict, List, Optional + +from linked_list import ll_insert, ll_find, ll_delete, ll_list_all + + +Bucket = Optional[Dict[str, Any]] + + +def _hash_name(name: str, buckets_count: int) -> int: + if buckets_count <= 0: + return 0 + return sum(ord(ch) for ch in name) % buckets_count + + +def ht_insert(buckets: List[Bucket], name: str, phone: str) -> List[Bucket]: + if not buckets: + return buckets + index = _hash_name(name, len(buckets)) + buckets[index] = ll_insert(buckets[index], name, phone) + return buckets + + +def ht_find(buckets: List[Bucket], name: str) -> Optional[str]: + if not buckets: + return None + index = _hash_name(name, len(buckets)) + return ll_find(buckets[index], name) + + +def ht_delete(buckets: List[Bucket], name: str) -> List[Bucket]: + if not buckets: + return buckets + index = _hash_name(name, len(buckets)) + buckets[index] = ll_delete(buckets[index], name) + return buckets + + +def ht_list_all(buckets: List[Bucket]) -> List[Dict[str, str]]: + records: List[Dict[str, str]] = [] + for head in buckets: + records.extend(ll_list_all(head)) + return sorted(records, key=lambda x: x["name"]) diff --git a/petryainiyas/task1/linked_list.py b/petryainiyas/task1/linked_list.py new file mode 100644 index 0000000..0260036 --- /dev/null +++ b/petryainiyas/task1/linked_list.py @@ -0,0 +1,73 @@ + + +from typing import Any, Dict, List, Optional + + +Node = Dict[str, Any] + + +def _make_node(name: str, phone: str) -> Node: + return {"name": name, "phone": phone, "next": None} + + +def sort_records(records: List[Dict[str, str]]) -> List[Dict[str, str]]: + + return sorted(records, key=lambda x: x["name"]) + + +def ll_insert(head: Optional[Node], name: str, phone: str) -> Node: + + new_node = _make_node(name, phone) + + if head is None: + return new_node + + current = head + while current is not None: + if current["name"] == name: + current["phone"] = phone + return head + if current["next"] is None: + current["next"] = new_node + return head + current = current["next"] + + return head + + +def ll_find(head: Optional[Node], name: str) -> Optional[str]: + current = head + while current is not None: + if current["name"] == name: + return current["phone"] + current = current["next"] + return None + + +def ll_delete(head: Optional[Node], name: str) -> Optional[Node]: + if head is None: + return None + + if head["name"] == name: + return head["next"] + + prev = head + current = head["next"] + + while current is not None: + if current["name"] == name: + prev["next"] = current["next"] + return head + prev = current + current = current["next"] + + return head + + +def ll_list_all(head: Optional[Node]) -> List[Dict[str, str]]: + records: List[Dict[str, str]] = [] + current = head + while current is not None: + records.append({"name": current["name"], "phone": current["phone"]}) + current = current["next"] + return sort_records(records) diff --git a/petryainiyas/task1/main.py b/petryainiyas/task1/main.py new file mode 100644 index 0000000..70de618 --- /dev/null +++ b/petryainiyas/task1/main.py @@ -0,0 +1,21 @@ + + +from __future__ import annotations + +import csv +from pathlib import Path + +from experiments import run_experiments, save_results_csv +from plot_results import build_graphs, load_average_results + + +def main(): + rows = run_experiments(n=10000, buckets_count=2048, repeats=5) + save_results_csv(rows, "results.csv") + averaged = load_average_results("results.csv") + build_graphs(averaged, output_dir="docs/data") + print("Done.") + + +if __name__ == "__main__": + main() diff --git a/petryainiyas/task1/plot_results.py b/petryainiyas/task1/plot_results.py new file mode 100644 index 0000000..f6b9d3a --- /dev/null +++ b/petryainiyas/task1/plot_results.py @@ -0,0 +1,66 @@ +from __future__ import annotations +import csv +from collections import defaultdict +from pathlib import Path +import matplotlib.pyplot as plt +from typing import List, Dict, Any + +# Карта соответствия заголовков CSV для удобства поддержки +_CSV_KEYS = { + "STRUCTURE": "Структура", + "MODE": "Режим", + "OP": "Операция", + "MEASURE": "Замер", + "TIME": "Время (сек)" +} + +def load_average_results(csv_file: str) -> List[Dict[str, Any]]: + """Загружает только строки со средними значениями из CSV.""" + with open(csv_file, "r", encoding="utf-8") as fh: + return [ + { + "structure": row[_CSV_KEYS["STRUCTURE"]], + "mode": row[_CSV_KEYS["MODE"]], + "operation": row[_CSV_KEYS["OP"]], + "time": float(row[_CSV_KEYS["TIME"]]) + } + for row in csv.DictReader(fh) + if row[_CSV_KEYS["MEASURE"]] == "среднее" + ] + +def _render_chart(operation: str, data: List[Dict[str, Any]], save_path: Path) -> None: + """Вспомогательная функция для отрисовки одного столбчатого графика.""" + fig, ax = plt.subplots(figsize=(10, 5)) + labels = [f"{rec['structure']}\n{rec['mode']}" for rec in data] + values = [rec["time"] for rec in data] + + ax.bar(labels, values, color="cornflowerblue", edgecolor="navy") + ax.set_title(f"{operation.capitalize()} Performance Comparison") + ax.set_xlabel("Data Structure & Input Order") + ax.set_ylabel("Execution Time (seconds)") + ax.tick_params(axis="x", rotation=15) + fig.tight_layout() + fig.savefig(save_path, dpi=150) + plt.close(fig) + print(f"Saved chart: {save_path}") + +def build_graphs(results: List[Dict[str, Any]], output_dir: str = "docs/data") -> None: + """Группирует результаты по операциям и сохраняет графики.""" + out_path = Path(output_dir) + out_path.mkdir(parents=True, exist_ok=True) + + grouped = defaultdict(list) + for record in results: + grouped[record["operation"]].append(record) + + for op_name in ("insert", "find", "delete"): + if op_name in grouped: + target_file = out_path / f"{op_name}.png" + _render_chart(op_name, grouped[op_name], target_file) + +def main(): + avg_data = load_average_results("results.csv") + build_graphs(avg_data) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/petryainiyas/task1/requirements.txt b/petryainiyas/task1/requirements.txt new file mode 100644 index 0000000..a9006fd --- /dev/null +++ b/petryainiyas/task1/requirements.txt @@ -0,0 +1 @@ +matplotlib>=3.8 diff --git a/petryainiyas/task1/results.csv b/petryainiyas/task1/results.csv new file mode 100644 index 0000000..4624802 --- /dev/null +++ b/petryainiyas/task1/results.csv @@ -0,0 +1,109 @@ +Структура,Режим,Операция,Замер,Время (сек) +LinkedList,случайный,insert,1,6.8132659000 +LinkedList,случайный,find,1,0.0553455000 +LinkedList,случайный,delete,1,0.0304272000 +LinkedList,случайный,insert,2,6.8263299000 +LinkedList,случайный,find,2,0.0562645000 +LinkedList,случайный,delete,2,0.0303858000 +LinkedList,случайный,insert,3,6.8117682000 +LinkedList,случайный,find,3,0.0567219000 +LinkedList,случайный,delete,3,0.0302671000 +LinkedList,случайный,insert,4,7.0393228000 +LinkedList,случайный,find,4,0.0544359000 +LinkedList,случайный,delete,4,0.0313359000 +LinkedList,случайный,insert,5,7.0633509000 +LinkedList,случайный,find,5,0.0552176000 +LinkedList,случайный,delete,5,0.0318664000 +LinkedList,случайный,insert,среднее,6.9108075400 +LinkedList,случайный,find,среднее,0.0555970800 +LinkedList,случайный,delete,среднее,0.0308564800 +HashTable,случайный,insert,1,0.3898307000 +HashTable,случайный,find,1,0.0022226000 +HashTable,случайный,delete,1,0.0012723000 +HashTable,случайный,insert,2,0.3844561000 +HashTable,случайный,find,2,0.0024825000 +HashTable,случайный,delete,2,0.0013425000 +HashTable,случайный,insert,3,0.3740853000 +HashTable,случайный,find,3,0.0026141000 +HashTable,случайный,delete,3,0.0013710000 +HashTable,случайный,insert,4,0.3870099000 +HashTable,случайный,find,4,0.0022063000 +HashTable,случайный,delete,4,0.0015940000 +HashTable,случайный,insert,5,0.3869087000 +HashTable,случайный,find,5,0.0022110000 +HashTable,случайный,delete,5,0.0012524000 +HashTable,случайный,insert,среднее,0.3844581400 +HashTable,случайный,find,среднее,0.0023473000 +HashTable,случайный,delete,среднее,0.0013664400 +BST,случайный,insert,1,0.0375842000 +BST,случайный,find,1,0.0003637000 +BST,случайный,delete,1,0.0001893000 +BST,случайный,insert,2,0.0364385000 +BST,случайный,find,2,0.0003284000 +BST,случайный,delete,2,0.0002408000 +BST,случайный,insert,3,0.0359604000 +BST,случайный,find,3,0.0003705000 +BST,случайный,delete,3,0.0001883000 +BST,случайный,insert,4,0.0371920000 +BST,случайный,find,4,0.0003347000 +BST,случайный,delete,4,0.0001711000 +BST,случайный,insert,5,0.0385580000 +BST,случайный,find,5,0.0003628000 +BST,случайный,delete,5,0.0001828000 +BST,случайный,insert,среднее,0.0371466200 +BST,случайный,find,среднее,0.0003520200 +BST,случайный,delete,среднее,0.0001944600 +LinkedList,отсортированный,insert,1,6.7166487000 +LinkedList,отсортированный,find,1,0.0614362000 +LinkedList,отсортированный,delete,1,0.0324487000 +LinkedList,отсортированный,insert,2,6.8582294000 +LinkedList,отсортированный,find,2,0.0588546000 +LinkedList,отсортированный,delete,2,0.0332572000 +LinkedList,отсортированный,insert,3,6.6991410000 +LinkedList,отсортированный,find,3,0.0521304000 +LinkedList,отсортированный,delete,3,0.0296734000 +LinkedList,отсортированный,insert,4,6.7166336000 +LinkedList,отсортированный,find,4,0.0521848000 +LinkedList,отсортированный,delete,4,0.0286064000 +LinkedList,отсортированный,insert,5,6.6510829000 +LinkedList,отсортированный,find,5,0.0547075000 +LinkedList,отсортированный,delete,5,0.0285625000 +LinkedList,отсортированный,insert,среднее,6.7283471200 +LinkedList,отсортированный,find,среднее,0.0558627000 +LinkedList,отсортированный,delete,среднее,0.0305096400 +HashTable,отсортированный,insert,1,0.3645209000 +HashTable,отсортированный,find,1,0.0032770000 +HashTable,отсортированный,delete,1,0.0018631000 +HashTable,отсортированный,insert,2,0.3579217000 +HashTable,отсортированный,find,2,0.0032143000 +HashTable,отсортированный,delete,2,0.0016427000 +HashTable,отсортированный,insert,3,0.3684879000 +HashTable,отсортированный,find,3,0.0026412000 +HashTable,отсортированный,delete,3,0.0014870000 +HashTable,отсортированный,insert,4,0.3873619000 +HashTable,отсортированный,find,4,0.0028024000 +HashTable,отсортированный,delete,4,0.0015786000 +HashTable,отсортированный,insert,5,0.3642992000 +HashTable,отсортированный,find,5,0.0031264000 +HashTable,отсортированный,delete,5,0.0015886000 +HashTable,отсортированный,insert,среднее,0.3685183200 +HashTable,отсортированный,find,среднее,0.0030122600 +HashTable,отсортированный,delete,среднее,0.0016320000 +BST,отсортированный,insert,1,10.5552378000 +BST,отсортированный,find,1,0.1016856000 +BST,отсортированный,delete,1,0.0422728000 +BST,отсортированный,insert,2,10.3035871000 +BST,отсортированный,find,2,0.1008642000 +BST,отсортированный,delete,2,0.0450330000 +BST,отсортированный,insert,3,10.6304005000 +BST,отсортированный,find,3,0.1073470000 +BST,отсортированный,delete,3,0.0816121000 +BST,отсортированный,insert,4,10.3183078000 +BST,отсортированный,find,4,0.1005074000 +BST,отсортированный,delete,4,0.0422195000 +BST,отсортированный,insert,5,10.3131368000 +BST,отсортированный,find,5,0.1001096000 +BST,отсортированный,delete,5,0.0416660000 +BST,отсортированный,insert,среднее,10.4241340000 +BST,отсортированный,find,среднее,0.1021027600 +BST,отсортированный,delete,среднее,0.0505606800 diff --git a/petryainiyas/task1/utils.py b/petryainiyas/task1/utils.py new file mode 100644 index 0000000..2befe4b --- /dev/null +++ b/petryainiyas/task1/utils.py @@ -0,0 +1,29 @@ +import random +from typing import List, Tuple + +Record = Tuple[str, str] + +NAME_POOL = ( + "User_Alex", "User_Bob", "User_Cat", "User_Dan", "User_Eva", + "User_Fox", "User_Geo", "User_Hen", "User_Ira", "User_Leo" +) + +def generate_records(n: int, repeat_names: bool = False, seed: int = 42) -> List[Record]: + """Генерирует n кортежей (имя, телефон).""" + rng = random.Random(seed) + if repeat_names: + return [ + (rng.choice(NAME_POOL), str(rng.randint(10**9, 10**10 - 1))) + for _ in range(n) + ] + return [ + (f"User_{i:05d}", str(10**9 + i)) + for i in range(n) + ] + +def prepare_records_variants(records: List[Record], seed: int = 42) -> Tuple[List[Record], List[Record]]: + """Возвращает пару: (перемешанный список, отсортированный по имени список).""" + shuffled = records.copy() + random.Random(seed).shuffle(shuffled) + sorted_records = sorted(records, key=lambda rec: rec[0]) + return shuffled, sorted_records \ No newline at end of file diff --git a/petryainiyas/task2/builders/__init__.py b/petryainiyas/task2/builders/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/petryainiyas/task2/builders/maze_builder.py b/petryainiyas/task2/builders/maze_builder.py new file mode 100644 index 0000000..b055db8 --- /dev/null +++ b/petryainiyas/task2/builders/maze_builder.py @@ -0,0 +1,7 @@ +from abc import ABC, abstractmethod + + +class MazeBuilder(ABC): + @abstractmethod + def buildFromFile(self, filename): + raise NotImplementedError diff --git a/petryainiyas/task2/builders/text_file_maze_builder.py b/petryainiyas/task2/builders/text_file_maze_builder.py new file mode 100644 index 0000000..5e9ca03 --- /dev/null +++ b/petryainiyas/task2/builders/text_file_maze_builder.py @@ -0,0 +1,52 @@ +from core.cell import Cell +from core.maze import Maze +from builders.maze_builder import MazeBuilder + + +class TextFileMazeBuilder(MazeBuilder): + def buildFromFile(self, filename): + with open(filename, "r", encoding="utf-8") as f: + lines = [line.rstrip("\n") for line in f] + + if not lines: + raise ValueError("Maze file is empty") + + width = max(len(line) for line in lines) + height = len(lines) + + cells = [] + startCell = None + exitCell = None + + for y, line in enumerate(lines): + row = [] + for x in range(width): + ch = line[x] if x < len(line) else "#" + + if ch == "#": + cell = Cell(x, y, isWall=True) + elif ch == "S": + if startCell is not None: + raise ValueError("Multiple start cells found") + cell = Cell(x, y, isWall=False, isStart=True) + startCell = cell + elif ch == "E": + if exitCell is not None: + raise ValueError("Multiple exit cells found") + cell = Cell(x, y, isWall=False, isExit=True) + exitCell = cell + elif ch in (" ", "."): + cell = Cell(x, y, isWall=False) + elif ch.isdigit(): + cell = Cell(x, y, isWall=False, weight=max(1, int(ch))) + else: + raise ValueError(f"Unsupported symbol '{ch}' at ({x}, {y})") + row.append(cell) + cells.append(row) + + if startCell is None: + raise ValueError("Start cell 'S' not found") + if exitCell is None: + raise ValueError("Exit cell 'E' not found") + + return Maze(cells, width, height, startCell, exitCell) diff --git a/petryainiyas/task2/commands/__init__.py b/petryainiyas/task2/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/petryainiyas/task2/commands/command.py b/petryainiyas/task2/commands/command.py new file mode 100644 index 0000000..71f2dc6 --- /dev/null +++ b/petryainiyas/task2/commands/command.py @@ -0,0 +1,11 @@ +from abc import ABC, abstractmethod + + +class Command(ABC): + @abstractmethod + def execute(self): + raise NotImplementedError + + @abstractmethod + def undo(self): + raise NotImplementedError diff --git a/petryainiyas/task2/commands/move_command.py b/petryainiyas/task2/commands/move_command.py new file mode 100644 index 0000000..e90b7f1 --- /dev/null +++ b/petryainiyas/task2/commands/move_command.py @@ -0,0 +1,37 @@ +from commands.command import Command + + +class MoveCommand(Command): + DIRECTION_TO_DELTA = { + "W": (0, -1), + "A": (-1, 0), + "S": (0, 1), + "D": (1, 0), + } + + def __init__(self, player, maze, direction): + self.player = player + self.maze = maze + self.direction = direction.upper() + self.previousCell = None + + def execute(self): + if self.direction not in self.DIRECTION_TO_DELTA: + return False + + dx, dy = self.DIRECTION_TO_DELTA[self.direction] + current = self.player.currentCell + new_cell = self.maze.getCell(current.x + dx, current.y + dy) + + if new_cell is None or not new_cell.isPassable(): + return False + + self.previousCell = current + self.player.setCell(new_cell) + return True + + def undo(self): + if self.previousCell is None: + return False + self.player.setCell(self.previousCell) + return True diff --git a/petryainiyas/task2/controller/__init__.py b/petryainiyas/task2/controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/petryainiyas/task2/controller/game_controller.py b/petryainiyas/task2/controller/game_controller.py new file mode 100644 index 0000000..0a4cb39 --- /dev/null +++ b/petryainiyas/task2/controller/game_controller.py @@ -0,0 +1,30 @@ +from commands.move_command import MoveCommand + + +class GameController: + def __init__(self, maze, player, view): + self.maze = maze + self.player = player + self.view = view + self.history = [] + + def move(self, direction): + command = MoveCommand(self.player, self.maze, direction) + if command.execute(): + self.history.append(command) + self.view.update({"type": "move", "direction": direction}) + self.view.render(self.maze, player_position=self.player.currentCell) + return True + print("Cannot move there") + return False + + def undo(self): + if not self.history: + print("Nothing to undo") + return False + command = self.history.pop() + if command.undo(): + self.view.update({"type": "undo"}) + self.view.render(self.maze, player_position=self.player.currentCell) + return True + return False diff --git a/petryainiyas/task2/core/__init__.py b/petryainiyas/task2/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/petryainiyas/task2/core/cell.py b/petryainiyas/task2/core/cell.py new file mode 100644 index 0000000..44e2d76 --- /dev/null +++ b/petryainiyas/task2/core/cell.py @@ -0,0 +1,26 @@ +from dataclasses import dataclass + + +@dataclass +class Cell: + x: int + y: int + isWall: bool = False + isStart: bool = False + isExit: bool = False + weight: int = 1 + + def isPassable(self): + return not self.isWall + + def __repr__(self): + parts = [f"Cell({self.x}, {self.y}"] + if self.isWall: + parts.append("WALL") + if self.isStart: + parts.append("START") + if self.isExit: + parts.append("EXIT") + if self.weight != 1: + parts.append(f"w={self.weight}") + return ", ".join(parts) + ")" diff --git a/petryainiyas/task2/core/maze.py b/petryainiyas/task2/core/maze.py new file mode 100644 index 0000000..59c86dd --- /dev/null +++ b/petryainiyas/task2/core/maze.py @@ -0,0 +1,49 @@ +class Maze: + def __init__(self, cells, width, height, startCell=None, exitCell=None): + self.cells = cells + self.width = width + self.height = height + self.startCell = startCell + self.exitCell = exitCell + + def getCell(self, x, y): + if 0 <= x < self.width and 0 <= y < self.height: + return self.cells[y][x] + return None + + def getNeighbors(self, cell): + neighbors = [] + for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)): + nx, ny = cell.x + dx, cell.y + dy + neighbor = self.getCell(nx, ny) + if neighbor is not None and neighbor.isPassable(): + neighbors.append(neighbor) + return neighbors + + def render_lines(self, player_position=None, path=None): + path_set = {(c.x, c.y) for c in path} if path else set() + player_pos = None if player_position is None else (player_position.x, player_position.y) + lines = [] + for y in range(self.height): + row = [] + for x in range(self.width): + cell = self.cells[y][x] + if player_pos == (x, y): + row.append("P") + elif cell.isStart: + row.append("S") + elif cell.isExit: + row.append("E") + elif cell.isWall: + row.append("#") + elif (x, y) in path_set: + row.append("*") + elif cell.weight > 1: + row.append(str(cell.weight)) + else: + row.append(" ") + lines.append("".join(row)) + return lines + + def render(self, player_position=None, path=None): + return "\n".join(self.render_lines(player_position=player_position, path=path)) diff --git a/petryainiyas/task2/core/player.py b/petryainiyas/task2/core/player.py new file mode 100644 index 0000000..b68a0ff --- /dev/null +++ b/petryainiyas/task2/core/player.py @@ -0,0 +1,6 @@ +class Player: + def __init__(self, currentCell): + self.currentCell = currentCell + + def setCell(self, cell): + self.currentCell = cell diff --git a/petryainiyas/task2/core/search_stats.py b/petryainiyas/task2/core/search_stats.py new file mode 100644 index 0000000..5548118 --- /dev/null +++ b/petryainiyas/task2/core/search_stats.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass, field + + +@dataclass +class SearchStats: + timeMs: float + visitedCells: int + pathLength: int + path: list = field(default_factory=list) + found: bool = False + algorithm: str = "" diff --git a/petryainiyas/task2/docs/otchet 2.docx b/petryainiyas/task2/docs/otchet 2.docx new file mode 100644 index 0000000..8f3d20c Binary files /dev/null and b/petryainiyas/task2/docs/otchet 2.docx differ diff --git a/petryainiyas/task2/experiment.py b/petryainiyas/task2/experiment.py new file mode 100644 index 0000000..588f377 --- /dev/null +++ b/petryainiyas/task2/experiment.py @@ -0,0 +1,225 @@ +from pathlib import Path +from statistics import mean +import csv +import random + +import matplotlib.pyplot as plt + +from core.cell import Cell +from core.maze import Maze +from solver.maze_solver import MazeSolver +from strategies.astar_strategy import AStarStrategy +from strategies.bfs_strategy import BFSStrategy +from strategies.dfs_strategy import DFSStrategy +from strategies.dijkstra_strategy import DijkstraStrategy + + +BASE_DIR = Path(__file__).resolve().parent +OUT_DIR = BASE_DIR / "experiment_results" + + +def build_maze_from_symbols(lines): + height = len(lines) + width = max(len(line) for line in lines) + cells = [] + start = None + exit_cell = None + for y, line in enumerate(lines): + row = [] + for x in range(width): + ch = line[x] if x < len(line) else "#" + if ch == "#": + cell = Cell(x, y, isWall=True) + elif ch == "S": + cell = Cell(x, y, isWall=False, isStart=True) + start = cell + elif ch == "E": + cell = Cell(x, y, isWall=False, isExit=True) + exit_cell = cell + elif ch == " " or ch == ".": + cell = Cell(x, y, isWall=False) + elif ch.isdigit(): + cell = Cell(x, y, isWall=False, weight=int(ch)) + else: + raise ValueError(f"Unknown symbol '{ch}' at {x},{y}") + row.append(cell) + cells.append(row) + return Maze(cells, width, height, start, exit_cell) + + +def generate_empty_maze(width, height): + lines = [" " * width for _ in range(height)] + lines = [list(row) for row in lines] + lines[1][1] = "S" + lines[height - 2][width - 2] = "E" + return build_maze_from_symbols(["".join(row) for row in lines]) + + +def generate_simple_maze(width, height): + grid = [["#" for _ in range(width)] for _ in range(height)] + for x in range(1, width - 1): + grid[1][x] = " " + for y in range(1, height - 1): + grid[y][width - 2] = " " + grid[1][1] = "S" + grid[height - 2][width - 2] = "E" + return build_maze_from_symbols(["".join(row) for row in grid]) + + +def generate_branching_maze(width, height, seed=42, wall_density=0.30): + rng = random.Random(seed) + grid = [["#" for _ in range(width)] for _ in range(height)] + x, y = 1, 1 + grid[y][x] = "S" + while (x, y) != (width - 2, height - 2): + candidates = [] + for dx, dy in [(1, 0), (0, 1)]: + nx, ny = x + dx, y + dy + if 1 <= nx < width - 1 and 1 <= ny < height - 1: + candidates.append((nx, ny)) + if not candidates: + break + x, y = rng.choice(candidates) + grid[y][x] = " " + grid[height - 2][width - 2] = "E" + + # carve extra corridors and dead ends + for yy in range(1, height - 1): + for xx in range(1, width - 1): + if grid[yy][xx] == "#" and rng.random() > wall_density: + grid[yy][xx] = " " + grid[1][1] = "S" + grid[height - 2][width - 2] = "E" + return build_maze_from_symbols(["".join(row) for row in grid]) + + +def generate_no_path_maze(width, height): + grid = [[" " for _ in range(width)] for _ in range(height)] + for x in range(width): + grid[height // 2][x] = "#" + grid[1][1] = "S" + grid[height - 2][width - 2] = "E" + return build_maze_from_symbols(["".join(row) for row in grid]) + + +def generate_weighted_maze(width, height, seed=123): + rng = random.Random(seed) + grid = [[" " for _ in range(width)] for _ in range(height)] + for y in range(height): + for x in range(width): + r = rng.random() + if r < 0.12: + grid[y][x] = "#" + elif r < 0.25: + grid[y][x] = "3" + elif r < 0.40: + grid[y][x] = "2" + else: + grid[y][x] = "1" + # ensure path-ish + for x in range(width): + grid[1][x] = "1" + for y in range(1, height): + grid[y][width - 2] = "1" + grid[1][1] = "S" + grid[height - 2][width - 2] = "E" + return build_maze_from_symbols(["".join(row) for row in grid]) + + +def bench_one_maze(maze_name, maze, strategies, repeats=5): + summary_rows = [] + raw_rows = [] + for strategy_name, strategy_factory in strategies: + times, visiteds, lengths = [], [], [] + for run in range(1, repeats + 1): + solver = MazeSolver(maze) + solver.setStrategy(strategy_factory()) + stats = solver.solve() + raw_rows.append([maze_name, strategy_name, run, f"{stats.timeMs:.6f}", stats.visitedCells, stats.pathLength]) + times.append(stats.timeMs) + visiteds.append(stats.visitedCells) + lengths.append(stats.pathLength) + summary_rows.append([maze_name, strategy_name, f"{mean(times):.6f}", f"{mean(visiteds):.2f}", f"{mean(lengths):.2f}", repeats]) + return summary_rows, raw_rows + + +def save_csv(path, rows): + with open(path, "w", newline="", encoding="utf-8") as f: + csv.writer(f).writerows(rows) + + +def plot_summary(summary_rows): + by_maze = {} + for row in summary_rows[1:]: + maze_name, strategy, avg_time, avg_visited, avg_len, runs = row + by_maze.setdefault(maze_name, []).append((strategy, float(avg_time), float(avg_visited), float(avg_len))) + + for maze_name, items in by_maze.items(): + items.sort(key=lambda t: t[0]) + strategies = [i[0] for i in items] + x = list(range(len(strategies))) + + plt.figure(figsize=(8, 4)) + plt.bar(x, [i[1] for i in items]) + plt.xticks(x, strategies) + plt.ylabel("ms") + plt.title(f"{maze_name} — avg time") + plt.tight_layout() + plt.savefig(OUT_DIR / f"{maze_name}_time.png", dpi=150) + plt.close() + + plt.figure(figsize=(8, 4)) + plt.bar(x, [i[2] for i in items]) + plt.xticks(x, strategies) + plt.ylabel("cells") + plt.title(f"{maze_name} — visited cells") + plt.tight_layout() + plt.savefig(OUT_DIR / f"{maze_name}_visited.png", dpi=150) + plt.close() + + plt.figure(figsize=(8, 4)) + plt.bar(x, [i[3] for i in items]) + plt.xticks(x, strategies) + plt.ylabel("cells") + plt.title(f"{maze_name} — path length") + plt.tight_layout() + plt.savefig(OUT_DIR / f"{maze_name}_length.png", dpi=150) + plt.close() + + +def main(): + OUT_DIR.mkdir(exist_ok=True) + + strategies = [ + ("BFS", BFSStrategy), + ("DFS", DFSStrategy), + ("A*", AStarStrategy), + ("Dijkstra", DijkstraStrategy), + ] + + mazes = [ + ("small_10x10", generate_simple_maze(10, 10)), + ("medium_50x50", generate_branching_maze(50, 50)), + ("large_100x100", generate_branching_maze(100, 100, seed=99, wall_density=0.35)), + ("empty_30x30", generate_empty_maze(30, 30)), + ("no_path_30x30", generate_no_path_maze(30, 30)), + ("weighted_30x30", generate_weighted_maze(30, 30)), + ] + + summary = [["maze", "strategy", "avg_time_ms", "avg_visited_cells", "avg_path_length", "runs"]] + raw = [["maze", "strategy", "run", "time_ms", "visited_cells", "path_length"]] + + for maze_name, maze in mazes: + s_rows, r_rows = bench_one_maze(maze_name, maze, strategies, repeats=5) + summary.extend(s_rows) + raw.extend(r_rows) + + save_csv(OUT_DIR / "summary.csv", summary) + save_csv(OUT_DIR / "raw.csv", raw) + plot_summary(summary) + + print("Saved to", OUT_DIR.resolve()) + + +if __name__ == "__main__": + main() diff --git a/petryainiyas/task2/experiment_results/empty_30x30_length.png b/petryainiyas/task2/experiment_results/empty_30x30_length.png new file mode 100644 index 0000000..53d8f2c Binary files /dev/null and b/petryainiyas/task2/experiment_results/empty_30x30_length.png differ diff --git a/petryainiyas/task2/experiment_results/empty_30x30_time.png b/petryainiyas/task2/experiment_results/empty_30x30_time.png new file mode 100644 index 0000000..c1a3ed2 Binary files /dev/null and b/petryainiyas/task2/experiment_results/empty_30x30_time.png differ diff --git a/petryainiyas/task2/experiment_results/empty_30x30_visited.png b/petryainiyas/task2/experiment_results/empty_30x30_visited.png new file mode 100644 index 0000000..891d00b Binary files /dev/null and b/petryainiyas/task2/experiment_results/empty_30x30_visited.png differ diff --git a/petryainiyas/task2/experiment_results/large_100x100_length.png b/petryainiyas/task2/experiment_results/large_100x100_length.png new file mode 100644 index 0000000..e799e4a Binary files /dev/null and b/petryainiyas/task2/experiment_results/large_100x100_length.png differ diff --git a/petryainiyas/task2/experiment_results/large_100x100_time.png b/petryainiyas/task2/experiment_results/large_100x100_time.png new file mode 100644 index 0000000..812271a Binary files /dev/null and b/petryainiyas/task2/experiment_results/large_100x100_time.png differ diff --git a/petryainiyas/task2/experiment_results/large_100x100_visited.png b/petryainiyas/task2/experiment_results/large_100x100_visited.png new file mode 100644 index 0000000..0e0ca86 Binary files /dev/null and b/petryainiyas/task2/experiment_results/large_100x100_visited.png differ diff --git a/petryainiyas/task2/experiment_results/medium_50x50_length.png b/petryainiyas/task2/experiment_results/medium_50x50_length.png new file mode 100644 index 0000000..1d8cb11 Binary files /dev/null and b/petryainiyas/task2/experiment_results/medium_50x50_length.png differ diff --git a/petryainiyas/task2/experiment_results/medium_50x50_time.png b/petryainiyas/task2/experiment_results/medium_50x50_time.png new file mode 100644 index 0000000..508c196 Binary files /dev/null and b/petryainiyas/task2/experiment_results/medium_50x50_time.png differ diff --git a/petryainiyas/task2/experiment_results/medium_50x50_visited.png b/petryainiyas/task2/experiment_results/medium_50x50_visited.png new file mode 100644 index 0000000..7c91f22 Binary files /dev/null and b/petryainiyas/task2/experiment_results/medium_50x50_visited.png differ diff --git a/petryainiyas/task2/experiment_results/no_path_30x30_length.png b/petryainiyas/task2/experiment_results/no_path_30x30_length.png new file mode 100644 index 0000000..3f92f3e Binary files /dev/null and b/petryainiyas/task2/experiment_results/no_path_30x30_length.png differ diff --git a/petryainiyas/task2/experiment_results/no_path_30x30_time.png b/petryainiyas/task2/experiment_results/no_path_30x30_time.png new file mode 100644 index 0000000..d851257 Binary files /dev/null and b/petryainiyas/task2/experiment_results/no_path_30x30_time.png differ diff --git a/petryainiyas/task2/experiment_results/no_path_30x30_visited.png b/petryainiyas/task2/experiment_results/no_path_30x30_visited.png new file mode 100644 index 0000000..72e6e55 Binary files /dev/null and b/petryainiyas/task2/experiment_results/no_path_30x30_visited.png differ diff --git a/petryainiyas/task2/experiment_results/raw.csv b/petryainiyas/task2/experiment_results/raw.csv new file mode 100644 index 0000000..ad0b80f --- /dev/null +++ b/petryainiyas/task2/experiment_results/raw.csv @@ -0,0 +1,121 @@ +maze,strategy,run,time_ms,visited_cells,path_length +small_10x10,BFS,1,0.086300,15,15 +small_10x10,BFS,2,0.061100,15,15 +small_10x10,BFS,3,0.059300,15,15 +small_10x10,BFS,4,0.058400,15,15 +small_10x10,BFS,5,0.058500,15,15 +small_10x10,DFS,1,0.073400,15,15 +small_10x10,DFS,2,0.063500,15,15 +small_10x10,DFS,3,0.062500,15,15 +small_10x10,DFS,4,0.062700,15,15 +small_10x10,DFS,5,0.070900,15,15 +small_10x10,A*,1,0.110100,15,15 +small_10x10,A*,2,0.089200,15,15 +small_10x10,A*,3,0.087800,15,15 +small_10x10,A*,4,0.087600,15,15 +small_10x10,A*,5,0.087000,15,15 +small_10x10,Dijkstra,1,0.290000,15,15 +small_10x10,Dijkstra,2,0.083300,15,15 +small_10x10,Dijkstra,3,0.091500,15,15 +small_10x10,Dijkstra,4,0.081000,15,15 +small_10x10,Dijkstra,5,0.080400,15,15 +medium_50x50,BFS,1,6.799200,1579,95 +medium_50x50,BFS,2,6.960100,1579,95 +medium_50x50,BFS,3,6.337000,1579,95 +medium_50x50,BFS,4,7.431700,1579,95 +medium_50x50,BFS,5,6.517900,1579,95 +medium_50x50,DFS,1,6.463000,1277,647 +medium_50x50,DFS,2,6.815500,1277,647 +medium_50x50,DFS,3,5.816100,1277,647 +medium_50x50,DFS,4,6.492400,1277,647 +medium_50x50,DFS,5,6.532500,1277,647 +medium_50x50,A*,1,6.940500,927,95 +medium_50x50,A*,2,7.275400,927,95 +medium_50x50,A*,3,7.062500,927,95 +medium_50x50,A*,4,7.727600,927,95 +medium_50x50,A*,5,7.321000,927,95 +medium_50x50,Dijkstra,1,11.483200,1579,95 +medium_50x50,Dijkstra,2,11.194200,1579,95 +medium_50x50,Dijkstra,3,11.255200,1579,95 +medium_50x50,Dijkstra,4,10.512500,1579,95 +medium_50x50,Dijkstra,5,10.696400,1579,95 +large_100x100,BFS,1,25.623500,5566,195 +large_100x100,BFS,2,24.348800,5566,195 +large_100x100,BFS,3,25.452600,5566,195 +large_100x100,BFS,4,30.516900,5566,195 +large_100x100,BFS,5,33.694700,5566,195 +large_100x100,DFS,1,19.415200,3543,1531 +large_100x100,DFS,2,19.919000,3543,1531 +large_100x100,DFS,3,19.104600,3543,1531 +large_100x100,DFS,4,20.000600,3543,1531 +large_100x100,DFS,5,17.840200,3543,1531 +large_100x100,A*,1,7.509300,853,195 +large_100x100,A*,2,7.221200,853,195 +large_100x100,A*,3,6.486700,853,195 +large_100x100,A*,4,6.357600,853,195 +large_100x100,A*,5,6.723800,853,195 +large_100x100,Dijkstra,1,40.782300,5571,195 +large_100x100,Dijkstra,2,41.155000,5571,195 +large_100x100,Dijkstra,3,39.456200,5571,195 +large_100x100,Dijkstra,4,41.388700,5571,195 +large_100x100,Dijkstra,5,40.962500,5571,195 +empty_30x30,BFS,1,4.143200,896,55 +empty_30x30,BFS,2,3.987000,896,55 +empty_30x30,BFS,3,3.777100,896,55 +empty_30x30,BFS,4,3.682300,896,55 +empty_30x30,BFS,5,3.737900,896,55 +empty_30x30,DFS,1,4.024200,842,815 +empty_30x30,DFS,2,4.333900,842,815 +empty_30x30,DFS,3,5.411000,842,815 +empty_30x30,DFS,4,4.677200,842,815 +empty_30x30,DFS,5,5.177400,842,815 +empty_30x30,A*,1,6.603700,784,55 +empty_30x30,A*,2,6.200600,784,55 +empty_30x30,A*,3,6.798400,784,55 +empty_30x30,A*,4,7.178500,784,55 +empty_30x30,A*,5,6.660800,784,55 +empty_30x30,Dijkstra,1,6.396000,896,55 +empty_30x30,Dijkstra,2,6.275200,896,55 +empty_30x30,Dijkstra,3,6.845700,896,55 +empty_30x30,Dijkstra,4,6.531200,896,55 +empty_30x30,Dijkstra,5,6.783400,896,55 +no_path_30x30,BFS,1,2.000100,450,0 +no_path_30x30,BFS,2,1.797900,450,0 +no_path_30x30,BFS,3,1.796200,450,0 +no_path_30x30,BFS,4,1.774100,450,0 +no_path_30x30,BFS,5,1.775200,450,0 +no_path_30x30,DFS,1,2.090400,450,0 +no_path_30x30,DFS,2,2.222600,450,0 +no_path_30x30,DFS,3,2.454300,450,0 +no_path_30x30,DFS,4,2.476200,450,0 +no_path_30x30,DFS,5,2.073700,450,0 +no_path_30x30,A*,1,3.651700,450,0 +no_path_30x30,A*,2,3.495200,450,0 +no_path_30x30,A*,3,3.754200,450,0 +no_path_30x30,A*,4,3.286800,450,0 +no_path_30x30,A*,5,3.335200,450,0 +no_path_30x30,Dijkstra,1,3.050900,450,0 +no_path_30x30,Dijkstra,2,3.109900,450,0 +no_path_30x30,Dijkstra,3,3.292500,450,0 +no_path_30x30,Dijkstra,4,3.418600,450,0 +no_path_30x30,Dijkstra,5,3.212100,450,0 +weighted_30x30,BFS,1,3.418900,788,55 +weighted_30x30,BFS,2,3.368200,788,55 +weighted_30x30,BFS,3,3.516400,788,55 +weighted_30x30,BFS,4,3.224300,788,55 +weighted_30x30,BFS,5,3.131100,788,55 +weighted_30x30,DFS,1,3.291200,693,479 +weighted_30x30,DFS,2,3.362300,693,479 +weighted_30x30,DFS,3,3.523200,693,479 +weighted_30x30,DFS,4,3.521400,693,479 +weighted_30x30,DFS,5,3.332300,693,479 +weighted_30x30,A*,1,1.181000,126,55 +weighted_30x30,A*,2,1.080200,126,55 +weighted_30x30,A*,3,1.368400,126,55 +weighted_30x30,A*,4,1.109800,126,55 +weighted_30x30,A*,5,1.079300,126,55 +weighted_30x30,Dijkstra,1,6.112700,781,55 +weighted_30x30,Dijkstra,2,5.464800,781,55 +weighted_30x30,Dijkstra,3,5.794500,781,55 +weighted_30x30,Dijkstra,4,6.171700,781,55 +weighted_30x30,Dijkstra,5,6.640500,781,55 diff --git a/petryainiyas/task2/experiment_results/small_10x10_length.png b/petryainiyas/task2/experiment_results/small_10x10_length.png new file mode 100644 index 0000000..1094873 Binary files /dev/null and b/petryainiyas/task2/experiment_results/small_10x10_length.png differ diff --git a/petryainiyas/task2/experiment_results/small_10x10_time.png b/petryainiyas/task2/experiment_results/small_10x10_time.png new file mode 100644 index 0000000..d78930d Binary files /dev/null and b/petryainiyas/task2/experiment_results/small_10x10_time.png differ diff --git a/petryainiyas/task2/experiment_results/small_10x10_visited.png b/petryainiyas/task2/experiment_results/small_10x10_visited.png new file mode 100644 index 0000000..2bbc256 Binary files /dev/null and b/petryainiyas/task2/experiment_results/small_10x10_visited.png differ diff --git a/petryainiyas/task2/experiment_results/summary.csv b/petryainiyas/task2/experiment_results/summary.csv new file mode 100644 index 0000000..eaf8e47 --- /dev/null +++ b/petryainiyas/task2/experiment_results/summary.csv @@ -0,0 +1,25 @@ +maze,strategy,avg_time_ms,avg_visited_cells,avg_path_length,runs +small_10x10,BFS,0.064720,15.00,15.00,5 +small_10x10,DFS,0.066600,15.00,15.00,5 +small_10x10,A*,0.092340,15.00,15.00,5 +small_10x10,Dijkstra,0.125240,15.00,15.00,5 +medium_50x50,BFS,6.809180,1579.00,95.00,5 +medium_50x50,DFS,6.423900,1277.00,647.00,5 +medium_50x50,A*,7.265400,927.00,95.00,5 +medium_50x50,Dijkstra,11.028300,1579.00,95.00,5 +large_100x100,BFS,27.927300,5566.00,195.00,5 +large_100x100,DFS,19.255920,3543.00,1531.00,5 +large_100x100,A*,6.859720,853.00,195.00,5 +large_100x100,Dijkstra,40.748940,5571.00,195.00,5 +empty_30x30,BFS,3.865500,896.00,55.00,5 +empty_30x30,DFS,4.724740,842.00,815.00,5 +empty_30x30,A*,6.688400,784.00,55.00,5 +empty_30x30,Dijkstra,6.566300,896.00,55.00,5 +no_path_30x30,BFS,1.828700,450.00,0.00,5 +no_path_30x30,DFS,2.263440,450.00,0.00,5 +no_path_30x30,A*,3.504620,450.00,0.00,5 +no_path_30x30,Dijkstra,3.216800,450.00,0.00,5 +weighted_30x30,BFS,3.331780,788.00,55.00,5 +weighted_30x30,DFS,3.406080,693.00,479.00,5 +weighted_30x30,A*,1.163740,126.00,55.00,5 +weighted_30x30,Dijkstra,6.036840,781.00,55.00,5 diff --git a/petryainiyas/task2/experiment_results/weighted_30x30_length.png b/petryainiyas/task2/experiment_results/weighted_30x30_length.png new file mode 100644 index 0000000..98d4a30 Binary files /dev/null and b/petryainiyas/task2/experiment_results/weighted_30x30_length.png differ diff --git a/petryainiyas/task2/experiment_results/weighted_30x30_time.png b/petryainiyas/task2/experiment_results/weighted_30x30_time.png new file mode 100644 index 0000000..018e5b3 Binary files /dev/null and b/petryainiyas/task2/experiment_results/weighted_30x30_time.png differ diff --git a/petryainiyas/task2/experiment_results/weighted_30x30_visited.png b/petryainiyas/task2/experiment_results/weighted_30x30_visited.png new file mode 100644 index 0000000..e3b871d Binary files /dev/null and b/petryainiyas/task2/experiment_results/weighted_30x30_visited.png differ diff --git a/petryainiyas/task2/main.py b/petryainiyas/task2/main.py new file mode 100644 index 0000000..08f22c7 --- /dev/null +++ b/petryainiyas/task2/main.py @@ -0,0 +1,59 @@ +from builders.text_file_maze_builder import TextFileMazeBuilder +from core.player import Player +from observer.console_view import ConsoleView +from solver.maze_solver import MazeSolver +from strategies.astar_strategy import AStarStrategy +from strategies.bfs_strategy import BFSStrategy +from strategies.dfs_strategy import DFSStrategy +from controller.game_controller import GameController + + +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent + + +def run_demo(): + builder = TextFileMazeBuilder() + maze = builder.buildFromFile(str(BASE_DIR / "mazes" / "maze_small.txt")) + + view = ConsoleView() + view.update({"type": "maze_loaded", "message": "Maze loaded"}) + view.render(maze) + + solver = MazeSolver(maze) + solver.addObserver(view) + + for strategy in (BFSStrategy(), DFSStrategy(), AStarStrategy()): + solver.setStrategy(strategy) + stats = solver.solve() + + print() + print(f"=== {strategy.name} ===") + print(f"Time: {stats.timeMs:.3f} ms") + print(f"Visited cells: {stats.visitedCells}") + print(f"Path length: {stats.pathLength}") + print(f"Path found: {'yes' if stats.found else 'no'}") + + view.render(maze, path=stats.path) + + player = Player(maze.startCell) + controller = GameController(maze, player, view) + + print("Manual mode: W/A/S/D move, Z undo, Q quit") + view.render(maze, player_position=player.currentCell) + + while True: + cmd = input("Command: ").strip().upper() + if cmd == "Q": + break + if cmd == "Z": + controller.undo() + elif cmd in {"W", "A", "S", "D"}: + controller.move(cmd) + else: + print("Unknown command") + + +if __name__ == "__main__": + run_demo() diff --git a/petryainiyas/task2/mazes/maze_empty.txt b/petryainiyas/task2/mazes/maze_empty.txt new file mode 100644 index 0000000..8267fd0 --- /dev/null +++ b/petryainiyas/task2/mazes/maze_empty.txt @@ -0,0 +1,9 @@ +S + + + + + + + + E diff --git a/petryainiyas/task2/mazes/maze_large.txt b/petryainiyas/task2/mazes/maze_large.txt new file mode 100644 index 0000000..eb03326 --- /dev/null +++ b/petryainiyas/task2/mazes/maze_large.txt @@ -0,0 +1,11 @@ +#################################################################################################### +#S # # # # # # # # # # # # # # # E# +# # ### ### # ###### # ### # ## # #### # ####### # #### # # ### ## # ## # # ## # ## # ##### ### ## +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# ##### # ######## # ### # ## # #### # ####### ## ### # # #### ####### ## ####### ####### # ### ## +# # # # # # # # # # # # # # # # # # # # # +### # # ###### # ########### ########### ### ####### # ####### ### # # ###### # ### ### # ### #### +# # # # # # # # # # # # # # # # # # # # # # +# ### ###### # ##### # ### # ####### # ### ### ## # ###### # ### # ### ###### # ### # ### ### ## # +# # # # # # # # # +#################################################################################################### diff --git a/petryainiyas/task2/mazes/maze_medium.txt b/petryainiyas/task2/mazes/maze_medium.txt new file mode 100644 index 0000000..67ecd65 --- /dev/null +++ b/petryainiyas/task2/mazes/maze_medium.txt @@ -0,0 +1,11 @@ +################################################## +#S # # # # # # E# +# # ### ### # ###### # ### # ## # #### # ####### ## +# # # # # # # # # # # # # # +# ##### # ######## # ### # ## # #### # ####### ## # +# # # # # # # # # # +### # # ###### # ########### ########### ### ###### +# # # # # # # # # # # +# ### ###### # ##### # ### # ####### # ### ### ## # +# # # # # +################################################## diff --git a/petryainiyas/task2/mazes/maze_no_path.txt b/petryainiyas/task2/mazes/maze_no_path.txt new file mode 100644 index 0000000..9633160 --- /dev/null +++ b/petryainiyas/task2/mazes/maze_no_path.txt @@ -0,0 +1,9 @@ +########## +#S # +# ###### # +# # # +########## +# #E# +# ###### # +# # +########## diff --git a/petryainiyas/task2/mazes/maze_small.txt b/petryainiyas/task2/mazes/maze_small.txt new file mode 100644 index 0000000..e829a58 --- /dev/null +++ b/petryainiyas/task2/mazes/maze_small.txt @@ -0,0 +1,7 @@ +########## +#S #E# +# ## # # ## +# # # +# #### # # +# # # +########## diff --git a/petryainiyas/task2/mazes/maze_weighted.txt b/petryainiyas/task2/mazes/maze_weighted.txt new file mode 100644 index 0000000..be8718d --- /dev/null +++ b/petryainiyas/task2/mazes/maze_weighted.txt @@ -0,0 +1,10 @@ +1111111111111111111111111111 +1S11111111111111111111111111 +1111111111111111111111111111 +1111111111111111111111111111 +1111111111111222222222222111 +1111111111111222222222222111 +1111111111111333333333333111 +1111111111111333333333333111 +111111111111111111111111111E +1111111111111111111111111111 diff --git a/petryainiyas/task2/observer/__init__.py b/petryainiyas/task2/observer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/petryainiyas/task2/observer/console_view.py b/petryainiyas/task2/observer/console_view.py new file mode 100644 index 0000000..77248a5 --- /dev/null +++ b/petryainiyas/task2/observer/console_view.py @@ -0,0 +1,26 @@ +import os +from observer.observer import Observer + + +class ConsoleView(Observer): + def update(self, event): + if isinstance(event, str): + print(f"[EVENT] {event}") + elif isinstance(event, dict): + event_type = event.get("type", "unknown") + if event_type == "search_finished": + stats = event.get("stats") + print(f"[EVENT] search finished: {stats}") + else: + print(f"[EVENT] {event_type}: {event}") + else: + print("[EVENT] unknown") + + def clear(self): + os.system("cls" if os.name == "nt" else "clear") + + def render(self, maze, player_position=None, path=None, clear_screen=False): + if clear_screen: + self.clear() + print(maze.render(player_position=player_position, path=path)) + print() diff --git a/petryainiyas/task2/observer/observer.py b/petryainiyas/task2/observer/observer.py new file mode 100644 index 0000000..0ccca59 --- /dev/null +++ b/petryainiyas/task2/observer/observer.py @@ -0,0 +1,7 @@ +from abc import ABC, abstractmethod + + +class Observer(ABC): + @abstractmethod + def update(self, event): + raise NotImplementedError diff --git a/petryainiyas/task2/requirements.txt b/petryainiyas/task2/requirements.txt new file mode 100644 index 0000000..6ccafc3 --- /dev/null +++ b/petryainiyas/task2/requirements.txt @@ -0,0 +1 @@ +matplotlib diff --git a/petryainiyas/task2/solver/__init__.py b/petryainiyas/task2/solver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/petryainiyas/task2/solver/maze_solver.py b/petryainiyas/task2/solver/maze_solver.py new file mode 100644 index 0000000..7894661 --- /dev/null +++ b/petryainiyas/task2/solver/maze_solver.py @@ -0,0 +1,50 @@ +import time +from core.search_stats import SearchStats + + +class MazeSolver: + def __init__(self, maze, strategy=None): + self.maze = maze + self.strategy = strategy + self.observers = [] + + def setStrategy(self, strategy): + self.strategy = strategy + + def addObserver(self, observer): + if observer not in self.observers: + self.observers.append(observer) + + def removeObserver(self, observer): + if observer in self.observers: + self.observers.remove(observer) + + def notify(self, event): + for observer in self.observers: + observer.update(event) + + def solve(self): + if self.strategy is None: + raise ValueError("Strategy is not set") + self.notify({"type": "search_started", "strategy": self.strategy.name}) + + start_time = time.perf_counter() + path = self.strategy.findPath(self.maze, self.maze.startCell, self.maze.exitCell) + end_time = time.perf_counter() + + stats = SearchStats( + timeMs=(end_time - start_time) * 1000.0, + visitedCells=getattr(self.strategy, "visitedCount", 0), + pathLength=len(path), + path=path, + found=bool(path), + algorithm=getattr(self.strategy, "name", "") + ) + + if stats.found: + self.notify({"type": "path_found", "strategy": stats.algorithm, "length": stats.pathLength}) + else: + self.notify({"type": "path_not_found", "strategy": stats.algorithm}) + + self.notify({"type": "search_finished", "stats": stats}) + return stats diff --git a/petryainiyas/task2/strategies/__init__.py b/petryainiyas/task2/strategies/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/petryainiyas/task2/strategies/astar_strategy.py b/petryainiyas/task2/strategies/astar_strategy.py new file mode 100644 index 0000000..4da5535 --- /dev/null +++ b/petryainiyas/task2/strategies/astar_strategy.py @@ -0,0 +1,45 @@ +import heapq +from strategies.pathfinding_strategy import PathFindingStrategy + + +class AStarStrategy(PathFindingStrategy): + name = "A*" + + def heuristic(self, cell, exitCell): + return abs(cell.x - exitCell.x) + abs(cell.y - exitCell.y) + + def findPath(self, maze, start, exitCell): + self.visitedCount = 0 + if start is None or exitCell is None: + return [] + + open_set = [] + heapq.heappush(open_set, (0, 0, start.x, start.y, start)) + parent = {} + g_score = {(start.x, start.y): 0} + closed = set() + + while open_set: + f_score, current_g, _, _, current = heapq.heappop(open_set) + pos = (current.x, current.y) + + if pos in closed: + continue + + closed.add(pos) + self.visitedCount += 1 + + if current.x == exitCell.x and current.y == exitCell.y: + return self._restore_path(parent, start, exitCell) + + for neighbor in maze.getNeighbors(current): + npos = (neighbor.x, neighbor.y) + tentative_g = current_g + getattr(neighbor, "weight", 1) + + if tentative_g < g_score.get(npos, float("inf")): + g_score[npos] = tentative_g + parent[npos] = current + new_f = tentative_g + self.heuristic(neighbor, exitCell) + heapq.heappush(open_set, (new_f, tentative_g, neighbor.x, neighbor.y, neighbor)) + + return [] diff --git a/petryainiyas/task2/strategies/bfs_strategy.py b/petryainiyas/task2/strategies/bfs_strategy.py new file mode 100644 index 0000000..7a98b50 --- /dev/null +++ b/petryainiyas/task2/strategies/bfs_strategy.py @@ -0,0 +1,31 @@ +from collections import deque +from strategies.pathfinding_strategy import PathFindingStrategy + + +class BFSStrategy(PathFindingStrategy): + name = "BFS" + + def findPath(self, maze, start, exitCell): + self.visitedCount = 0 + if start is None or exitCell is None: + return [] + + queue = deque([start]) + visited = {(start.x, start.y)} + parent = {} + + while queue: + current = queue.popleft() + self.visitedCount += 1 + + if current.x == exitCell.x and current.y == exitCell.y: + return self._restore_path(parent, start, exitCell) + + for neighbor in maze.getNeighbors(current): + pos = (neighbor.x, neighbor.y) + if pos not in visited: + visited.add(pos) + parent[pos] = current + queue.append(neighbor) + + return [] diff --git a/petryainiyas/task2/strategies/dfs_strategy.py b/petryainiyas/task2/strategies/dfs_strategy.py new file mode 100644 index 0000000..36451b3 --- /dev/null +++ b/petryainiyas/task2/strategies/dfs_strategy.py @@ -0,0 +1,35 @@ +from strategies.pathfinding_strategy import PathFindingStrategy + + +class DFSStrategy(PathFindingStrategy): + name = "DFS" + + def findPath(self, maze, start, exitCell): + self.visitedCount = 0 + if start is None or exitCell is None: + return [] + + stack = [start] + visited = set() + parent = {} + + while stack: + current = stack.pop() + pos = (current.x, current.y) + if pos in visited: + continue + + visited.add(pos) + self.visitedCount += 1 + + if current.x == exitCell.x and current.y == exitCell.y: + return self._restore_path(parent, start, exitCell) + + neighbors = maze.getNeighbors(current) + for neighbor in reversed(neighbors): + npos = (neighbor.x, neighbor.y) + if npos not in visited: + parent[npos] = current + stack.append(neighbor) + + return [] diff --git a/petryainiyas/task2/strategies/dijkstra_strategy.py b/petryainiyas/task2/strategies/dijkstra_strategy.py new file mode 100644 index 0000000..fd3163f --- /dev/null +++ b/petryainiyas/task2/strategies/dijkstra_strategy.py @@ -0,0 +1,41 @@ +import heapq +from strategies.pathfinding_strategy import PathFindingStrategy + + +class DijkstraStrategy(PathFindingStrategy): + name = "Dijkstra" + + def findPath(self, maze, start, exitCell): + self.visitedCount = 0 + if start is None or exitCell is None: + return [] + + pq = [(0, start.x, start.y, start)] + dist = {(start.x, start.y): 0} + parent = {} + closed = set() + + while pq: + current_cost, _, _, current = heapq.heappop(pq) + pos = (current.x, current.y) + + if pos in closed: + continue + + closed.add(pos) + self.visitedCount += 1 + + if current.x == exitCell.x and current.y == exitCell.y: + return self._restore_path(parent, start, exitCell) + + for neighbor in maze.getNeighbors(current): + npos = (neighbor.x, neighbor.y) + step_cost = getattr(neighbor, "weight", 1) + new_cost = current_cost + step_cost + + if new_cost < dist.get(npos, float("inf")): + dist[npos] = new_cost + parent[npos] = current + heapq.heappush(pq, (new_cost, neighbor.x, neighbor.y, neighbor)) + + return [] diff --git a/petryainiyas/task2/strategies/pathfinding_strategy.py b/petryainiyas/task2/strategies/pathfinding_strategy.py new file mode 100644 index 0000000..17b3ee4 --- /dev/null +++ b/petryainiyas/task2/strategies/pathfinding_strategy.py @@ -0,0 +1,30 @@ +from abc import ABC, abstractmethod + + +class PathFindingStrategy(ABC): + name = "Base" + + def __init__(self): + self.visitedCount = 0 + + @abstractmethod + def findPath(self, maze, start, exitCell): + raise NotImplementedError + + def _restore_path(self, parent, start, exitCell): + if exitCell is None or start is None: + return [] + + path = [] + current = exitCell + + while True: + path.append(current) + if current.x == start.x and current.y == start.y: + break + current = parent.get((current.x, current.y)) + if current is None: + return [] + + path.reverse() + return path