diff --git a/overcooked_simulator/counters.py b/overcooked_simulator/counters.py new file mode 100644 index 0000000000000000000000000000000000000000..77ab32f890e3c0a464b168f11b5739fca438978f --- /dev/null +++ b/overcooked_simulator/counters.py @@ -0,0 +1,12 @@ +import numpy as np + + +class Counter: + """Simple class for a counter at a specified position (center of counter). Can hold things on top.""" + + def __init__(self, pos: np.array): + self.pos = pos + self.occupied_by = None + + def __repr__(self): + return f"Counter(pos:{str(self.pos)},holds:{self.occupied_by})" diff --git a/overcooked_simulator/layouts/basic.layout b/overcooked_simulator/layouts/basic.layout index 65a375bb3b13216a9737eaf9be24f8926cbb5a33..82524fc6037ce7d9a7a11667bd290819986fa268 100644 --- a/overcooked_simulator/layouts/basic.layout +++ b/overcooked_simulator/layouts/basic.layout @@ -2,9 +2,9 @@ EEEEEEEEEEE ECCCCCCCCCE ECEEEEEEECE ECEEEEEEECE -ECEEEEEEECE -ECEEEEEEECE -ECEEEEEEECE +ECEEEEEEEEE +ECEEEEEEEEE +ECEEEEEEEEE ECEEEEEEECE ECEEEEEEECE ECCCCCCCCCE diff --git a/overcooked_simulator/layouts/empty.layout b/overcooked_simulator/layouts/empty.layout new file mode 100644 index 0000000000000000000000000000000000000000..9fb75b8d4f4c7faa7ba59d138746231ada07c7b0 --- /dev/null +++ b/overcooked_simulator/layouts/empty.layout @@ -0,0 +1 @@ +E \ No newline at end of file diff --git a/overcooked_simulator/main.py b/overcooked_simulator/main.py index 168a240f80ef4f2284cec14dc0058e07708d0397..a53e56731e0f3749f9aa3573e115afef660e9753 100644 --- a/overcooked_simulator/main.py +++ b/overcooked_simulator/main.py @@ -1,17 +1,22 @@ from overcooked_simulator.player import Player import sys +from pathlib import Path + +from overcooked_simulator.player import Player +from overcooked_simulator.pygame_gui import PyGameGUI from overcooked_simulator.simulation_runner import Simulator -from overcooked_simulator.overcooked_environment import Environment -def main(): - simulator = Simulator(Environment, 300) +def main(): + simulator = Simulator(Path("overcooked_simulator/layouts/basic.layout"), 300) simulator.register_player(Player("p1", [100, 200])) simulator.register_player(Player("p2", [200, 100])) + gui = PyGameGUI(simulator) + simulator.start() - print(simulator.get_state()) + gui.start_pygame() simulator.stop() sys.exit() diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py index 71060740332ed8a2c041cfac8a6dd841c1edeb64..8fe744c7667809e596b96ac759c9bbaffeab79f4 100644 --- a/overcooked_simulator/overcooked_environment.py +++ b/overcooked_simulator/overcooked_environment.py @@ -1,44 +1,44 @@ from __future__ import annotations + from typing import TYPE_CHECKING if TYPE_CHECKING: from overcooked_simulator.player import Player from pathlib import Path import numpy as np - - -class Counter: - """Simple class for a counter at a specified position (center of counter). Can hold things on top. - """ - def __init__(self, pos: np.array): - self.pos = pos - self.occupied_by = None - - def __repr__(self): - return f"Counter(pos:{str(self.pos)},holds:{self.occupied_by})" +from scipy.spatial import distance_matrix +from overcooked_simulator.counters import Counter class Action: - """Action class, specifies player, action type and action itself. - """ + """Action class, specifies player, action type and action itself.""" + def __init__(self, player, act_type, action): self.player = player self.act_type = act_type - assert self.act_type in ["movement", "pickup", "interact"], "Unknown action type" + assert self.act_type in [ + "movement", + "pickup", + "interact", + ], "Unknown action type" self.action = action class Environment: """Environment class which handles the game logic for the overcooked-inspired environment. - Handles player movement, collision-detection, counters, cooking processes, recipes, incoming orders, time. - """ - def __init__(self): - self.players = {} - self.counter_side_length = 40 - self.layout_path = Path("overcooked_simulator/layouts/basic.layout") - self.counters = self.create_counters(self.layout_path) - self.score = 0 + Handles player movement, collision-detection, counters, cooking processes, recipes, incoming orders, time. + # TODO Abstract base class for different environments + """ + + def __init__(self, layout_path): + self.players: dict[str, Player] = {} + self.counter_side_length: float = 40 + self.layout_path: Path = layout_path + self.counters: list[Counter] = self.create_counters(self.layout_path) + self.score: int = 0 + self.world_width: int = 800 + self.world_height: int = 600 def create_counters(self, layout_file: Path): """Creates layout of kitchen counters in the environment based on layout file. @@ -64,8 +64,7 @@ class Environment: counters.append(counter) current_x += self.counter_side_length elif character == "E": - pass - + current_x += self.counter_side_length current_y += self.counter_side_length return counters @@ -87,15 +86,34 @@ class Environment: elif action.act_type == "interact": self.perform_interact(player) - def get_closest_counter(self, player: Player): - """Determines the closest counter in the environment of a player. + def get_closest_counter(self, point: np.array): + """Determines the closest counter for a given 2d-coordinate point in the env. Args: - player: The player for which to find the closest counter + point: The point in the env for which to find the closest counter - Returns: The closest counter for the given player. + Returns: The closest counter for the given point. """ - pass + counter_distances = distance_matrix( + [point], [counter.pos for counter in self.counters] + )[0] + closest_counter_idx = np.argmin(counter_distances) + return self.counters[closest_counter_idx] + + def get_facing_counter(self, player: Player): + """Determines the counter which the player is looking at. + Adds a multiple of the player facing direction onto the player position and finds the closest + counter for that point. + + Args: + player: The player for which to find the facing counter. + + Returns: + + """ + facing_point = player.pos + (player.facing_direction * player.interaction_range) + facing_counter = self.get_closest_counter(facing_point) + return facing_counter def perform_pickup(self, player: Player): """Performs the game action corresponding to picking up an item @@ -118,15 +136,45 @@ class Environment: """ pass - def perform_movement(self, player: Player, action): + def perform_movement(self, player: Player, move_vector: np.array): """Moves a player in the direction specified in the action.action. If the player collides with a counter or other player through this movement, then they are not moved. + (The extended code with the two ifs is for sliding movement at the counters, which feels a bit smoother. + This happens, when the player moves diagonally against the counters or world boundary. + This just checks if the single axis party of the movement could move the player and does so at a lower rate.) + + The movement action is a unit 2d vector. Args: player: The player to move. - action: The Action which now contains a unit-2d-vector of the movement direction + move_vector: The movement vector which is a unit-2d-vector of the movement direction """ - pass + old_pos = player.pos.copy() + + step = move_vector * player.move_dist + player.move(step) + if self.detect_collision(player): + player.move_abs(old_pos) + + old_pos = player.pos.copy() + + step_sliding = step.copy() + step_sliding[0] = 0 + player.move(step_sliding * 0.5) + player.turn(step) + + if self.detect_collision(player): + player.move_abs(old_pos) + + old_pos = player.pos.copy() + + step_sliding = step.copy() + step_sliding[1] = 0 + player.move(step_sliding * 0.5) + player.turn(step) + + if self.detect_collision(player): + player.move_abs(old_pos) def detect_collision(self, player: Player): """Detect collisions between the player and other players or counters. @@ -135,13 +183,85 @@ class Environment: player: The player for which to check collisions. Returns: True if the player is intersecting with any object in the environment. + """ + return ( + self.detect_player_collision(player) + or self.detect_collision_counters(player) + or self.detect_collision_world_bounds(player) + ) + + def detect_player_collision(self, player: Player): + """Detects collisions between the queried player and other players. + A player is modelled as a circle. Collision is detected if the distance between the players is smaller + than the sum of the radius's. + + Args: + player: The player to check collisions with other players for. + + Returns: True if the player collides with other players, False if not. """ - pass + other_players = filter(lambda p: p.name != player.name, self.players.values()) + + def collide(p): + return np.linalg.norm(player.pos - p.pos) <= (player.radius + p.radius) + + return any(map(collide, other_players)) + + def detect_collision_counters(self, player: Player): + """Checks for collisions of the queried player with each counter. + + Args: + player: The player to check collisions with counters for. + + Returns: True if the player collides with any counter, False if not. + + """ + return any( + map( + lambda counter: self.detect_collision_player_counter(player, counter), + self.counters, + ) + ) + + def detect_collision_player_counter(self, player: Player, counter: Counter): + """Checks if the player and counter collide (overlap). + A counter is modelled as a rectangle (square actually), a player is modelled as a circle. + The distance of the player position (circle center) and the counter rectangle is calculated, if it is + smaller than the player radius, a collision is detected. + TODO: Efficiency improvement by checking only nearest counters? Quadtree...? + + Args: + player: The player to check the collision for. + counter: The counter to check the collision for. + + Returns: True if player and counter overlap, False if not. + + """ + size = self.counter_side_length + cx, cy = player.pos + dx = max(np.abs(cx - counter.pos[0]) - size / 2, 0) + dy = max(np.abs(cy - counter.pos[1]) - size / 2, 0) + distance = np.linalg.norm([dx, dy]) + return distance < player.radius + + def detect_collision_world_bounds(self, player: Player): + """Checks for detections of the player and the world bounds. + + Args: + player: The player which to not let escape the world. + + Returns: True if the player touches the world bounds, False if not. + """ + collisions_lower = any((player.pos - player.radius) < 0) + collisions_upper = any( + (player.pos + player.radius) > [self.world_width, self.world_height] + ) + return collisions_lower or collisions_upper def step(self): """Performs a step of the environment. Affects time based events such as cooking or cutting things, orders - and timelimits. + and time limits. """ pass @@ -151,9 +271,7 @@ class Environment: Returns: Dict of lists of the current relevant game objects. """ - return {"players": self.players, - "counters": self.counters, - "score": self.score} + return {"players": self.players, "counters": self.counters, "score": self.score} def get_state_json(self): """Get the current state of the game environment as a json-like nested dictionary. diff --git a/overcooked_simulator/player.py b/overcooked_simulator/player.py index 42f3d26fff900cbbd059de18c68657da34fbe558..c4b11115671a69b6df4a1526ef6d9cd7731ae9d4 100644 --- a/overcooked_simulator/player.py +++ b/overcooked_simulator/player.py @@ -1,5 +1,6 @@ import numpy as np -from overcooked_simulator.overcooked_environment import Counter + +from overcooked_simulator.counters import Counter class Player: @@ -8,12 +9,15 @@ class Player: This class handles interactions with counters and objects. """ + def __init__(self, name, pos): self.name = name self.pos = np.array(pos, dtype=float) self.holding = None + self.radius = 18 self.move_dist = 5 + self.interaction_range = 50 self.facing_direction = np.array([0, 1]) def move(self, movement: np.array): @@ -23,7 +27,9 @@ class Player: Args: movement: 2D-Vector of length 1 """ - pass + self.pos += movement + if np.linalg.norm(movement) != 0: + self.turn(movement) def move_abs(self, new_pos: np.array): """Overwrites the player location by the new_pos 2d-vector. Absolute movement. @@ -32,7 +38,7 @@ class Player: Args: new_pos: 2D-Vector of the new player position. """ - pass + self.pos = new_pos def turn(self, direction: np.array): """Turns the player in the given direction. Overwrites the facing_direction by a given 2d-vector. @@ -41,7 +47,8 @@ class Player: Args: direction: 2D-Vector of the direction for the player to face. """ - pass + if np.linalg.norm(direction) != 0: + self.facing_direction = direction / np.linalg.norm(direction) def pick_action(self, counter: Counter): """Performs the pickup-action with the counter. Handles the logic of what the player is currently holding, diff --git a/overcooked_simulator/pygame_gui.py b/overcooked_simulator/pygame_gui.py new file mode 100644 index 0000000000000000000000000000000000000000..a1c655ef4bee18860ae1efd2a1ffe0b9fd61c8af --- /dev/null +++ b/overcooked_simulator/pygame_gui.py @@ -0,0 +1,205 @@ +import numpy as np +import pygame + +from overcooked_simulator.overcooked_environment import Action +from overcooked_simulator.simulation_runner import Simulator + +WHITE = (255, 255, 255) +GREY = (190, 190, 190) +BLACK = (0, 0, 0) +COUNTERCOLOR = (240, 240, 240) +LIGHTGREY = (220, 220, 220) +GREEN = (0, 255, 0) +RED = (255, 0, 0) +BLUE = (0, 0, 255) +YELLOW = (255, 255, 0) +BACKGROUND_COLOR = GREY +BACKGROUND_LINES_COLOR = (200, 200, 200) + + +class PlayerKeyset: + """Set of keyboard keys for controlling a player. + First four keys are for movement, + 5th key is for interacting with counters. + 6th key ist for picking up things or dropping them. + """ + + def __init__(self, keys: list[pygame.key]): + self.player_keys = keys + self.move_vectors = [[-1, 0], [1, 0], [0, -1], [0, 1]] + self.key_to_movement = { + key: vec for (key, vec) in zip(self.player_keys[:-2], self.move_vectors) + } + self.interact_key = self.player_keys[-2] + self.pickup_key = self.player_keys[-1] + + +class PyGameGUI: + """Visualisation of the overcooked environmnent and reading keyboard inputs using pygame.""" + + def __init__(self, simulator: Simulator): + self.FPS = 60 + self.simulator = simulator + self.counter_size = self.simulator.env.counter_side_length + self.window_width, self.window_height = ( + simulator.env.world_width, + simulator.env.world_height, + ) + + self.GET_CONTINOUS_INTERACT_AND_PICKUP = False + + keys1 = [ + pygame.K_LEFT, + pygame.K_RIGHT, + pygame.K_UP, + pygame.K_DOWN, + pygame.K_SPACE, + pygame.K_i, + ] + keys2 = [pygame.K_a, pygame.K_d, pygame.K_w, pygame.K_s, pygame.K_f, pygame.K_e] + self.player_keysets: list[PlayerKeyset] = [ + PlayerKeyset(keys1), + PlayerKeyset(keys2), + ] + + def send_action(self, action: Action): + """Sends an action to the game environment. + + Args: + action: The action to be sent. Contains the player, action type and move direction if action is a movement. + """ + self.simulator.enter_action(action) + + def handle_keys(self): + """Handles keyboard inputs. Sends action for the respective players. When a key is held down, every frame + an action is sent in this function. + """ + keys = pygame.key.get_pressed() + for player_idx, keyset in enumerate(self.player_keysets): + relevant_keys = [keys[k] for k in keyset.player_keys] + if any(relevant_keys[:-2]): + move_vec = np.zeros(2) + for idx, pressed in enumerate(relevant_keys[:-2]): + if pressed: + move_vec += keyset.move_vectors[idx] + if np.linalg.norm(move_vec) != 0: + move_vec = move_vec / np.linalg.norm(move_vec) + + action = Action(f"p{player_idx + 1}", "movement", move_vec) + self.send_action(action) + if self.GET_CONTINOUS_INTERACT_AND_PICKUP: + if relevant_keys[-2]: + action = Action(f"p{player_idx + 1}", "interact", "interact") + self.send_action(action) + if relevant_keys[-1]: + action = Action(f"p{player_idx + 1}", "pickup", "pickup") + self.send_action(action) + + def handle_interact_single_send(self, event): + """Handles key events. Here when a key is held down, only one action is sent. (This can be + switched by the GET_CONTINOUS_INTERACT_AND_PICKUP flag) + + Args: + event: Pygame event for extracting the key. + """ + for player_idx, keyset in enumerate(self.player_keysets): + if event.key == keyset.pickup_key: + action = Action(f"p{player_idx + 1}", "pickup", "pickup") + self.send_action(action) + elif event.key == keyset.interact_key: + action = Action(f"p{player_idx + 1}", "interact", "interact") + self.send_action(action) + + def draw_background(self): + """Visualizes a game background.""" + block_size = self.counter_size // 2 # Set the size of the grid block + for x in range(0, self.window_width, block_size): + for y in range(0, self.window_height, block_size): + rect = pygame.Rect(x, y, block_size, block_size) + pygame.draw.rect(self.screen, BACKGROUND_LINES_COLOR, rect, 1) + + def draw_players(self, state): + """Visualizes the players as circles with a triangle for the facing diretion. + + Args: + state: The game state returned by the environment. + """ + for player in state["players"].values(): + pos = player.pos + size = player.radius + color1 = RED if player.name == "p1" else GREEN + color2 = WHITE + + rect = pygame.Rect(pos[0] - (size / 2), pos[1] - (size / 2), size, size) + pygame.draw.circle(self.screen, color2, pos, size) + pygame.draw.rect(self.screen, color1, rect) + + facing = player.facing_direction + + pygame.draw.polygon( + self.screen, + BLUE, + ( + (pos[0] + (facing[1] * 5), pos[1] - (facing[0] * 5)), + (pos[0] - (facing[1] * 5), pos[1] + (facing[0] * 5)), + player.pos + (facing * 20), + ), + ) + + def draw_counters(self, state): + """Visualizes the counters in the environment. + + Args: + state: The game state returned by the environment. + """ + for idx, counter in enumerate(state["counters"]): + counter_rect_outline = pygame.Rect( + counter.pos[0] - (self.counter_size / 2), + counter.pos[1] - (self.counter_size / 2), + self.counter_size, + self.counter_size, + ) + + pygame.draw.rect(self.screen, COUNTERCOLOR, counter_rect_outline) + + def draw(self, state): + """Main visualization function. + + Args: + state: The game state returned by the environment. + """ + self.screen.fill(BACKGROUND_COLOR) + self.draw_background() + + self.draw_counters(state) + self.draw_players(state) + + pygame.display.flip() + + def start_pygame(self): + """Starts pygame and the gui loop. Each frame the gamestate is visualized and keyboard inputs are read.""" + pygame.init() + pygame.font.init() + + self.screen = pygame.display.set_mode((self.window_width, self.window_height)) + pygame.display.set_caption("Simple Overcooked Simulator") + self.screen.fill(BACKGROUND_COLOR) + + clock = pygame.time.Clock() + + # Game loop + running = True + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + if not self.GET_CONTINOUS_INTERACT_AND_PICKUP: + if event.type == pygame.KEYDOWN: + self.handle_interact_single_send(event) + + self.handle_keys() + clock.tick(self.FPS) + state = self.simulator.get_state() + self.draw(state) + + pygame.quit() diff --git a/overcooked_simulator/simulation_runner.py b/overcooked_simulator/simulation_runner.py index 846a56bcbb2e243c1df54ae287104c8eedacb90a..470330eb36097d8371c1c429dfb42f8c2fb2b97c 100644 --- a/overcooked_simulator/simulation_runner.py +++ b/overcooked_simulator/simulation_runner.py @@ -1,5 +1,6 @@ -from threading import Thread import time +from threading import Thread + from overcooked_simulator.overcooked_environment import Environment, Action from overcooked_simulator.player import Player @@ -17,79 +18,77 @@ class Simulator(Thread): sim.start() """ - def __init__(self, env_class: type, frequency: int): + def __init__(self, env_layout_path, frequency: int): self.finished: bool = False self.step_frequency: int = frequency self.prefered_sleeptime_ns: float = 1e9 / self.step_frequency - - self.env = env_class() + self.env: Environment = Environment(env_layout_path) super().__init__() def step(self): - """One simulation step of the environment. - """ + """One simulation step of the environment.""" self.env.step() def enter_action(self, action: Action): """Takes an action and executes it in the environment. - Args: - action (Action): The action object to be executed. - """ + Args: + action (Action): The action object to be executed. + """ self.env.perform_action(action) def get_state(self): """Get the current gamestate as python objects. - Returns: - The current state of the game. Currently as dict with lists of environment objects. - """ + Returns: + The current state of the game. Currently as dict with lists of environment objects. + """ return self.env.get_state() def get_state_json(self): """Get the current gamestate in json-like dict. - Returns: - The gamestate encoded in a json style nested dict. - """ + Returns: + The gamestate encoded in a json style nested dict. + """ return self.env.get_state_json() def register_player(self, player: Player): - print(f"Added player {player.name} to the game.") """Adds a player to the environment. - Args: - player: The player to be added. - """ + Args: + player: The player to be added. + """ + print(f"Added player {player.name} to the game.") self.env.players[player.name] = player def register_players(self, players: list[Player]): """Registers multiple players from a list - Args: - players: List of players to be added. - """ + Args: + players: List of players to be added. + """ for p in players: self.register_player(p) def run(self): - """Starts the simulator thread. Runs in a loop until stopped. - """ + """Starts the simulator thread. Runs in a loop until stopped.""" overslept_in_ns = 0 while not self.finished: - step_start = time.time_ns() self.step() step_duration = time.time_ns() - step_start - time_to_sleep_ns = self.prefered_sleeptime_ns - (step_duration + overslept_in_ns) + time_to_sleep_ns = self.prefered_sleeptime_ns - ( + step_duration + overslept_in_ns + ) sleep_start = time.time_ns() time.sleep(max(time_to_sleep_ns / 1e9, 0)) @@ -97,7 +96,6 @@ class Simulator(Thread): overslept_in_ns = sleep_function_duration - time_to_sleep_ns def stop(self): - """Stops the simulator - """ + """Stops the simulator""" print("Stopping the simulation.") self.finished = True diff --git a/setup.py b/setup.py index a267d074c93d777f787a8fd47cf4bc7c28e1f4df..c0df42492874be1aae15b203538b3deb1f9324a8 100644 --- a/setup.py +++ b/setup.py @@ -4,39 +4,41 @@ from setuptools import setup, find_packages -with open('README.md') as readme_file: +with open("README.md") as readme_file: readme = readme_file.read() -with open('CHANGELOG.md') as history_file: +with open("CHANGELOG.md") as history_file: history = history_file.read() -requirements = ["numpy", "pygame", "scipy"] +requirements = ["numpy", "pygame", "scipy", "pytest>=3"] -test_requirements = ['pytest>=3', ] +test_requirements = [ + "pytest>=3", +] setup( author="Annika Österdiekhoff, Dominik Battefeld, Fabian Heinrich, Florian Schröder", - author_email='florian.schroeder@uni-bielefeld.de', - python_requires='>=3.10', + author_email="florian.schroeder@uni-bielefeld.de", + python_requires=">=3.10", classifiers=[ - 'Development Status :: 2 - Pre-Alpha', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Natural Language :: English', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.10', + "Development Status :: 2 - Pre-Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", ], description="The real-time overcooked simulation for a cognitive cooperative system", install_requires=requirements, license="MIT license", - long_description=readme + '\n\n' + history, + long_description=readme + "\n\n" + history, include_package_data=True, - keywords=['aaambos', 'overcooked_simulator'], - name='overcooked_simulator', - packages=find_packages(include=['overcooked_simulator', 'overcooked_simulator.*']), - test_suite='tests', + keywords=["aaambos", "overcooked_simulator"], + name="overcooked_simulator", + packages=find_packages(include=["overcooked_simulator", "overcooked_simulator.*"]), + test_suite="tests", tests_require=test_requirements, - url='https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator', - version='0.1.0', + url="https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator", + version="0.1.0", zip_safe=False, ) diff --git a/tests/test_start.py b/tests/test_start.py index 0cc343f7846358e7cffd35f6b4da72a5ba60e15f..c1388940f1ecb758382c8fea1dd7395afda35017 100644 --- a/tests/test_start.py +++ b/tests/test_start.py @@ -1,12 +1,23 @@ -from overcooked_simulator.simulation_runner import Simulator -from overcooked_simulator.player import Player -from overcooked_simulator.overcooked_environment import Environment, Action -import numpy as np import time +from pathlib import Path -def test_player_registration(): +import numpy as np + +from overcooked_simulator.counters import Counter +from overcooked_simulator.overcooked_environment import Action +from overcooked_simulator.player import Player +from overcooked_simulator.simulation_runner import Simulator + +try: + with open("../overcooked_simulator/layouts/basic.layout", "r") as textfile: + pass + layouts_folder = Path("../overcooked_simulator/layouts") +except FileNotFoundError as e: + layouts_folder = Path("overcooked_simulator/layouts") - sim = Simulator(Environment, 200) + +def test_player_registration(): + sim = Simulator(layouts_folder / "basic.layout", 200) p1 = Player("player1", np.array([0, 0])) sim.register_player(p1) @@ -19,12 +30,13 @@ def test_player_registration(): assert len(sim.env.players) == 2, "Wrong number of players" p3 = Player("player2", np.array([100, 100])) - sim.register_player(p2) # same player name + sim.register_player(p2) # same player name assert len(sim.env.players) == 2, "Wrong number of players" sim.start() sim.stop() + def test_simulator_frequency(): class TestEnv: def __init__(self): @@ -33,10 +45,11 @@ def test_simulator_frequency(): def step(self): self.c += 1 - frequency = 1000 - running_time_seconds = 8 + frequency = 2000 + running_time_seconds = 3 - sim = Simulator(TestEnv, frequency) + sim = Simulator(layouts_folder / "basic.layout", frequency) + sim.env = TestEnv() # Overwrite environment with a simple counting env sim.start() time.sleep(running_time_seconds) @@ -44,6 +57,57 @@ def test_simulator_frequency(): print(sim.env.c) accepted_tolerance = 0.02 - lower = frequency * running_time_seconds * (1-accepted_tolerance) - upper = frequency * running_time_seconds * (1+accepted_tolerance) - assert sim.env.c > lower and sim.env.c < upper, "Timing error in the environment at 1000hz" + lower = frequency * running_time_seconds * (1 - accepted_tolerance) + upper = frequency * running_time_seconds * (1 + accepted_tolerance) + assert lower < sim.env.c < upper, "Timing error in the environment at 1000hz" + + +def test_movement(): + sim = Simulator(layouts_folder / "empty.layout", 200) + player_name = "p1" + start_pos = np.array([50, 50]) + player1 = Player(player_name, start_pos) + sim.register_player(player1) + move_direction = np.array([1, 0]) + move_action = Action(player_name, "movement", move_direction) + do_moves_number = 6 + for i in range(do_moves_number): + sim.enter_action(move_action) + + expected = start_pos + do_moves_number * (move_direction * player1.move_dist) + + assert ( + np.linalg.norm(expected - sim.env.players[player_name].pos) == 0 + ), "Should be here?" + + +def test_collision_detection(): + sim = Simulator(layouts_folder / "empty.layout", 200) + counter_pos = np.array([50, 100]) + counter = Counter(counter_pos) + sim.env.counters = [counter] + player1 = Player("p1", np.array([50, 50])) + player2 = Player("p2", np.array([50, 200])) + sim.register_player(player1) + sim.register_player(player2) # same player name + + sim.start() + assert not sim.env.detect_collision_counters(player1), "Should not collide" + assert not sim.env.detect_player_collision(player1), "Should not collide yet." + + assert not sim.env.detect_collision(player1), "Does not collide yet." + + player1.move_abs(counter_pos) + assert sim.env.detect_collision_counters( + player1 + ), "Player and counter at same pos. Not detected." + player2.move_abs(counter_pos) + assert sim.env.detect_player_collision( + player1 + ), "Players at same pos. Not detected." + + player1.move_abs(np.array([0, 0])) + assert sim.env.detect_collision_world_bounds( + player1 + ), "Player collides with world bounds." + sim.stop()