From 1ecf8e815965e64493039ff9ab8701b8d5331aba Mon Sep 17 00:00:00 2001 From: Joltras <72395471+Joltras@users.noreply.github.com> Date: Mon, 2 Nov 2026 12:03:36 +0000 Subject: [PATCH 1/8] refactor: fix typo in has_boss_room_as_neighbour --- src/floors/floor.py | 2 +- src/generators/generator.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/floors/floor.py b/src/floors/floor.py index 616489f..9791b2e 100644 --- a/src/floors/floor.py +++ b/src/floors/floor.py @@ -252,7 +252,7 @@ def _get_room(self, coordinates: Tuple[int, int]) -> Optional[Room]: return room return None - def has_boos_room_as_neighbour(self, coordinates: Tuple[int, int]) -> bool: + def has_boss_room_as_neighbour(self, coordinates: Tuple[int, int]) -> bool: """ Checks if the given coordinates have a boss room as neighbour. @param coordinates: coordinates to check diff --git a/src/generators/generator.py b/src/generators/generator.py index 8e7c97f..ff06e2e 100644 --- a/src/generators/generator.py +++ b/src/generators/generator.py @@ -336,7 +336,7 @@ def place_super_secret_room(self) -> None: if ( floor.is_within_border(neighbour) and not floor.contains_room(neighbour) - and not floor.has_boos_room_as_neighbour(neighbour) + and not floor.has_boss_room_as_neighbour(neighbour) and floor.count_neighbours(neighbour[0], neighbour[1]) == 1 ): distance = util_functions.calculate_distance(boss_room, neighbour) @@ -381,7 +381,7 @@ def _check_directions_for_secret_room( current_neighbour = floor.count_neighbours(neighbour[0], neighbour[1]) if ( current_neighbour >= neighbour_rooms - and not floor.has_boos_room_as_neighbour(neighbour) + and not floor.has_boss_room_as_neighbour(neighbour) ): floor.add_room(neighbour[0], neighbour[1], RoomType.SECRET_ROOM) return True From 972009a1b0a7255d4efabf4f375c385502dc6071 Mon Sep 17 00:00:00 2001 From: Joltras <72395471+Joltras@users.noreply.github.com> Date: Mon, 2 Nov 2026 12:04:13 +0000 Subject: [PATCH 2/8] refactor: remove dead code and unused attributes --- src/floors/tkinter_floor.py | 1 - src/generators/generator.py | 4 ---- src/generators/tkinter_generator.py | 9 +++------ 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/floors/tkinter_floor.py b/src/floors/tkinter_floor.py index 39c0cf7..a2a091d 100644 --- a/src/floors/tkinter_floor.py +++ b/src/floors/tkinter_floor.py @@ -27,7 +27,6 @@ def __init__(self, height: int, width: int, canvas, name: str, seed: str): """ super().__init__(height, width, seed) self._canvas = canvas - self._first_draw = True self.name = name self._after_id: Optional[str] = None diff --git a/src/generators/generator.py b/src/generators/generator.py index ff06e2e..806648b 100644 --- a/src/generators/generator.py +++ b/src/generators/generator.py @@ -130,12 +130,8 @@ def generate(self, new_seed=None) -> None: if len(room_tuple_queue) == 0 and number_of_current_rooms < number_of_rooms: room_tuple_queue.append(room_tuple_list.pop(0)) - # room_tuple_queue.remove(start_room) while len(room_tuple_queue) > 0: room_tuple_list.append(room_tuple_queue.pop()) - while len(room_tuple_queue) > 0: - room_tuple = room_tuple_queue.pop() - room_tuple_list.append(room_tuple) room_tuple_list.remove(start_room) while len(room_tuple_list) > 0: room_tuple = room_tuple_list.pop() diff --git a/src/generators/tkinter_generator.py b/src/generators/tkinter_generator.py index c6a27f6..0913f72 100644 --- a/src/generators/tkinter_generator.py +++ b/src/generators/tkinter_generator.py @@ -46,7 +46,6 @@ def __init__( ) self._path = tk.StringVar() self._path.set(self._output_file_path) - self._menu_bar = tk.Menu(self._tk) self._theme_handler = ThemeHandler(self._tk, self._canvas) self._floor_manager = FloorManager(output_file_name) self._tk.protocol("WM_DELETE_WINDOW", self.quit) @@ -67,13 +66,11 @@ def _create_floor(self) -> None: def _generate_and_draw_floor(self) -> None: """ - Cals the generate method and the draw method of the floor. + Calls the generate method and the draw method of the floor. """ self._floor.stop_drawing() self.generate(self._floor_manager.next_seed.get()) - self._floor_manager.next_seed.set(self._floor_manager.next_seed.get()) self._floor_manager.next_seed.set(secrets.token_hex(16)) - # self._floors[self._current_floor].draw(tk) self._floor_manager.get_current_floor().draw_step_by_step() self._floor = self._floor_manager.get_current_floor() @@ -84,7 +81,7 @@ def open(self) -> None: """ options = util_functions.json_file_options.copy() options["initialdir"] = self._output_file_path - options["title"] = "Open File" + options["title"] = util_functions.open_file_text path = filedialog.askopenfilename(**options) if len(path) > 0: self._output_file_path = os.path.dirname(path) @@ -130,7 +127,7 @@ def save(self, path: str = "") -> str: options = util_functions.json_file_options.copy() options["initialdir"] = self._output_file_path options["initialfile"] = self._output_file_name - options["title"] = "Save File" + options["title"] = util_functions.save_file_text file_path = filedialog.asksaveasfilename(**options) if len(file_path) > 0: self._output_file_path = os.path.dirname(file_path) From de3ba8a48c971a539c1da29ac95ea9bad4dac393 Mon Sep 17 00:00:00 2001 From: Joltras <72395471+Joltras@users.noreply.github.com> Date: Mon, 2 Nov 2026 12:04:41 +0000 Subject: [PATCH 3/8] refactor: use the floor dimensions consistently in the generator --- src/generators/generator.py | 7 +++++-- test/generator_test.py | 6 ++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/generators/generator.py b/src/generators/generator.py index 806648b..473e854 100644 --- a/src/generators/generator.py +++ b/src/generators/generator.py @@ -45,7 +45,7 @@ def __init__( self._output_file_name = output_file_name self._output_file_path = output_file_path self._floor: Floor = Floor( - my_globals.FLOOR_WIDTH, my_globals.FLOOR_HEIGHT, seed + my_globals.FLOOR_HEIGHT, my_globals.FLOOR_WIDTH, seed ) self._random = random.Random(seed) @@ -112,7 +112,10 @@ def generate(self, new_seed=None) -> None: ) # Add start room - start_room: tuple = (self._random.randint(0, 8), self._random.randint(0, 7)) + start_room: tuple = ( + self._random.randint(0, my_globals.FLOOR_WIDTH - 1), + self._random.randint(0, my_globals.FLOOR_HEIGHT - 1), + ) floor.add_room(start_room[0], start_room[1], RoomType.START_ROOM) number_of_current_rooms = 1 room_tuple_queue: deque = deque([]) # Room coordinates diff --git a/test/generator_test.py b/test/generator_test.py index d94118f..b62f53d 100644 --- a/test/generator_test.py +++ b/test/generator_test.py @@ -173,6 +173,12 @@ def test_generate_is_not_affected_by_the_global_random_state(self): self.assertEqual(expected._floor.get_rooms(), generator._floor.get_rooms()) + def test_initial_floor_has_the_floor_size(self): + floor = Generator("1", "test")._floor + + self.assertEqual((FLOOR_WIDTH - 1, FLOOR_HEIGHT - 1), floor.bottom_right()) + self.assertEqual((FLOOR_HEIGHT, FLOOR_WIDTH), floor.get_floor_grid().shape) + def test_negative_stage_id_is_rejected(self): with self.assertRaises(ValueError): Generator(seed="1", output_file_name="floor", stage_id=-1) From 416eb71e03c1d8ba7766492d5f37aa47350a3af6 Mon Sep 17 00:00:00 2001 From: Joltras <72395471+Joltras@users.noreply.github.com> Date: Mon, 2 Nov 2026 12:05:25 +0000 Subject: [PATCH 4/8] refactor: add generated floors directly to the floor manager --- src/generators/floor_manager.py | 22 ---------------------- src/generators/tkinter_generator.py | 3 +-- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/src/generators/floor_manager.py b/src/generators/floor_manager.py index 202eb30..476339a 100644 --- a/src/generators/floor_manager.py +++ b/src/generators/floor_manager.py @@ -7,8 +7,6 @@ from collections import deque from typing import Deque -from utils import globals as my_globals - from floors.tkinter_floor import TkinterFloor @@ -30,26 +28,6 @@ def __init__(self, output_file_name: str) -> None: self.next_seed = tk.StringVar() self.next_seed.set(secrets.token_hex(16)) - def add_new_floor(self, floor: TkinterFloor, canvas) -> None: - """ - Adds a new floor to the floor manager. - @param floor: floor to add - @param canvas: canvas to draw on - """ - self._floors.append( - TkinterFloor( - my_globals.FLOOR_HEIGHT, - my_globals.FLOOR_WIDTH, - canvas=canvas, - name=my_globals.DEFAULT_FLOOR_NAME + my_globals.JSON_SUFFIX, - seed=floor.seed, - ) - ) - self.current_floor_index = len(self._floors) - 1 - current = self._floors[self.current_floor_index] - self.current_floor_name.set(current.name) - self.current_floor_seed_var.set(current.seed) - def add_floor(self, floor: TkinterFloor) -> None: """ Adds a floor to the floor manager. diff --git a/src/generators/tkinter_generator.py b/src/generators/tkinter_generator.py index 0913f72..da272ba 100644 --- a/src/generators/tkinter_generator.py +++ b/src/generators/tkinter_generator.py @@ -61,8 +61,7 @@ def _create_floor(self) -> None: my_globals.DEFAULT_FLOOR_NAME + my_globals.JSON_SUFFIX, self._seed, ) - self._floor_manager.add_new_floor(self._floor, self._canvas) - self._floor = self._floor_manager.get_current_floor() + self._floor_manager.add_floor(self._floor) def _generate_and_draw_floor(self) -> None: """ From 8a9f708fa395a5939bfd02907dc8c233490a9052 Mon Sep 17 00:00:00 2001 From: Joltras <72395471+Joltras@users.noreply.github.com> Date: Mon, 2 Nov 2026 12:06:00 +0000 Subject: [PATCH 5/8] refactor: simplify direction and special room checks --- src/floors/floor.py | 20 ++++---------------- src/utils/room_type.py | 17 ++++++----------- test/floor_test.py | 14 ++++++++++++++ 3 files changed, 24 insertions(+), 27 deletions(-) diff --git a/src/floors/floor.py b/src/floors/floor.py index 9791b2e..9f60546 100644 --- a/src/floors/floor.py +++ b/src/floors/floor.py @@ -131,22 +131,10 @@ def add_room_next_to( @param direction: direction in which the new room is placed @param room_type: type for the room """ - if direction == Direction.UP: - self.add_room(room[0], room[1] - 1, room_type) - elif direction == Direction.DOWN: - self.add_room(room[0], room[1] + 1, room_type) - elif direction == Direction.LEFT: - self.add_room(room[0] - 1, room[1], room_type) - elif direction == Direction.RIGHT: - self.add_room(room[0] + 1, room[1], room_type) - elif direction == Direction.UP_RIGHT: - self.add_room(room[0] + 1, room[1] - 1, room_type) - elif direction == Direction.UP_LEFT: - self.add_room(room[0] - 1, room[1] - 1, room_type) - elif direction == Direction.DOWN_RIGHT: - self.add_room(room[0] + 1, room[1] + 1, room_type) - elif direction == Direction.DOWN_LEFT: - self.add_room(room[0] - 1, room[1] + 1, room_type) + x, y = utils.util_functions.add_direction_to_coordinates( + direction, (room[0], room[1]) + ) + self.add_room(x, y, room_type) def add_teleport_room(self, room: Room) -> None: """ diff --git a/src/utils/room_type.py b/src/utils/room_type.py index 034007c..edd1ca2 100644 --- a/src/utils/room_type.py +++ b/src/utils/room_type.py @@ -38,17 +38,12 @@ def is_special(self) -> bool: - Secret room @return: True if special otherwise False """ - switcher = { - self.NORMAL_ROOM: False, - self.DEAD_END: False, - self.ITEM_ROOM: True, - self.SHOP_ROOM: True, - self.START_ROOM: False, - self.TELEPORT_ROOM: False, - self.BOSS_ROOM: True, - self.SECRET_ROOM: True, - } - return switcher[self] + return self in ( + RoomType.ITEM_ROOM, + RoomType.SHOP_ROOM, + RoomType.BOSS_ROOM, + RoomType.SECRET_ROOM, + ) def get_color(self) -> Color: """ diff --git a/test/floor_test.py b/test/floor_test.py index 5a273a6..d9a2fa2 100644 --- a/test/floor_test.py +++ b/test/floor_test.py @@ -6,6 +6,7 @@ from utils.globals import DoorFace from rooms.room import Room from utils.room_type import RoomType +from utils.direction import Direction class FloorTest(unittest.TestCase): @@ -32,6 +33,19 @@ def test_add_two_floor_grid(self): self._floor.add_to_floor_grid(2, 3) self.assertTrue(self._floor.contains_room((2, 3))) + def test_add_room_next_to_places_the_room_in_the_given_direction(self): + room = Room(5, 5, 0, RoomType.NORMAL_ROOM) + for direction in Direction: + with self.subTest(direction=direction): + floor = Floor(20, 30, "1") + + floor.add_room_next_to(room, direction, RoomType.BOSS_ROOM) + + expected = (5 + direction.value[0], 5 + direction.value[1]) + added = floor.get_rooms()[0] + self.assertEqual(expected, (added[0], added[1])) + self.assertEqual(RoomType.BOSS_ROOM, added.get_type()) + def test_add_room(self): self._floor.add_room(1, 1) self.assertTrue(self._floor.contains_room((1, 1))) From 0ec491386392158aa89ee9bdc0f91e0fc8ddfc39 Mon Sep 17 00:00:00 2001 From: Joltras <72395471+Joltras@users.noreply.github.com> Date: Mon, 2 Nov 2026 12:06:26 +0000 Subject: [PATCH 6/8] fix: raise index error for invalid room indices --- src/rooms/room.py | 2 +- test/room_test.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/rooms/room.py b/src/rooms/room.py index 0adfcab..aa23acb 100644 --- a/src/rooms/room.py +++ b/src/rooms/room.py @@ -40,7 +40,7 @@ def __getitem__(self, index: int) -> int: @return: Returns x if index equals 0 and y if index equals 1 """ if index < 0 or index > 1: - raise ValueError(str(index) + "is not a valid index") + raise IndexError(f"{index} is not a valid index") if index == 0: return self._x return self._y diff --git a/test/room_test.py b/test/room_test.py index 2a90ad0..a350e89 100644 --- a/test/room_test.py +++ b/test/room_test.py @@ -66,9 +66,13 @@ def test_set_cord(self): self.assertEqual(20, self._room[1]) def test_get_cord_with_exception(self): - with self.assertRaises(ValueError, msg="20 is not a valid index"): + with self.assertRaises(IndexError, msg="20 is not a valid index"): _ = self._room[20] + def test_room_can_be_unpacked_into_its_coordinates(self): + x, y = self._room + self.assertEqual((10, 20), (x, y)) + if __name__ == "__main__": unittest.main() From 31ef0757c1db4eb184c353b2290ba6bc24e00412 Mon Sep 17 00:00:00 2001 From: Joltras <72395471+Joltras@users.noreply.github.com> Date: Mon, 2 Nov 2026 12:06:46 +0000 Subject: [PATCH 7/8] refactor: close the output file with a context manager --- src/generators/generator.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/generators/generator.py b/src/generators/generator.py index 473e854..6febdcc 100644 --- a/src/generators/generator.py +++ b/src/generators/generator.py @@ -397,7 +397,6 @@ def save(self, path: str = "") -> str: n = self._output_file_name + time + my_globals.JSON_SUFFIX output = os.path.join(self._output_file_path, n) - f = open(output, "w", encoding="utf-8") - f.write(self.to_json(1)) - f.close() + with open(output, "w", encoding="utf-8") as file: + file.write(self.to_json(1)) return output From 8cd099f64169eac011bbee4987739d23c3791657 Mon Sep 17 00:00:00 2001 From: Joltras <72395471+Joltras@users.noreply.github.com> Date: Mon, 2 Nov 2026 12:08:05 +0000 Subject: [PATCH 8/8] refactor: parse command line arguments with argparse --- src/main.py | 116 +++++++++++++++++++++++++--------------------- test/main_test.py | 54 +++++++++++++++++++++ 2 files changed, 118 insertions(+), 52 deletions(-) create mode 100644 test/main_test.py diff --git a/src/main.py b/src/main.py index e8a297d..a36a379 100644 --- a/src/main.py +++ b/src/main.py @@ -3,25 +3,65 @@ Parses the command line arguments and calls the generator. """ +import argparse import os.path import secrets -import sys +from typing import List, Optional from utils import globals as my_globals from generators.generator import Generator -from generators.tkinter_generator import TkinterGenerator -HELP_MESSAGE = """Creates a dungeon floor and saves it to a json file. -Options: - -h, --help: Prints this message. - -s, --seed: Sets the seed for the random number generator. - -o, --output: Sets the output file name, default is output.json. - -u, --ui: Enables the UI, default is disabled. - -f --floor: Sets the id for the floor, default is 0. (Must be 0 or greater) + +def _floor_id(value: str) -> int: + """ + Parses a floor id from the command line. + @param value: value to parse + @return: floor id """ + try: + floor_id = int(value) + except ValueError as error: + raise argparse.ArgumentTypeError(f"{value} is not a valid number") from error + if floor_id < 0: + raise argparse.ArgumentTypeError("the floor id must be 0 or greater") + return floor_id -def main(seed: str, output: str, ui: bool, floor_id): +def parse_arguments(argv: Optional[List[str]] = None) -> argparse.Namespace: + """ + Parses the command line arguments. + @param argv: arguments to parse, defaults to the command line arguments + @return: parsed arguments + """ + parser = argparse.ArgumentParser( + description="Creates a dungeon floor and saves it to a json file." + ) + parser.add_argument( + "-s", + "--seed", + default="", + help="seed for the random number generator, default is a random seed", + ) + parser.add_argument( + "-o", + "--output", + default=my_globals.DEFAULT_FLOOR_NAME, + help="output file name, default is %(default)s", + ) + parser.add_argument( + "-u", "--ui", action="store_true", help="enables the UI, default is disabled" + ) + parser.add_argument( + "-f", + "--floor", + type=_floor_id, + default=2, + help="id for the floor, must be 0 or greater, default is %(default)s", + ) + return parser.parse_args(argv) + + +def main(seed: str, output: str, ui: bool, floor_id: int) -> None: """ Main function for the dungeon generator. @param seed: Seed for the random number generator. @@ -33,57 +73,29 @@ def main(seed: str, output: str, ui: bool, floor_id): output = os.path.join(output_folder, output) if not os.path.exists(output_folder): - os.mkdir(output_folder) + os.makedirs(output_folder) print("No output folder found. Creating specific folder") if ui: + # Imported here so the command line works without tkinter + # pylint: disable=import-outside-toplevel + from generators.tkinter_generator import TkinterGenerator + TkinterGenerator( seed, my_globals.DEFAULT_FLOOR_NAME, output_folder, floor_id ).run() else: generator = Generator(seed=seed, output_file_name=output, stage_id=floor_id) generator.generate() - generator.save() - print("Floor saved to " + output) + path = generator.save() + print("Floor saved to " + path) if __name__ == "__main__": - _seed: str = "" - _output: str = "floor" - _show_ui: bool = False - _floor_id: int = 2 - i: int = 1 - while i < len(sys.argv): - if sys.argv[i] == "-s" or sys.argv[i] == "--seed": - if i + 1 < len(sys.argv): - _seed = sys.argv[i + 1] - i += 1 - elif sys.argv[i] == "-u" or sys.argv[i] == "--ui": - _show_ui = True - elif sys.argv[i] == "-h" or sys.argv[i] == "--help": - print(HELP_MESSAGE) - sys.exit(0) - elif sys.argv[i] == "-o" or sys.argv[i] == "--output": - if i + 1 < len(sys.argv): - _output = sys.argv[i + 1] - i += 1 - elif sys.argv[i] == "-f" or sys.argv[i] == "--floor": - if i + 1 < len(sys.argv): - try: - _floor_id = int(sys.argv[i + 1]) - i += 1 - if _floor_id < 0: - print("The floor id must be greater than 0!") - sys.exit(-1) - except ValueError: - print(sys.argv[i + 1] + " is not a valid number!") - sys.exit(-1) - else: - print(sys.argv[i] + " is not a valid argument!") - sys.exit(-1) - i += 1 - - if _seed == "": - _seed = secrets.token_hex(16) - - main(_seed, _output, _show_ui, _floor_id) + arguments = parse_arguments() + main( + arguments.seed or secrets.token_hex(16), + arguments.output, + arguments.ui, + arguments.floor, + ) diff --git a/test/main_test.py b/test/main_test.py new file mode 100644 index 0000000..64dc5ae --- /dev/null +++ b/test/main_test.py @@ -0,0 +1,54 @@ +"""Unit tests for the command line interface.""" + +import contextlib +import io +import json +import os +import tempfile +import unittest +from unittest import mock + +import main + + +class ParseArgumentsTest(unittest.TestCase): + def test_defaults(self): + arguments = main.parse_arguments([]) + + self.assertEqual("", arguments.seed) + self.assertEqual("floor", arguments.output) + self.assertFalse(arguments.ui) + self.assertEqual(2, arguments.floor) + + def test_all_options(self): + arguments = main.parse_arguments(["-s", "abc", "-o", "out", "-u", "-f", "0"]) + + self.assertEqual("abc", arguments.seed) + self.assertEqual("out", arguments.output) + self.assertTrue(arguments.ui) + self.assertEqual(0, arguments.floor) + + def test_invalid_floor_ids_are_rejected(self): + for value in ("-1", "x"): + with self.subTest(value=value), self.assertRaises(SystemExit): + with contextlib.redirect_stderr(io.StringIO()): + main.parse_arguments(["-f", value]) + + +class MainTest(unittest.TestCase): + def test_main_saves_the_floor_in_the_generation_folder(self): + with tempfile.TemporaryDirectory() as tmp_dir, mock.patch( + "utils.globals.APPLICATION_PATH", tmp_dir + ), contextlib.redirect_stdout(io.StringIO()): + main.main("abc", "out", False, 1) + + files = os.listdir(os.path.join(tmp_dir, "generation")) + self.assertEqual(1, len(files)) + with open( + os.path.join(tmp_dir, "generation", files[0]), encoding="utf-8" + ) as file: + self.assertEqual("abc", json.load(file)["_floor"]["_seed"]) + + +if __name__ == "__main__": + unittest.main()