diff --git a/overcooked_simulator/counters.py b/overcooked_simulator/counters.py index 41bd67eeb5c10e23ea15538dd90fe9ca1afce515..d62109ebaf861a8daf05e1e10740d80cfe5dc441 100644 --- a/overcooked_simulator/counters.py +++ b/overcooked_simulator/counters.py @@ -1,7 +1,9 @@ from __future__ import annotations import logging +import uuid from collections import deque +from collections.abc import Iterable from datetime import datetime, timedelta from typing import TYPE_CHECKING, Optional, Callable @@ -23,11 +25,19 @@ from overcooked_simulator.game_items import ( log = logging.getLogger(__name__) +COUNTER_CATEGORY = "Counter" + class Counter: """Simple class for a counter at a specified position (center of counter). Can hold things on top.""" - def __init__(self, pos: npt.NDArray[float], occupied_by: Optional[Item] = None): + def __init__( + self, + pos: npt.NDArray[float], + occupied_by: Optional[Item] = None, + uid: hex = None, + ): + self.uuid = uuid.uuid4().hex if uid is None else None self.pos: npt.NDArray[float] = pos self.occupied_by: Optional[Item] = occupied_by @@ -94,6 +104,21 @@ class Counter: f"{self.__class__.__name__}(pos={self.pos},occupied_by={self.occupied_by})" ) + def to_dict(self) -> dict: + return { + "id": self.uuid, + "category": COUNTER_CATEGORY, + "type": self.__class__.__name__, + "pos": self.pos.tolist(), + "occupied_by": None + if self.occupied_by is None + else ( + [o.to_dict() for o in self.occupied_by] + if isinstance(self.occupied_by, Iterable) + else self.occupied_by.to_dict() + ), + } + class CuttingBoard(Counter): def __init__(self, pos: np.ndarray, transitions: dict): @@ -137,6 +162,11 @@ class CuttingBoard(Counter): """Handles player interaction, stopping to hold key down.""" self.pause_progress() + def to_dict(self) -> dict: + d = super().to_dict() + d.update((("progressing", self.progressing),)) + return d + class ServingWindow(Counter): def __init__( @@ -204,6 +234,11 @@ class Dispenser(Counter): } return Item(**kwargs) + def to_dict(self) -> dict: + d = super().to_dict() + d.update((("type", self.__repr__()),)) + return d + class PlateDispenser(Counter): def __init__( @@ -360,7 +395,6 @@ class Sink(Counter): ) if self.occupied_by[-1].progress_percentage == 1.0: self.occupied_by[-1].reset() - print(self.transitions[self.occupied_by[-1].name]["result"]) self.occupied_by[-1].name = self.transitions[self.occupied_by[-1].name][ "result" ] @@ -397,6 +431,11 @@ class Sink(Counter): def set_addon(self, sink_addon): self.sink_addon = sink_addon + def to_dict(self) -> dict: + d = super().to_dict() + d.update((("progressing", self.progressing),)) + return d + class SinkAddon(Counter): def __init__(self, pos, occupied_by=None): diff --git a/overcooked_simulator/game_items.py b/overcooked_simulator/game_items.py index 4592e68bbe4057719b3618fbf7b34513fb4d232b..ac244df9660d113248437c27d5911a12b880051e 100644 --- a/overcooked_simulator/game_items.py +++ b/overcooked_simulator/game_items.py @@ -4,11 +4,15 @@ import collections import dataclasses import datetime import logging +import uuid from enum import Enum from typing import Optional log = logging.getLogger(__name__) +ITEM_CATEGORY = "Item" +COOKING_EQUIPMENT_ITEM_CATEGORY = "ItemCookingEquipment" + class ItemType(Enum): Ingredient = "Ingredient" @@ -58,11 +62,16 @@ class ItemInfo: class Item: """Base class for game items which can be held by a player.""" - def __init__(self, name: str, item_info: ItemInfo, *args, **kwargs): + item_category = ITEM_CATEGORY + + def __init__( + self, name: str, item_info: ItemInfo, uid: str = None, *args, **kwargs + ): self.name = self.__class__.__name__ if name is None else name self.item_info = item_info self.progress_equipment = None self.progress_percentage = 0.0 + self.uuid = uuid.uuid4().hex if uid is None else uid def __repr__(self): if self.progress_equipment is None: @@ -100,13 +109,24 @@ class Item: self.progress_equipment = None self.progress_percentage = 0.0 + def to_dict(self) -> dict: + return { + "id": self.uuid, + "category": self.item_category, + "type": self.name, + "progress_percentage": self.progress_percentage, + } + class CookingEquipment(Item): + item_category = "Cooking Equipment" + def __init__(self, transitions: dict, *args, **kwargs): super().__init__(*args, **kwargs) self.transitions = transitions self.active_transition: Optional[dict] = None + # TODO change content ready just to str (name of the item)? self.content_ready: Item | None = None self.content_list: list[Item] = [] @@ -129,7 +149,6 @@ class CookingEquipment(Item): ingredients = collections.Counter( item.name for item in self.content_list + other ) - print(ingredients) return any( ingredients <= recipe["recipe"] for recipe in self.transitions.values() ) @@ -156,7 +175,6 @@ class CookingEquipment(Item): "seconds": transition["seconds"], "result": Item(name=result, item_info=transition["info"]), } - print(f"{self.name} {self.active_transition}, {self.content_list}") break else: self.content_ready = None @@ -204,6 +222,21 @@ class CookingEquipment(Item): return self.content_list[0] return None + def to_dict(self) -> dict: + d = super().to_dict() + d.update( + ( + ("content_list", [c.to_dict() for c in self.content_list]), + ( + "content_ready", + self.content_ready.to_dict() + if self.content_ready is not None + else None, + ), + ) + ) + return d + class Plate(CookingEquipment): def __init__(self, transitions, clean, *args, **kwargs): diff --git a/overcooked_simulator/game_server.py b/overcooked_simulator/game_server.py index 30ae33140122dd66e90bfede32a312597ea1f720..92aaad5af0a9703805d431577dac8069b8425b72 100644 --- a/overcooked_simulator/game_server.py +++ b/overcooked_simulator/game_server.py @@ -149,7 +149,7 @@ class EnvironmentHandler: # TODO normal json state return self.envs[ self.player_data[player_hash].env_id - ].environment.get_state_simple_json() + ].environment.get_json_state() def pause_env(self, manager_id: str, env_id: str, reason: str): if ( @@ -195,6 +195,9 @@ class EnvironmentHandler: def set_player_disconnected(self, client_id: str) -> bool: if client_id in self.client_ids_to_player_hashes: + log.warning( + f"Player {self.player_data[self.client_ids_to_player_hashes[client_id]].player_id} in env {self.player_data[self.client_ids_to_player_hashes[client_id]].env_id} disconnected" + ) self.player_data[ self.client_ids_to_player_hashes[client_id] ].connected = False diff --git a/overcooked_simulator/gui_2d_vis/drawing.py b/overcooked_simulator/gui_2d_vis/drawing.py index 26b19a85f382e7d4b34cc1ef5e7f692b08863cb7..00f5990c0bce8124ef56b56e61803b358febd6b5 100644 --- a/overcooked_simulator/gui_2d_vis/drawing.py +++ b/overcooked_simulator/gui_2d_vis/drawing.py @@ -1,5 +1,6 @@ import colorsys import math +from datetime import datetime, timedelta from pathlib import Path import numpy as np @@ -9,7 +10,6 @@ from scipy.spatial import KDTree from overcooked_simulator import ROOT_DIR from overcooked_simulator.gui_2d_vis.game_colors import colors -from overcooked_simulator.order import Order def create_polygon(n, length): @@ -139,7 +139,7 @@ class Visualizer: for p_idx, player_dict in enumerate(state_dict["players"]): pos = np.array(player_dict["pos"]) * grid_size - facing = np.array(player_dict["facing"]) + facing = np.array(player_dict["facing_direction"]) if USE_PLAYER_COOK_SPRITES: img_path = self.config["Cook"]["parts"][0]["path"] @@ -193,27 +193,26 @@ class Visualizer: if player_dict["holding"] is not None: holding_item_pos = pos + (20 * facing) - - self.draw_thing( - holding_item_pos, - self.config[player_dict["holding"]]["parts"], + self.draw_item( + pos=holding_item_pos, + grid_size=grid_size, + item=player_dict["holding"], + screen=screen, ) - - # TODO MAKE THIS WORK - # if player.current_nearest_counter: - # counter: Counter = player.current_nearest_counter - # pos = counter.pos * self.grid_size - # pygame.draw.rect( - # self.game_screen, - # colors[self.player_colors[p_idx]], - # rect=pygame.Rect( - # pos[0] - (self.grid_size // 2), - # pos[1] - (self.grid_size // 2), - # self.grid_size, - # self.grid_size, - # ), - # width=2, - # ) + + if player_dict["current_nearest_counter_pos"]: + pos = player_dict["current_nearest_counter_pos"] + pygame.draw.rect( + screen, + colors[self.player_colors[p_idx]], + rect=pygame.Rect( + pos[0] * grid_size - (grid_size // 2), + pos[1] * grid_size - (grid_size // 2), + grid_size, + grid_size, + ), + width=2, + ) def draw_thing( self, @@ -267,18 +266,17 @@ class Visualizer: pygame.draw.circle( screen, color, - pos + (np.array(part["center_offset"]) * grid_size), + np.array(pos) + + (np.array(part["center_offset"]) * grid_size), radius, ) else: pygame.draw.circle(screen, color, pos, radius) - # TODO MAKE THIS WORK def draw_item( self, pos: npt.NDArray[float], grid_size, - config, item, scale: float = 1.0, plate=False, @@ -297,40 +295,47 @@ class Visualizer: """ if not isinstance(item, list): - if item.name in config: - item_key = item.name - if "Soup" in item.name and plate: + if item["type"] in self.config: + item_key = item["type"] + if "Soup" in item_key and plate: item_key += "Plate" self.draw_thing( - pos, - config[item_key]["parts"], + pos=pos, + parts=self.config[item_key]["parts"], scale=scale, screen=screen, + grid_size=grid_size, ) # - if isinstance(item, (Item, Plate)) and item.progress_percentage > 0.0: - self.draw_progress_bar(screen, pos, item.progress_percentage) + if "progress_percentage" in item and item["progress_percentage"] > 0.0: + self.draw_progress_bar( + screen, pos, item["progress_percentage"], grid_size=grid_size + ) - if isinstance(item, CookingEquipment) and item.content_list: - if item.content_ready and item.content_ready.name in config: - self.draw_thing( - pos, - config[item.content_ready.name]["parts"], + if ( + "content_ready" in item + and item["content_ready"] + and item["content_ready"]["type"] in self.config + ): + self.draw_thing( + pos=pos, + parts=self.config[item["content_ready"]["type"]]["parts"], + screen=screen, + grid_size=grid_size, + ) + elif "content_list" in item and item["content_list"]: + triangle_offsets = create_polygon(len(item["content_list"]), length=10) + scale = 1 if len(item["content_list"]) == 1 else 0.6 + for idx, o in enumerate(item["content_list"]): + self.draw_item( + pos=np.array(pos) + triangle_offsets[idx], + item=o, + scale=scale, + plate="Plate" in item["type"], screen=screen, + grid_size=grid_size, ) - else: - triangle_offsets = create_polygon(len(item.content_list), length=10) - scale = 1 if len(item.content_list) == 1 else 0.6 - for idx, o in enumerate(item.content_list): - self.draw_item( - pos + triangle_offsets[idx], - o, - scale=scale, - plate=isinstance(item, Plate), - screen=screen, - ) - # TODO MAKE THIS WORK def draw_progress_bar( self, screen: pygame.Surface, @@ -364,27 +369,31 @@ class Visualizer: self.draw_thing(screen, pos, grid_size, self.config[counter_type]["parts"]) else: self.draw_thing( - screen, - pos, - self.config[counter_type]["parts"], + screen=screen, + pos=pos, + parts=self.config[counter_type]["parts"], + grid_size=grid_size, ) occupied_by = counter_dict["occupied_by"] if occupied_by is not None: # Multiple plates on plate return: - # if isinstance(occupied_by, (list, deque)): - # with self.simulator.env.lock: - # - # for i, o in enumerate(occupied_by): - # self.draw_item(np.abs([pos[0], pos[1] - (i * 3)]), o) - # # All other items: - # else: - self.draw_thing( - screen, - pos, - grid_size, - self.config[occupied_by]["parts"], - ) + if isinstance(occupied_by, list): + for i, o in enumerate(occupied_by): + self.draw_item( + screen=screen, + pos=np.abs([pos[0], pos[1] - (i * 3)]), + grid_size=grid_size, + item=o, + ) + # All other items: + else: + self.draw_item( + pos=pos, + grid_size=grid_size, + item=occupied_by, + screen=screen, + ) def draw_counters( self, screen: pygame, state, grid_size, SHOW_COUNTER_CENTERS=False @@ -398,13 +407,11 @@ class Visualizer: if SHOW_COUNTER_CENTERS: pygame.draw.circle(screen, colors["green1"], counter.pos, 3) - # TODO MAKE THIS WORK def draw_orders( self, screen, state, grid_size, width, height, screen_margin, config ): orders_width = width - 100 orders_height = screen_margin - order_screen = pygame.Surface( (orders_width, orders_height), ) @@ -414,9 +421,8 @@ class Visualizer: order_rects_start = (orders_height // 2) - (grid_size // 2) for idx, order in enumerate(state["orders"]): - order: Order order_upper_left = [ - order_rects_start + idx * self.grid_size * 1.2, + order_rects_start + idx * grid_size * 1.2, order_rects_start, ] pygame.draw.rect( @@ -425,31 +431,42 @@ class Visualizer: pygame.Rect( order_upper_left[0], order_upper_left[1], - self.grid_size, - self.grid_size, + grid_size, + grid_size, ), width=2, ) center = np.array(order_upper_left) + np.array( - [self.grid_size / 2, self.grid_size / 2] + [grid_size / 2, grid_size / 2] ) self.draw_thing( - center, - config["Plate"]["parts"], + pos=center, + parts=config["Plate"]["parts"], screen=order_screen, + grid_size=grid_size, ) self.draw_item( - center, - order.meal, + pos=center, + item={"type": order["meal"]}, plate=True, screen=order_screen, + grid_size=grid_size, ) order_done_seconds = ( - (order.start_time + order.max_duration) - state["env_time"] + ( + datetime.fromisoformat(order["start_time"]) + + timedelta(seconds=order["max_duration"]) + ) + - datetime.fromisoformat(state["env_time"]) ).total_seconds() - percentage = order_done_seconds / order.max_duration.total_seconds() - self.draw_progress_bar(center, percentage, screen=order_screen) + percentage = order_done_seconds / order["max_duration"] + self.draw_progress_bar( + pos=center, + percent=percentage, + screen=order_screen, + grid_size=grid_size, + ) orders_rect = order_screen.get_rect() orders_rect.center = [ diff --git a/overcooked_simulator/gui_2d_vis/overcooked_gui.py b/overcooked_simulator/gui_2d_vis/overcooked_gui.py index da414675aaea7fed35422a7f7953d95447f5deb8..1b4695f9fa4e98471116a5285616ee73ce78b587 100644 --- a/overcooked_simulator/gui_2d_vis/overcooked_gui.py +++ b/overcooked_simulator/gui_2d_vis/overcooked_gui.py @@ -340,7 +340,15 @@ class PyGameGUI: # self.manager.draw_ui(self.main_window) self.update_remaining_time(state["remaining_time"]) - # self.draw_orders(state) + self.vis.draw_orders( + screen=self.game_screen, + state=state, + grid_size=self.grid_size, + width=self.game_width, + height=self.game_height, + screen_margin=self.screen_margin, + config=self.visualization_config, + ) self.update_score_label(state) def set_window_size(self): @@ -416,7 +424,7 @@ class PyGameGUI: environment_config_path = ROOT_DIR / "game_content" / "environment_config.yaml" layout_path = ROOT_DIR / "game_content" / "layouts" / "basic.layout" - item_info_path = ROOT_DIR / "game_content" / "item_info.yaml" + item_info_path = ROOT_DIR / "game_content" / "item_info_debug.yaml" with open(item_info_path, "r") as file: item_info = file.read() with open(layout_path, "r") as file: @@ -585,7 +593,7 @@ class PyGameGUI: self.disconnect_websockets() case self.reset_button: self.reset_button_press() - self.disconnect_websockets() + self.start_button_press() self.manage_button_visibility() diff --git a/overcooked_simulator/order.py b/overcooked_simulator/order.py index 5d7abbf72cdbbd1a6d2fe110934b8561d342ca44..13ce34c391152499782c9439bfcd6115cbc84521 100644 --- a/overcooked_simulator/order.py +++ b/overcooked_simulator/order.py @@ -1,6 +1,7 @@ import dataclasses import logging import random +import uuid from abc import abstractmethod from collections import deque from datetime import datetime, timedelta @@ -10,6 +11,8 @@ from overcooked_simulator.game_items import Item, Plate, ItemInfo log = logging.getLogger(__name__) +ORDER_CATEGORY = "Order" + @dataclasses.dataclass class Order: @@ -21,6 +24,7 @@ class Order: Tuple[timedelta, float] | Tuple[timedelta, float, int, timedelta] ] expired_penalty: float + uuid: str = dataclasses.field(default_factory=lambda: uuid.uuid4().hex) finished_info: dict[str, Any] = dataclasses.field(default_factory=dict) _timed_penalties: list[Tuple[datetime, float]] = dataclasses.field( @@ -315,6 +319,18 @@ class OrderAndScoreManager: for order in new_orders: order.create_penalties(env_time) + def order_state(self) -> list[dict]: + return [ + { + "id": order.uuid, + "category": ORDER_CATEGORY, + "meal": order.meal.name, + "start_time": order.start_time.isoformat(), + "max_duration": order.max_duration.total_seconds(), + } + for order in self.open_orders + ] + if __name__ == "__main__": import yaml diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py index ba97fbe2c7d24f353e3eb222b0847efbdf830f75..d4b2bdee8af7e20e78ddaa41ff33e64a8aae3f4c 100644 --- a/overcooked_simulator/overcooked_environment.py +++ b/overcooked_simulator/overcooked_environment.py @@ -33,6 +33,7 @@ from overcooked_simulator.game_items import ( ) from overcooked_simulator.order import OrderAndScoreManager from overcooked_simulator.player import Player +from overcooked_simulator.state_representation import StateRepresentation from overcooked_simulator.utils import create_init_env_time log = logging.getLogger(__name__) @@ -200,7 +201,7 @@ class Environment: self.env_time: datetime.datetime = create_init_env_time() self.order_and_score.create_init_orders(self.env_time) - self.beginning_time = self.env_time + self.start_time = self.env_time self.env_time_end = self.env_time + timedelta( seconds=self.environment_config["game"]["time_limit_seconds"] ) @@ -617,6 +618,22 @@ class Environment: "remaining_time": max(self.env_time_end - self.env_time, timedelta(0)), } + def get_json_state(self, player_id: str | int = None): + state = { + "players": [p.to_dict() for p in self.players.values()], + "counters": [c.to_dict() for c in self.counters], + "score": self.order_and_score.score, + "orders": self.order_and_score.order_state(), + "ended": self.game_ended, + "env_time": self.env_time.isoformat(), + "remaining_time": max( + self.env_time_end - self.env_time, timedelta(0) + ).total_seconds(), + } + json_data = json.dumps(state) + assert StateRepresentation.model_validate_json(json_data=json_data) + return json_data + def get_state_simple_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 ed1c29a950b423c5d673683dc676e9313035b292..4283e76f43b77725b7410d279f5859456548c998 100644 --- a/overcooked_simulator/player.py +++ b/overcooked_simulator/player.py @@ -144,3 +144,18 @@ class Player: def __repr__(self): return f"Player(name:{self.name},pos:{str(self.pos)},holds:{self.holding})" + + def to_dict(self): + # TODO add color to player class for vis independent player color + return { + "id": self.name, + "pos": self.pos.tolist(), + "facing_direction": self.facing_direction.tolist(), + "holding": self.holding.to_dict() if self.holding else None, + "current_nearest_counter_pos": self.current_nearest_counter.pos.tolist() + if self.current_nearest_counter + else None, + "current_nearest_counter_id": self.current_nearest_counter.uuid + if self.current_nearest_counter + else None, + } diff --git a/overcooked_simulator/state_representation.py b/overcooked_simulator/state_representation.py new file mode 100644 index 0000000000000000000000000000000000000000..e8ad5d17a79b4db29c2977934f0127c7a09c83f9 --- /dev/null +++ b/overcooked_simulator/state_representation.py @@ -0,0 +1,68 @@ +from datetime import datetime + +from pydantic import BaseModel +from typing_extensions import Literal, TypedDict + + +class OrderState(TypedDict): + id: str + category: Literal["Order"] + meal: str + start_time: datetime # isoformat str + max_duration: float + + +class ItemState(TypedDict): + id: str + category: Literal["Item"] | Literal["ItemCookingEquipment"] + type: str + progress_percentage: float | int + # add ItemType Meal ? + + +class CookingEquipmentState(TypedDict): + content_list: list[ItemState] + content_ready: None | ItemState + + +class CounterState(TypedDict): + id: str + category: Literal["Counter"] + type: str + pos: list[float] + occupied_by: None | list[ + ItemState | CookingEquipmentState + ] | ItemState | CookingEquipmentState + # list[ItemState] -> type in ["Sink", "PlateDispenser"] + + +class CuttingBoardAndSinkState(TypedDict): + type: Literal["CuttingBoard"] | Literal["Sink"] + progressing: bool + + +class PlayerState(TypedDict): + id: str + pos: list[float] + facing_direction: list[float] + holding: ItemState | CookingEquipmentState | None + current_nearest_counter_pos: list[float] | None + current_nearest_counter_id: str | None + + +class StateRepresentation(BaseModel): + players: list[PlayerState] + counters: list[CounterState] + score: float | int + orders: list[OrderState] + ended: bool + env_time: datetime # isoformat str + remaining_time: float + + +def create_json_schema(): + return StateRepresentation.model_json_schema() + + +if __name__ == "__main__": + print(create_json_schema()) diff --git a/tests/test_start.py b/tests/test_start.py index 0f71d0e7c7feb9605cbc926b112627d7b1a20a66..3dc676af7bc5c79d6a2d4bb1b78643c50f6b1721 100644 --- a/tests/test_start.py +++ b/tests/test_start.py @@ -291,7 +291,7 @@ def test_time_limit(): assert not env.game_ended, "Game has not ended yet" passed_time_2 = timedelta( - seconds=(env.env_time_end - env.beginning_time).total_seconds() + seconds=(env.env_time_end - env.start_time).total_seconds() ) env.step(passed_time_2)