Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 28 additions & 24 deletions src/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import secrets

from fastapi import FastAPI, Path
from starlette.responses import FileResponse
from starlette.responses import Response
from fastapi.middleware.cors import CORSMiddleware

from utils import globals as my_globals
Expand All @@ -14,63 +14,67 @@
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
_FILE_NAME = my_globals.DEFAULT_FLOOR_NAME + my_globals.JSON_SUFFIX
_CONTENT_DISPOSITION = f'attachment; filename="{_FILE_NAME}"'
_ROOT = "/"
_GENERATE = "/gen"


def _generate_floor_response(seed: str, floor_id: int) -> Response:
"""
Generates a floor and returns it as a json file without writing it to disk.
@param seed: seed for the floor
@param floor_id: id for the floor
@return: Floor as json file
"""
generator = Generator(seed=seed, output_file_name=_FILE_NAME, stage_id=floor_id)
generator.generate()
return Response(
content=generator.to_json(1),
media_type="application/json",
headers={"Content-Disposition": _CONTENT_DISPOSITION},
)


@app.get(_ROOT)
async def root():
"""This endpoint returns a test message."""
return {"message": "Test"}


@app.get(_GENERATE + "/{floor_id}")
async def generate_with_id(floor_id: int = Path(ge=0)) -> FileResponse:
def generate_with_id(floor_id: int = Path(ge=0)) -> Response:
"""
This endpoint generates a floor with the given id
and returns it in a FileResponse as a json file.
and returns it as a json file.
@param floor_id id for the floor
@return: Floor in json file
"""
generator = Generator(
seed=secrets.token_hex(16), output_file_name=_FILE_NAME, stage_id=floor_id
)
generator.generate()
path = generator.save()
return FileResponse(path=path, filename=_FILE_NAME)
return _generate_floor_response(secrets.token_hex(16), floor_id)


@app.get(_GENERATE + "/{floor_id}/{seed}")
async def generate_with_id_and_seed(
def generate_with_id_and_seed(
floor_id: int = Path(ge=0), seed: str = Path()
) -> FileResponse:
) -> Response:
"""
This endpoint generates a floor with the given id
and seed and returns it in a FileResponse as a json file.
and seed and returns it as a json file.
@param floor_id: id for the floor
@param seed: seed for the floor
@return: Floor in json file
"""
generator = Generator(seed=seed, output_file_name=_FILE_NAME, stage_id=floor_id)
generator.generate()
path = generator.save()
return FileResponse(path=path, filename=_FILE_NAME)
return _generate_floor_response(seed, floor_id)


@app.get(_GENERATE)
async def generate() -> FileResponse:
def generate() -> Response:
"""
This endpoint generates a floor with the id zero and returns it as a json file.
@return: Floor as json file
"""
generator = Generator(
seed=secrets.token_hex(16), output_file_name=_FILE_NAME, stage_id=0
)
generator.generate()
path = generator.save()
return FileResponse(path=path, filename=_FILE_NAME)
return _generate_floor_response(secrets.token_hex(16), 0)
16 changes: 9 additions & 7 deletions src/generators/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def __init__(
self._output_file_name = output_file_name
self._output_file_path = output_file_path
self._floor: T = Floor(my_globals.FLOOR_WIDTH, my_globals.FLOOR_HEIGHT, seed)
random.seed(seed)
self._random = random.Random(seed)

def to_json(self, indent: int) -> str:
"""
Expand Down Expand Up @@ -96,7 +96,7 @@ def _add_new_room(
and (
self._floor.count_neighbours(new_room_tuple[0], new_room_tuple[1]) <= 1
)
and util_functions.place_room()
and util_functions.place_room(self._random)
):
room_tuple_queue.append(new_room_tuple)
self._floor.add_to_floor_grid(*new_room_tuple)
Expand All @@ -108,14 +108,16 @@ def generate(self, new_seed=None) -> None:
Generates the rooms for the floor.
"""
if new_seed is not None:
random.seed(new_seed)
self._random.seed(new_seed)
self._seed = new_seed
self._create_floor()
floor = self._floor
number_of_rooms = util_functions.calculate_room_amount(self._stage_id)
number_of_rooms = util_functions.calculate_room_amount(
self._stage_id, self._random
)

# Add start room
start_room: tuple = (random.randint(0, 8), random.randint(0, 7))
start_room: tuple = (self._random.randint(0, 8), self._random.randint(0, 7))
floor.add_room(start_room[0], start_room[1], RoomType.START_ROOM)
number_of_current_rooms = 1
room_tuple_queue: deque = deque([]) # Room coordinates
Expand Down Expand Up @@ -222,14 +224,14 @@ def add_boss_room(self, dead_end_indices: list, start_room: tuple) -> None:
self._place_boss_with_teleport_room(boss_room)
boss_room_placed = True

elif len(possible_locations) >= 2 and random.randint(0, 10) < 5:
elif len(possible_locations) >= 2 and self._random.randint(0, 10) < 5:
# Create a 2 * 2 boss-room
boss_room_placed = self._place_big_boss_room(possible_locations, boss_room)

if (
not boss_room_placed
and len(possible_locations) >= 1
and random.randint(0, 10) < 5
and self._random.randint(0, 10) < 5
):
# Create a 1 * 2 boss-room
floor.add_room_next_to(boss_room, possible_locations[0], RoomType.BOSS_ROOM)
Expand Down
11 changes: 7 additions & 4 deletions src/utils/util_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,23 +37,26 @@ def get_picture_for_room_type(room_type: RoomType):
return None


def calculate_room_amount(stage_id: int):
def calculate_room_amount(stage_id: int, rng: random.Random) -> int:
"""
Calculates the room amount.
@param stage_id: id for the floor
@param rng: random number generator to use
:return: room amount
"""
return min(
my_globals.MAX_ROOMS,
int(random.randint(0, 1) + 5 + math.floor(stage_id * 10) / 3.0),
int(rng.randint(0, 1) + 5 + math.floor(stage_id * 10) / 3.0),
)


def place_room() -> bool:
def place_room(rng: random.Random) -> bool:
"""
Checks if a new room should be placed.
@param rng: random number generator to use
:return: True when room should be placed otherwise False
"""
return random.randint(1, 50) >= 25
return rng.randint(1, 50) >= 25


def rgb2hex(r, g, b):
Expand Down
44 changes: 44 additions & 0 deletions test/controller_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
"""Unit tests for the FastAPI controller."""

import inspect
import json
import os
import tempfile
import unittest

from fastapi.testclient import TestClient
Expand All @@ -11,6 +15,46 @@ class ControllerTest(unittest.TestCase):
def setUp(self) -> None:
self._client = TestClient(controller.app)

def test_generate_returns_the_floor_as_json_file(self):
response = self._client.get("/gen/2/abc")

self.assertEqual(200, response.status_code)
self.assertEqual("application/json", response.headers["content-type"])
self.assertEqual(
'attachment; filename="floor.json"',
response.headers["content-disposition"],
)
self.assertEqual("abc", json.loads(response.text)["_floor"]["_seed"])

def test_generate_does_not_write_files(self):
working_directory = os.getcwd()
with tempfile.TemporaryDirectory() as tmp_dir:
os.chdir(tmp_dir)
try:
self._client.get("/gen")
self._client.get("/gen/1")
self._client.get("/gen/1/abc")
self.assertEqual([], os.listdir(tmp_dir))
finally:
os.chdir(working_directory)

def test_generate_endpoints_run_outside_the_event_loop(self):
# Synchronous endpoints are run in a thread pool by FastAPI,
# so a long generation does not block other requests.
for endpoint in (
controller.generate,
controller.generate_with_id,
controller.generate_with_id_and_seed,
):
with self.subTest(endpoint=endpoint.__name__):
self.assertFalse(inspect.iscoroutinefunction(endpoint))

def test_cors_allows_any_origin_without_credentials(self):
response = self._client.get("/", headers={"Origin": "https://example.com"})

self.assertEqual("*", response.headers["access-control-allow-origin"])
self.assertNotIn("access-control-allow-credentials", response.headers)

def test_negative_floor_id_is_rejected(self):
response = self._client.get("/gen/-1")

Expand Down
12 changes: 12 additions & 0 deletions test/generator_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""
import json
import os
import random
import tempfile
import unittest
from collections import deque
Expand Down Expand Up @@ -152,6 +153,17 @@ def test_to_json_round_trips_through_floor_from_json(self):

self.assertEqual(self._generator._floor.get_rooms(), restored.get_rooms())

def test_generate_is_not_affected_by_the_global_random_state(self):
expected = Generator("rng-seed", "test", "", 1)
expected.generate()
generator = Generator("rng-seed", "test", "", 1)
random.seed(999)
random.random()

generator.generate()

self.assertEqual(expected._floor.get_rooms(), generator._floor.get_rooms())

def test_negative_stage_id_is_rejected(self):
with self.assertRaises(ValueError):
Generator(seed="1", output_file_name="floor", stage_id=-1)
Expand Down
5 changes: 3 additions & 2 deletions test/util_test.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import random
import unittest
from utils import globals, util_functions


class UtilsTest(unittest.TestCase):
def test_calculate_room_amount(self):
rooms = util_functions.calculate_room_amount(2)
rooms = util_functions.calculate_room_amount(2, random.Random(1))
self.assertTrue(0 < rooms <= globals.MAX_ROOMS)

def test_place_room(self):
util_functions.place_room()
util_functions.place_room(random.Random(1))


if __name__ == "__main__":
Expand Down
Loading