From bade3a5f506d3a26db4dda6c6d0a09b881c77452 Mon Sep 17 00:00:00 2001 From: Joltras <72395471+Joltras@users.noreply.github.com> Date: Mon, 2 Nov 2026 07:38:24 +0000 Subject: [PATCH 1/2] fix: prevent infinite loop in secret room placement --- src/generators/generator.py | 11 ++++------- test/generator_test.py | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/generators/generator.py b/src/generators/generator.py index 5ed5cf6..00a27e8 100644 --- a/src/generators/generator.py +++ b/src/generators/generator.py @@ -336,18 +336,15 @@ def place_super_secret_room(self) -> None: def place_secret_room(self) -> None: """ Places a secret room on the floor. + If no valid position exists, no secret room is placed. """ floor = self._floor - secret_room_placed = False neighbour_rooms = 3 - while not secret_room_placed: + while neighbour_rooms > 0: for room in reversed(floor.get_rooms()): # Check all neighbours of the room - secret_room_placed = self._check_directions_for_secret_room( - room, neighbour_rooms - ) - if secret_room_placed: - break + if self._check_directions_for_secret_room(room, neighbour_rooms): + return neighbour_rooms -= 1 def _check_directions_for_secret_room( diff --git a/test/generator_test.py b/test/generator_test.py index cb3a27b..3b643d2 100644 --- a/test/generator_test.py +++ b/test/generator_test.py @@ -152,6 +152,22 @@ def test_to_json_round_trips_through_floor_from_json(self): self.assertEqual(self._generator._floor.get_rooms(), restored.get_rooms()) + def test_generate_terminates_when_no_secret_room_position_exists(self): + # Seed "9" on floor 0 used to loop forever in place_secret_room. + generator = Generator(seed="9", output_file_name="floor", stage_id=0) + + generator.generate() + + self.assertGreater(len(generator._floor.get_rooms()), 0) + + def test_generate_terminates_for_many_seeds_on_small_floors(self): + for stage_id in (0, 1): + for seed in range(200): + with self.subTest(stage_id=stage_id, seed=seed): + Generator( + seed=str(seed), output_file_name="floor", stage_id=stage_id + ).generate() + def test_save_writes_the_generated_floor_to_the_given_path(self): self._generator.generate() with tempfile.TemporaryDirectory() as tmp_dir: From f746e813b5db07de7e60495fb238310130664375 Mon Sep 17 00:00:00 2001 From: Joltras <72395471+Joltras@users.noreply.github.com> Date: Mon, 2 Nov 2026 10:19:55 +0000 Subject: [PATCH 2/2] fix: reject negative floor ids --- requirements.txt | 3 ++- src/controller.py | 8 +++++--- src/generators/generator.py | 4 +++- test/controller_test.py | 26 ++++++++++++++++++++++++++ test/generator_test.py | 4 ++++ 5 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 test/controller_test.py diff --git a/requirements.txt b/requirements.txt index ada9cc8..a7d694c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,5 @@ pylint~=4.1.3 flake8~=7.4.1 mypy~=2.4.0 pillow~=13.0.0 -pytest~=9.1.1 \ No newline at end of file +pytest~=9.1.1 +httpx~=0.28.1 \ No newline at end of file diff --git a/src/controller.py b/src/controller.py index 3b232ec..c876379 100644 --- a/src/controller.py +++ b/src/controller.py @@ -3,7 +3,7 @@ """ import secrets -from fastapi import FastAPI +from fastapi import FastAPI, Path from starlette.responses import FileResponse from fastapi.middleware.cors import CORSMiddleware @@ -30,7 +30,7 @@ async def root(): @app.get(_GENERATE + "/{floor_id}") -async def generate_with_id(floor_id: int) -> FileResponse: +async def generate_with_id(floor_id: int = Path(ge=0)) -> FileResponse: """ This endpoint generates a floor with the given id and returns it in a FileResponse as a json file. @@ -46,7 +46,9 @@ async def generate_with_id(floor_id: int) -> FileResponse: @app.get(_GENERATE + "/{floor_id}/{seed}") -async def generate_with_id_and_seed(floor_id: int, seed: str) -> FileResponse: +async def generate_with_id_and_seed( + floor_id: int = Path(ge=0), seed: str = Path() +) -> FileResponse: """ This endpoint generates a floor with the given id and seed and returns it in a FileResponse as a json file. diff --git a/src/generators/generator.py b/src/generators/generator.py index 00a27e8..a75b44a 100644 --- a/src/generators/generator.py +++ b/src/generators/generator.py @@ -36,8 +36,10 @@ def __init__( Creates a new generator with the given arguments. @param seed: seed for generating the floor @param output_file_name: name for the outputfile - @param stage_id: id for the floor + @param stage_id: id for the floor (must be 0 or greater) """ + if stage_id < 0: + raise ValueError(f"stage_id must be 0 or greater, got {stage_id}") self._stage_id = stage_id self._seed = seed self._output_file_name = output_file_name diff --git a/test/controller_test.py b/test/controller_test.py new file mode 100644 index 0000000..c4bf63d --- /dev/null +++ b/test/controller_test.py @@ -0,0 +1,26 @@ +"""Unit tests for the FastAPI controller.""" + +import unittest + +from fastapi.testclient import TestClient + +import controller + + +class ControllerTest(unittest.TestCase): + def setUp(self) -> None: + self._client = TestClient(controller.app) + + def test_negative_floor_id_is_rejected(self): + response = self._client.get("/gen/-1") + + self.assertEqual(422, response.status_code) + + def test_negative_floor_id_with_seed_is_rejected(self): + response = self._client.get("/gen/-1/abc") + + self.assertEqual(422, response.status_code) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/generator_test.py b/test/generator_test.py index 3b643d2..4de80d3 100644 --- a/test/generator_test.py +++ b/test/generator_test.py @@ -152,6 +152,10 @@ def test_to_json_round_trips_through_floor_from_json(self): self.assertEqual(self._generator._floor.get_rooms(), restored.get_rooms()) + def test_negative_stage_id_is_rejected(self): + with self.assertRaises(ValueError): + Generator(seed="1", output_file_name="floor", stage_id=-1) + def test_generate_terminates_when_no_secret_room_position_exists(self): # Seed "9" on floor 0 used to loop forever in place_secret_room. generator = Generator(seed="9", output_file_name="floor", stage_id=0)