diff --git a/overcooked_simulator/layouts/basic.layout b/overcooked_simulator/layouts/basic.layout new file mode 100644 index 0000000000000000000000000000000000000000..65a375bb3b13216a9737eaf9be24f8926cbb5a33 --- /dev/null +++ b/overcooked_simulator/layouts/basic.layout @@ -0,0 +1,11 @@ +EEEEEEEEEEE +ECCCCCCCCCE +ECEEEEEEECE +ECEEEEEEECE +ECEEEEEEECE +ECEEEEEEECE +ECEEEEEEECE +ECEEEEEEECE +ECEEEEEEECE +ECCCCCCCCCE +EEEEEEEEEEE diff --git a/overcooked_simulator/main.py b/overcooked_simulator/main.py new file mode 100644 index 0000000000000000000000000000000000000000..47897d248160f83aef69f958c1da01b2623c1725 --- /dev/null +++ b/overcooked_simulator/main.py @@ -0,0 +1,21 @@ +from player import Player +import sys +from overcooked_simulator.simulation_runner import Simulator +from overcooked_simulator.overcooked_environment import Environment + +def main(): + simulator = Simulator(Environment, 300) + + simulator.register_player(Player("p1", [100, 200])) + simulator.register_player(Player("p2", [200, 100])) + + simulator.start() + + print(simulator.get_state()) + + simulator.stop() + sys.exit() + + +if __name__ == "__main__": + main() diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py new file mode 100644 index 0000000000000000000000000000000000000000..71060740332ed8a2c041cfac8a6dd841c1edeb64 --- /dev/null +++ b/overcooked_simulator/overcooked_environment.py @@ -0,0 +1,164 @@ +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})" + + +class Action: + """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" + 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 + + def create_counters(self, layout_file: Path): + """Creates layout of kitchen counters in the environment based on layout file. + Counters are arranged in a fixed size grid starting at [0,0]. The center of the first counter is at + [counter_size/2, counter_size/2], counters are directly next to each other (of no empty space is specified + in layout). + + Args: + layout_file: Path to the layout file. + """ + current_y = self.counter_side_length / 2 + counters = [] + + with open(layout_file, "r") as layout_file: + lines = layout_file.readlines() + for line in lines: + line = line.replace("\n", "").replace(" ", "") # remove newline char + current_x = self.counter_side_length / 2 + for character in line: + character = character.capitalize() + if character == "C": + counter = Counter(np.array([current_x, current_y])) + counters.append(counter) + current_x += self.counter_side_length + elif character == "E": + pass + + current_y += self.counter_side_length + return counters + + def perform_action(self, action: Action): + """Performs an action of a player in the environment. Maps different types of action inputs to the + correct execution of the players. + Possible action types are movement, pickup and interact actions. + + Args: + action: The action to be performed + """ + assert action.player in self.players.keys(), "Unknown player." + + player = self.players[action.player] + if action.act_type == "movement": + self.perform_movement(player, action.action) + elif action.act_type == "pickup": + self.perform_pickup(player) + 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. + + Args: + player: The player for which to find the closest counter + + Returns: The closest counter for the given player. + """ + pass + + def perform_pickup(self, player: Player): + """Performs the game action corresponding to picking up an item + + Args: + player: The player which performs the pickup action. + + Returns: TODO? + + """ + pass + + def perform_interact(self, player: Player): + """Performs the game action corresponding to interacting with a counter or other object. + + Args: + player: The player which performs the interaction. + + Returns: TODO? + """ + pass + + def perform_movement(self, player: Player, action): + """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. + + Args: + player: The player to move. + action: The Action which now contains a unit-2d-vector of the movement direction + """ + pass + + def detect_collision(self, player: Player): + """Detect collisions between the player and other players or counters. + + Args: + player: The player for which to check collisions. + + Returns: True if the player is intersecting with any object in the environment. + + """ + pass + + def step(self): + """Performs a step of the environment. Affects time based events such as cooking or cutting things, orders + and timelimits. + """ + pass + + def get_state(self): + """Get the current state of the game environment. The state here is accessible by the current python objects. + + Returns: Dict of lists of the current relevant game objects. + + """ + 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. + + Returns: Json-like string of the current game state. + + """ + pass diff --git a/overcooked_simulator/player.py b/overcooked_simulator/player.py new file mode 100644 index 0000000000000000000000000000000000000000..42f3d26fff900cbbd059de18c68657da34fbe558 --- /dev/null +++ b/overcooked_simulator/player.py @@ -0,0 +1,66 @@ +import numpy as np +from overcooked_simulator.overcooked_environment import Counter + + +class Player: + """Class representing a player in the game environment. A player consists of a name, their position and what + the player is currently holding in the hands. + 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.move_dist = 5 + self.facing_direction = np.array([0, 1]) + + def move(self, movement: np.array): + """Moves the player position by the given movement vector. + A unit direction vector multiplied by move_dist is added to the player position. + + Args: + movement: 2D-Vector of length 1 + """ + pass + + def move_abs(self, new_pos: np.array): + """Overwrites the player location by the new_pos 2d-vector. Absolute movement. + Mostly needed for resetting the player after a collision. + + Args: + new_pos: 2D-Vector of the new player position. + """ + pass + + def turn(self, direction: np.array): + """Turns the player in the given direction. Overwrites the facing_direction by a given 2d-vector. + facing_direction is normalized to length 1. + + Args: + direction: 2D-Vector of the direction for the player to face. + """ + pass + + def pick_action(self, counter: Counter): + """Performs the pickup-action with the counter. Handles the logic of what the player is currently holding, + what is currently on the counter and what can be picked up or combined in hand. + + Args: + counter: The counter to pick things up from or put things down. + """ + pass + + def interact(self, counter: Counter): + """Performs the interact-action with the counter. Handles logic of starting processes + like for e.g. cutting onions. TODO holding the button vs pressing once? + + Args: + counter: The counter to interact with. + """ + + pass + + def __repr__(self): + return f"Player(name:{self.name},pos:{str(self.pos)},holds:{self.holding})" diff --git a/overcooked_simulator/simulation_runner.py b/overcooked_simulator/simulation_runner.py new file mode 100644 index 0000000000000000000000000000000000000000..846a56bcbb2e243c1df54ae287104c8eedacb90a --- /dev/null +++ b/overcooked_simulator/simulation_runner.py @@ -0,0 +1,103 @@ +from threading import Thread +import time +from overcooked_simulator.overcooked_environment import Environment, Action +from overcooked_simulator.player import Player + + +class Simulator(Thread): + """Simulator main class which runs manages the environment and player inputs and gamestate outputs. + + Main Simulator class which runs the game environment. Players can be registered in the game. + The simulator is run as its own thread. + + Typical usage example: + + sim = Simulator() + sim.register_player(Player("p1", [x,y])) + sim.start() + """ + + def __init__(self, env_class: type, frequency: int): + self.finished: bool = False + + self.step_frequency: int = frequency + self.prefered_sleeptime_ns: float = 1e9 / self.step_frequency + + self.env = env_class() + + super().__init__() + + def step(self): + """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. + """ + 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. + """ + + 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. + """ + + 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. + """ + + 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. + """ + + for p in players: + self.register_player(p) + + def run(self): + """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) + + sleep_start = time.time_ns() + time.sleep(max(time_to_sleep_ns / 1e9, 0)) + sleep_function_duration = time.time_ns() - sleep_start + overslept_in_ns = sleep_function_duration - time_to_sleep_ns + + def stop(self): + """Stops the simulator + """ + print("Stopping the simulation.") + self.finished = True diff --git a/setup.py b/setup.py index 06e9d0e8050d626df2c660bf9f1a1f6f955426e3..a267d074c93d777f787a8fd47cf4bc7c28e1f4df 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ with open('README.md') as readme_file: with open('CHANGELOG.md') as history_file: history = history_file.read() -requirements = [] +requirements = ["numpy", "pygame", "scipy"] test_requirements = ['pytest>=3', ] diff --git a/tests/test_start.py b/tests/test_start.py index fbd6dd5c68c7b9d29a4cb13b0849128c2dd25ce2..0cc343f7846358e7cffd35f6b4da72a5ba60e15f 100644 --- a/tests/test_start.py +++ b/tests/test_start.py @@ -1,4 +1,49 @@ +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 -def test_dummy(): - assert "overcooked".startswith("over"), "overcooked does not start with 'over'" +def test_player_registration(): + sim = Simulator(Environment, 200) + p1 = Player("player1", np.array([0, 0])) + sim.register_player(p1) + + assert len(sim.env.players) != 0, "Wrong number of players" + assert len(sim.env.players) == 1, "Wrong number of players" + + p2 = Player("player2", np.array([100, 100])) + sim.register_player(p2) + + assert len(sim.env.players) == 2, "Wrong number of players" + + p3 = Player("player2", np.array([100, 100])) + 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): + self.c = 0 + + def step(self): + self.c += 1 + + frequency = 1000 + running_time_seconds = 8 + + sim = Simulator(TestEnv, frequency) + + sim.start() + time.sleep(running_time_seconds) + sim.stop() + + 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"