Skip to content
Snippets Groups Projects
Commit ef9fbc7a authored by Florian Schröder's avatar Florian Schröder
Browse files

Merge remote-tracking branch 'origin/main' into...

Merge remote-tracking branch 'origin/main' into 66-replace-transitions-values-from-dict-just-to-the-iteminfo-because-it-already-stores-all-the-info

# Conflicts:
#	overcooked_simulator/counters.py
#	overcooked_simulator/game_items.py
#	overcooked_simulator/overcooked_environment.py
#	tests/test_start.py
parents bb1339ae e005a7ad
No related branches found
No related tags found
1 merge request!32Resolve "replace transitions values from dict just to the iteminfo because it already stores all the info"
Showing
with 1634 additions and 41 deletions
......@@ -20,14 +20,30 @@ In your `repo`, `PyCharmProjects` or similar directory with the correct environm
git clone https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator.git
cd overcooked_simulator
pip install -e .
``
```
#### Run
You can use it in your Python code or run the `main.py`from the command line:
Run it via the command line (in your pyenv/conda environment):
```bash
python3 overcooked_simulator/main.py
overcooked-sim --url "localhost" --port 8000
```
_The arguments are the defaults. Therefore, they are optional._
You can also start the **Game Server** and the **PyGame GUI** individually in different terminals.
```bash
python3 overcooked_simulator/game_server.py --url "localhost" --port 8000
python3 overcooked_simulator/gui_2d_vis/overcooked_simulator.py --url "localhost" --port 8000
```
You can start also several GUIs.
You can replace the GUI with your own GUI (+ study server/matchmaking server).
### Library Installation
The correct environment needs to be active:
......
......@@ -24,12 +24,22 @@ Our overcooked simulator is designed for real time interaction but also with rei
It focuses on configurability, extensibility and appealing visualization options.
## Human Player
Start `main.py` in your python/conda environment:
Run it via the command line (in your pyenv/conda environment):
```bash
python overcooked_simulator/main.py
overcooked-sim --url "localhost" --port 8000
```
## Connect with player and receive game state
_The arguments are the defaults. Therefore, they are optional._
You can also start the **Game Server** and the **PyGame GUI** individually in different terminals.
```bash
python3 overcooked_simulator/game_server.py --url "localhost" --port 8000
python3 overcooked_simulator/gui_2d_vis/overcooked_simulator.py --url "localhost" --port 8000
## Connect with agent and receive game state
...
## Direct integration into your code.
......@@ -43,7 +53,8 @@ Initialize an environment....
# Structure of the Documentation
The API documentation follows the file and content structure in the repo.
On the left you can find the navigation panel that brings you to the implementation of
- the **counters**, including the kitchen utility objects like dispenser, stove, sink, etc.,
- the **counters**, including the kitchen utility objects like dispenser, cooking counter (stove, deep fryer, oven),
sink, etc.,
- the **game items**, the holdable ingredients, cooking equipment, composed ingredients, and meals,
- in **main**, you find an example how to start a simulation,
- the **orders**, how to sample incoming orders and their attributes,
......
import argparse
import time
from multiprocessing import Process
from overcooked_simulator.utils import (
url_and_port_arguments,
disable_websocket_logging_arguments,
)
def start_game_server(cli_args):
from overcooked_simulator.game_server import main
main(cli_args.url, cli_args.port)
def start_pygame_gui(cli_args):
from overcooked_simulator.gui_2d_vis.overcooked_gui import main
main(cli_args.url, cli_args.port)
def main(cli_args=None):
if cli_args is None:
cli_args = parser.parse_args()
game_server = None
pygame_gui = None
try:
print("Start game engine:")
game_server = Process(target=start_game_server, args=(cli_args,))
game_server.start()
time.sleep(1)
print("Start PyGame GUI:")
pygame_gui = Process(target=start_pygame_gui, args=(cli_args,))
pygame_gui.start()
while pygame_gui.is_alive() and game_server.is_alive():
time.sleep(1)
except KeyboardInterrupt:
print("Received Keyboard interrupt")
finally:
if game_server is not None and game_server.is_alive():
print("Terminate game server")
game_server.terminate()
if pygame_gui is not None and pygame_gui.is_alive():
print("Terminate pygame gui")
game_server.terminate()
time.sleep(0.1)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
prog="Overcooked Simulator",
description="Game Engine Server + PyGameGUI: Starts overcooked game engine server and a PyGame 2D Visualization window in two processes.",
epilog="For further information, see https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator/overcooked_simulator.html",
)
url_and_port_arguments(parser)
disable_websocket_logging_arguments(parser)
args = parser.parse_args()
print(args)
main(args)
......@@ -2,11 +2,12 @@
what should happen when the agent wants to pick something up from the counter. On the other side,
the `Counter.drop_off` method receives the item what should be put on the counter. Before that the
`Counter.can_drop_off` method checked if the item can be put on the counter. The progress on Counters or on objects
on the counters are handled via the Counters. They have the task to delegate the progress call via the
`progress` method, e.g., the `CuttingBoard.progress`. On which type of counter the progress method is called is currently defined in the
environment class.
on the counters are handled via the Counters. They have the task to delegate the progress call via the `progress`
method, e.g., the `CuttingBoard.progress`. On which type of counter the progress method is called is currently
defined in the environment class.
Inside the item_info.yaml, equipment needs to be defined. It includes counters that are part of the interaction/requirements for the interaction.
Inside the item_info.yaml, equipment needs to be defined. It includes counters that are part of the
interaction/requirements for the interaction.
CuttingBoard:
type: Equipment
......@@ -24,7 +25,7 @@ The defined counter classes are:
- `Dispenser`
- `PlateDispenser`
- `Trashcan`
- `Stove` (maybe abstracted in a class for all cooking machine counters (stove, deep fryer, oven))
- `CookingCounter`
- `Sink`
- `SinkAddon`
......@@ -34,7 +35,9 @@ from __future__ import annotations
import dataclasses
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, Set
......@@ -56,6 +59,8 @@ 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.
......@@ -63,13 +68,19 @@ class Counter:
The character `#` in the `layout` file represents the standard Counter.
"""
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,
):
"""Constructor setting the arguments as attributes.
Args:
pos: Position of the counter in the environment. 2-element vector.
occupied_by: The item on top of the counter.
"""
self.uuid = uuid.uuid4().hex if uid is None else None
self.pos: npt.NDArray[float] = pos
self.occupied_by: Optional[Item] = occupied_by
......@@ -138,6 +149,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):
"""Cutting ingredients on. The requirement in a new object could look like
......@@ -205,9 +231,15 @@ 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):
"""The orders and scores are updated based on completed and dropped off meals. The plate dispenser is pinged for the info about a plate outside of the kitchen.
"""The orders and scores are updated based on completed and dropped off meals. The plate dispenser is pinged for
the info about a plate outside of the kitchen.
All items in the `item_info.yml` with the type meal are considered to be servable, if they are ordered. Not
ordered meals can also be served, if a `serving_not_ordered_meals` function is set in the `environment_config.yml`.
......@@ -300,6 +332,11 @@ class Dispenser(Counter):
}
return Item(**kwargs)
def to_dict(self) -> dict:
d = super().to_dict()
d.update((("type", self.__repr__()),))
return d
@dataclasses.dataclass
class PlateConfig:
......@@ -443,20 +480,34 @@ class Trashcan(Counter):
return True
class Stove(Counter):
"""Cooking machine. Currently, the stove which can have a pot and pan on top. In the future one class for stove,
deep fryer, and oven.
class CookingCounter(Counter):
"""Cooking machine. Class for the stove, deep fryer, and oven.
The character depends on the cooking equipment on top of it:
```yaml
U: Stove with a pot
Q: Stove with a pan
O: Oven with a (pizza) peel
F: DeepFryer with a basket
```
"""
def __init__(
self,
name: str,
cooking_counter_equipments: dict[str, list[str]],
**kwargs,
):
self.name = name
self.cooking_counter_equipments = cooking_counter_equipments
super().__init__(**kwargs)
def can_drop_off(self, item: Item) -> bool:
if self.occupied_by is None:
return isinstance(item, CookingEquipment) and item.name in ["Pot", "Pan"]
return (
isinstance(item, CookingEquipment)
and item.name in self.cooking_counter_equipments[self.name]
)
else:
return self.occupied_by.can_combine(item)
......@@ -465,10 +516,19 @@ class Stove(Counter):
if (
self.occupied_by
and isinstance(self.occupied_by, CookingEquipment)
and self.occupied_by.name in self.cooking_counter_equipments[self.name]
and self.occupied_by.can_progress()
):
self.occupied_by.progress(passed_time, now)
def __repr__(self):
return f"{self.name}(pos={self.pos},occupied_by={self.occupied_by})"
def to_dict(self) -> dict:
d = super().to_dict()
d.update((("type", self.name),))
return d
class Sink(Counter):
"""The counter in which the dirty plates can be washed to clean plates.
......@@ -560,6 +620,11 @@ class Sink(Counter):
def set_addon(self, sink_addon: SinkAddon):
self.sink_addon = sink_addon
def to_dict(self) -> dict:
d = super().to_dict()
d.update((("progressing", self.progressing),))
return d
class SinkAddon(Counter):
"""The counter on which the clean plates appear after cleaning them in the `Sink`
......
......@@ -5,10 +5,10 @@ plates:
# range of seconds until the dirty plate arrives.
game:
time_limit_seconds: 180
time_limit_seconds: 300
meals:
all: false
all: true
# if all: false -> only orders for these meals are generated
# TODO: what if this list is empty?
list:
......@@ -51,10 +51,10 @@ orders:
expired_penalty_func: !!python/name:overcooked_simulator.order.simple_expired_penalty ''
expired_penalty_kwargs:
default: -5
serving_not_ordered_meals: null
serving_not_ordered_meals: !!python/name:overcooked_simulator.order.serving_not_ordered_meals_with_zero_score ''
# a func that calcs a store for not ordered but served meals. Input: meal
player_config:
radius: 0.4
move_dist: 0.15
interaction_range: 1.6
\ No newline at end of file
player_speed_units_per_seconds: 8
interaction_range: 1.6
......@@ -7,6 +7,12 @@ Sink:
Stove:
type: Equipment
DeepFryer:
type: Equipment
Oven:
type: Equipment
Pot:
type: Equipment
equipment: Stove
......@@ -15,6 +21,14 @@ Pan:
type: Equipment
equipment: Stove
Basket:
type: Equipment
equipment: DeepFryer
Peel:
type: Equipment
equipment: Oven
DirtyPlate:
type: Equipment
......@@ -41,6 +55,21 @@ Meat:
Bun:
type: Ingredient
Potato:
type: Ingredient
Fish:
type: Ingredient
Dough:
type: Ingredient
Cheese:
type: Ingredient
Sausage:
type: Ingredient
ChoppedTomato:
type: Ingredient
needs: [ Tomato ]
......@@ -59,20 +88,62 @@ ChoppedOnion:
seconds: 5.0
equipment: CuttingBoard
ChoppedMeat:
RawPatty:
type: Ingredient
needs: [ Meat ]
seconds: 4.0
equipment: CuttingBoard
RawChips:
type: Ingredient
needs: [ Potato ]
seconds: 4.0
equipment: CuttingBoard
ChoppedFish:
type: Ingredient
needs: [ Fish ]
seconds: 4.0
equipment: CuttingBoard
PizzaBase:
type: Ingredient
needs: [ Dough ]
seconds: 4.0
equipment: CuttingBoard
GratedCheese:
type: Ingredient
needs: [ Cheese ]
seconds: 4.0
equipment: CuttingBoard
ChoppedSausage:
type: Ingredient
needs: [ Sausage ]
seconds: 4.0
equipment: CuttingBoard
CookedPatty:
type: Ingredient
seconds: 5.0
needs: [ ChoppedMeat ]
needs: [ RawPatty ]
equipment: Pan
# --------------------------------------------------------------------------------
Chips:
type: Meal
seconds: 5.0
needs: [ RawChips ]
equipment: Basket
FriedFish:
type: Meal
seconds: 5.0
needs: [ ChoppedFish ]
equipment: Basket
Burger:
type: Meal
needs: [ Bun, ChoppedLettuce, ChoppedTomato, CookedPatty ]
......@@ -94,3 +165,14 @@ OnionSoup:
needs: [ ChoppedOnion, ChoppedOnion, ChoppedOnion ]
seconds: 6.0
equipment: Pot
FishAndChips:
type: Meal
needs: [ FriedFish, Chips ]
equipment: ~
Pizza:
type: Meal
needs: [ PizzaBase, ChoppedTomato, GratedCheese, ChoppedSausage ]
seconds: 7.0
equipment: Peel
......@@ -7,6 +7,12 @@ Sink:
Stove:
type: Equipment
DeepFryer:
type: Equipment
Oven:
type: Equipment
Pot:
type: Equipment
equipment: Stove
......@@ -15,13 +21,21 @@ Pan:
type: Equipment
equipment: Stove
Basket:
type: Equipment
equipment: DeepFryer
Peel:
type: Equipment
equipment: Oven
DirtyPlate:
type: Equipment
Plate:
type: Equipment
needs: [ DirtyPlate ]
seconds: 1.0
seconds: 0.1
equipment: Sink
# --------------------------------------------------------------------------------
......@@ -41,6 +55,21 @@ Meat:
Bun:
type: Ingredient
Potato:
type: Ingredient
Fish:
type: Ingredient
Dough:
type: Ingredient
Cheese:
type: Ingredient
Sausage:
type: Ingredient
ChoppedTomato:
type: Ingredient
needs: [ Tomato ]
......@@ -59,20 +88,63 @@ ChoppedOnion:
seconds: 0.1
equipment: CuttingBoard
ChoppedMeat:
RawPatty:
type: Ingredient
needs: [ Meat ]
seconds: 0.1
equipment: CuttingBoard
RawChips:
type: Ingredient
needs: [ Potato ]
seconds: 0.1
equipment: CuttingBoard
ChoppedFish:
type: Ingredient
needs: [ Fish ]
seconds: 0.1
equipment: CuttingBoard
PizzaBase:
type: Ingredient
needs: [ Dough ]
seconds: 0.1
equipment: CuttingBoard
GratedCheese:
type: Ingredient
needs: [ Cheese ]
seconds: 0.1
equipment: CuttingBoard
ChoppedSausage:
type: Ingredient
needs: [ Sausage ]
seconds: 0.1
equipment: CuttingBoard
CookedPatty:
type: Ingredient
seconds: 2.0
needs: [ ChoppedMeat ]
seconds: 0.1
needs: [ RawPatty ]
equipment: Pan
# --------------------------------------------------------------------------------
Chips:
type: Meal
seconds: 0.1
needs: [ RawChips ]
equipment: Basket
FriedFish:
type: Meal
seconds: 0.1
needs: [ ChoppedFish ]
equipment: Basket
Burger:
type: Meal
needs: [ Bun, ChoppedLettuce, ChoppedTomato, CookedPatty ]
......@@ -86,11 +158,22 @@ Salad:
TomatoSoup:
type: Meal
needs: [ ChoppedTomato, ChoppedTomato, ChoppedTomato ]
seconds: 3.0
seconds: 0.1
equipment: Pot
OnionSoup:
type: Meal
needs: [ ChoppedOnion, ChoppedOnion, ChoppedOnion ]
seconds: 3.0
seconds: 0.1
equipment: Pot
FishAndChips:
type: Meal
needs: [ FriedFish, Chips ]
equipment: ~
Pizza:
type: Meal
needs: [ PizzaBase, ChoppedTomato, GratedCheese, ChoppedSausage ]
seconds: 0.1
equipment: Peel
#QU#T###NLB#
#QU#FO#TNLB#
#__________M
#__________K
W__________I
#__A_____A_D
C__________E
C__________G
#__________#
W___________
#__A_____A__
C___________
C__________#
#__________X
#P#S+####S+#
\ No newline at end of file
#P#S+#X##S+#
\ No newline at end of file
##########
#________#
#________#
#________#
#________#
#________#
#________#
#________#
#########P
#QU#T###NLB#
#__________M
#____A_____#
W__________#
############
C__________#
C_____A____#
#__________X
#P#S+####S+#
\ No newline at end of file
......@@ -24,11 +24,15 @@ import collections
import dataclasses
import datetime
import logging
import uuid
from enum import Enum
from typing import Optional, TypedDict
log = logging.getLogger(__name__)
ITEM_CATEGORY = "Item"
COOKING_EQUIPMENT_ITEM_CATEGORY = "ItemCookingEquipment"
class ItemType(Enum):
Ingredient = "Ingredient"
......@@ -100,11 +104,16 @@ class ActiveTransitionTypedDict(TypedDict):
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:
......@@ -142,10 +151,19 @@ 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):
"""Pot, Pan, ... that can hold items. It holds the progress of the content (e.g., the soup) in itself (
progress_percentage) and not in the items in the content list."""
item_category = "Cooking Equipment"
def __init__(self, transitions: dict[str, ItemInfo], *args, **kwargs):
super().__init__(*args, **kwargs)
......@@ -153,6 +171,7 @@ class CookingEquipment(Item):
self.active_transition: Optional[ActiveTransitionTypedDict] = None
"""The info how and when to convert the content_list to a new item."""
# TODO change content ready just to str (name of the item)?
self.content_ready: Item | None = None
"""Helper attribute that can have a ready meal which is also represented via it ingredients in the
content_list. But soups or other processed meals are not covered here. For a Burger or Salad, this attribute
......@@ -179,7 +198,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())
def combine(self, other) -> Item | None:
......@@ -203,7 +221,6 @@ class CookingEquipment(Item):
"seconds": transition.seconds,
"result": Item(name=result, item_info=transition),
}
print(f"{self.name} {self.active_transition}, {self.content_list}")
break
else:
self.content_ready = None
......@@ -245,6 +262,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):
"""The plate can have to states: clean and dirty. In the clean state it can hold content/other items."""
......
This diff is collapsed.
import colorsys
import math
from datetime import datetime, timedelta
from pathlib import Path
import numpy as np
import numpy.typing as npt
import pygame
from scipy.spatial import KDTree
from overcooked_simulator import ROOT_DIR
from overcooked_simulator.gui_2d_vis.game_colors import colors
from overcooked_simulator.state_representation import (
PlayerState,
CookingEquipmentState,
ItemState,
)
USE_PLAYER_COOK_SPRITES = True
SHOW_INTERACTION_RANGE = False
SHOW_COUNTER_CENTERS = False
def create_polygon(n, length):
if n == 1:
return np.array([0, 0])
vector = np.array([length, 0])
angle = (2 * np.pi) / n
rot_matrix = np.array(
[[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]]
)
vecs = [vector]
for i in range(n - 1):
vector = np.dot(rot_matrix, vector)
vecs.append(vector)
return vecs
class Visualizer:
def __init__(self, config):
self.image_cache_dict = {}
self.player_colors = []
self.config = config
def create_player_colors(self, n) -> None:
hue_values = np.linspace(0, 1, n + 1)
colors_vec = np.array([col for col in colors.values()])
tree = KDTree(colors_vec)
color_names = list(colors.keys())
self.player_colors = []
for hue in hue_values:
rgb = colorsys.hsv_to_rgb(hue, 1, 1)
query_color = np.array([int(c * 255) for c in rgb])
_, index = tree.query(query_color, k=1)
self.player_colors.append(color_names[index])
def draw_gamescreen(
self,
screen,
state,
width,
height,
grid_size,
):
self.draw_background(
surface=screen,
width=width,
height=height,
grid_size=grid_size,
)
self.draw_counters(
screen,
state,
grid_size,
)
self.draw_players(
screen,
state,
grid_size,
)
def draw_background(self, surface, width, height, grid_size):
"""Visualizes a game background."""
block_size = grid_size // 2 # Set the size of the grid block
surface.fill(colors[self.config["Kitchen"]["ground_tiles_color"]])
for x in range(0, width, block_size):
for y in range(0, height, block_size):
rect = pygame.Rect(x, y, block_size, block_size)
pygame.draw.rect(
surface,
self.config["Kitchen"]["background_lines"],
rect,
1,
)
def draw_image(
self,
screen: pygame.Surface,
img_path: Path | str,
size: float,
pos: npt.NDArray,
rot_angle=0,
):
cache_entry = f"{img_path}"
if cache_entry in self.image_cache_dict.keys():
image = self.image_cache_dict[cache_entry]
else:
image = pygame.image.load(
ROOT_DIR / "gui_2d_vis" / img_path
).convert_alpha()
self.image_cache_dict[cache_entry] = image
image = pygame.transform.scale(image, (size, size))
if rot_angle != 0:
image = pygame.transform.rotate(image, rot_angle)
rect = image.get_rect()
rect.center = pos
screen.blit(image, rect)
def draw_players(
self,
screen: pygame.Surface,
state_dict: dict,
grid_size: float,
):
"""Visualizes the players as circles with a triangle for the facing direction.
If the player holds something in their hands, it is displayed
Args: state: The game state returned by the environment.
"""
for p_idx, player_dict in enumerate(state_dict["players"]):
player_dict: PlayerState
pos = np.array(player_dict["pos"]) * grid_size
facing = np.array(player_dict["facing_direction"])
if USE_PLAYER_COOK_SPRITES:
img_path = self.config["Cook"]["parts"][0]["path"]
rel_x, rel_y = facing
angle = -np.rad2deg(math.atan2(rel_y, rel_x)) + 90
size = self.config["Cook"]["parts"][0]["size"] * grid_size
self.draw_image(screen, img_path, size, pos, angle)
else:
size = 0.4 * grid_size
color1 = self.player_colors[p_idx]
color2 = colors["white"]
pygame.draw.circle(screen, color2, pos, size)
pygame.draw.circle(screen, colors["blue"], pos, size, width=1)
pygame.draw.circle(screen, colors[color1], pos, size // 2)
pygame.draw.polygon(
screen,
colors["blue"],
(
(
pos[0] + (facing[1] * 0.1 * grid_size),
pos[1] - (facing[0] * 0.1 * grid_size),
),
(
pos[0] - (facing[1] * 0.1 * grid_size),
pos[1] + (facing[0] * 0.1 * grid_size),
),
pos + (facing * 0.5 * grid_size),
),
)
if SHOW_INTERACTION_RANGE:
facing_point = np.array(player_dict["facing"])
pygame.draw.circle(
screen,
colors["blue"],
facing_point * grid_size,
1.6 * grid_size,
width=1,
)
pygame.draw.circle(
screen,
colors["red1"],
facing * grid_size,
4,
)
pygame.draw.circle(screen, colors["red1"], facing, 4)
if player_dict["holding"] is not None:
holding_item_pos = pos + (20 * facing)
self.draw_item(
pos=holding_item_pos,
grid_size=grid_size,
item=player_dict["holding"],
screen=screen,
)
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,
screen: pygame.Surface,
pos: npt.NDArray[float],
grid_size: float,
parts: list[dict[str]],
scale: float = 1.0,
):
"""Draws an item, based on its visual parts specified in the visualization config.
Args:
screen: the game screen to draw on.
grid_size: size of a grid cell.
pos: Where to draw the item parts.
parts: The visual parts to draw.
scale: Rescale the item by this factor.
"""
for part in parts:
part_type = part["type"]
match part_type:
case "image":
if "center_offset" in part:
d = np.array(part["center_offset"]) * grid_size
pos += d
self.draw_image(
screen,
part["path"],
part["size"] * scale * grid_size,
pos,
)
case "rect":
height = part["height"] * grid_size
width = part["width"] * grid_size
color = part["color"]
if "center_offset" in part:
dx, dy = np.array(part["center_offset"]) * grid_size
rect = pygame.Rect(pos[0] + dx, pos[1] + dy, height, width)
pygame.draw.rect(screen, color, rect)
else:
rect = pygame.Rect(
pos[0] - (height / 2),
pos[1] - (width / 2),
height,
width,
)
pygame.draw.rect(screen, color, rect)
case "circle":
radius = part["radius"] * grid_size
color = colors[part["color"]]
if "center_offset" in part:
pygame.draw.circle(
screen,
color,
np.array(pos)
+ (np.array(part["center_offset"]) * grid_size),
radius,
)
else:
pygame.draw.circle(screen, color, pos, radius)
def draw_item(
self,
pos: npt.NDArray[float] | list[float],
grid_size: float,
item: ItemState | CookingEquipmentState,
scale: float = 1.0,
plate=False,
screen=None,
):
"""Visualization of an item at the specified position. On a counter or in the hands of the player.
The visual composition of the item is read in from visualization.yaml file, where it is specified as
different parts to be drawn.
Args:
grid_size: size of a grid cell.
pos: The position of the item to draw.
item: The item do be drawn in the game.
scale: Rescale the item by this factor.
screen: the pygame screen to draw on.
plate: item is on a plate (soup are is different on a plate and pot)
"""
if not isinstance(item, list): # can we remove this check?
if item["type"] in self.config:
item_key = item["type"]
if "Soup" in item_key and plate:
item_key += "Plate"
self.draw_thing(
pos=pos,
parts=self.config[item_key]["parts"],
scale=scale,
screen=screen,
grid_size=grid_size,
)
#
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 (
"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,
)
def draw_progress_bar(
self,
screen: pygame.Surface,
pos: npt.NDArray[float],
percent: float,
grid_size: float,
):
"""Visualize progress of progressing item as a green bar under the item."""
bar_height = grid_size * 0.2
progress_width = percent * grid_size
progress_bar = pygame.Rect(
pos[0] - (grid_size / 2),
pos[1] - (grid_size / 2) + grid_size - bar_height,
progress_width,
bar_height,
)
pygame.draw.rect(screen, colors["green1"], progress_bar)
def draw_counter(
self, screen: pygame.Surface, counter_dict: dict, grid_size: float
):
"""Visualization of a counter at its position. If it is occupied by an item, it is also shown.
The visual composition of the counter is read in from visualization.yaml file, where it is specified as
different parts to be drawn.
Args: counter: The counter to visualize.
"""
pos = np.array(counter_dict["pos"]) * grid_size
counter_type = counter_dict["type"]
self.draw_thing(screen, pos, grid_size, self.config["Counter"]["parts"])
if counter_type in self.config:
self.draw_thing(screen, pos, grid_size, self.config[counter_type]["parts"])
else:
if counter_type in self.config:
parts = self.config[counter_type]["parts"]
elif counter_type.endswith("Dispenser"):
parts = self.config["Dispenser"]["parts"]
else:
raise ValueError(f"Can not draw counter type {counter_type}")
self.draw_thing(
screen=screen,
pos=pos,
parts=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):
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
):
"""Visualizes the counters in the environment.
Args: state: The game state returned by the environment.
"""
for counter in state["counters"]:
self.draw_counter(screen, counter, grid_size)
if SHOW_COUNTER_CENTERS:
pygame.draw.circle(screen, colors["green1"], counter.pos, 3)
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),
)
bg_color = colors[config["GameWindow"]["background_color"]]
pygame.draw.rect(order_screen, bg_color, order_screen.get_rect())
order_rects_start = (orders_height // 2) - (grid_size // 2)
for idx, order in enumerate(state["orders"]):
order_upper_left = [
order_rects_start + idx * grid_size * 1.2,
order_rects_start,
]
pygame.draw.rect(
order_screen,
colors["red"],
pygame.Rect(
order_upper_left[0],
order_upper_left[1],
grid_size,
grid_size,
),
width=2,
)
center = np.array(order_upper_left) + np.array(
[grid_size / 2, grid_size / 2]
)
self.draw_thing(
pos=center,
parts=config["Plate"]["parts"],
screen=order_screen,
grid_size=grid_size,
)
self.draw_item(
pos=center,
item={"type": order["meal"]},
plate=True,
screen=order_screen,
grid_size=grid_size,
)
order_done_seconds = (
(
datetime.fromisoformat(order["start_time"])
+ timedelta(seconds=order["max_duration"])
)
- datetime.fromisoformat(state["env_time"])
).total_seconds()
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 = [
screen_margin + (orders_width // 2),
orders_height // 2,
]
screen.blit(order_screen, orders_rect)
overcooked_simulator/gui_2d_vis/images/basket.png

114 KiB

overcooked_simulator/gui_2d_vis/images/bell_gold.png

24.9 KiB

overcooked_simulator/gui_2d_vis/images/bell_silver.png

25.1 KiB

overcooked_simulator/gui_2d_vis/images/cheese3.png

67 KiB

overcooked_simulator/gui_2d_vis/images/chopped_fish.png

68.4 KiB

overcooked_simulator/gui_2d_vis/images/cut_fish.png

67.4 KiB

overcooked_simulator/gui_2d_vis/images/drip2.png

47.2 KiB

0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment