        state_dict = copy.deepcopy(state_dict)
        state_dict["players"] = [PlayerState.from_dict(p) for p in state_dict["players"]]
        object_list = [SoupState.from_dict(o) for o in state_dict["objects"]]
        state_dict["objects"] = { ob.position : ob for ob in object_list }
        return OvercookedState(**state_dict)

class ReducedOvercookedState(object):
    """A reduced state in OvercookedGridworld. Used to specify only properties RL agents care about."""
    def __init__(self, full_overcooked_state):
        """
        players (list(PlayerState)): Currently active PlayerStates (index corresponds to number)
        objects (dict({tuple:list(ObjectState)})):  Dictionary mapping positions (x, y) to ObjectStates.
            NOTE: Does NOT include objects held by players (they are in the PlayerState objects).
        """
        self.players = full_overcooked_state.players
        self.objects = full_overcooked_state.objects

    def __eq__(self, other):
        return isinstance(other, ReducedOvercookedState) and \
            self.players == other.players and \
            set(self.objects.items()) == set(other.objects.items())

    def __hash__(self):
        objects = {k: v for k, v in sorted(self.objects.items(), key=lambda item: item[1])}
        return hash(
             (self.players, tuple(objects))
        )

    def __str__(self):
        objects = {k: v for k, v in sorted(self.objects.items(), key=lambda item: item[1])}
        return 'Players:, Objects: {}'.format(
            str(self.players), str(list(objects)))


BASE_REW_SHAPING_PARAMS = {
    "PLACEMENT_IN_POT_REW": 3,
    "DISH_PICKUP_REWARD": 3,
    "SOUP_PICKUP_REWARD": 5,
    "DISH_DISP_DISTANCE_REW": 0,
    "POT_DISTANCE_REW": 0,
    "SOUP_DISTANCE_REW": 0,
    "DROP_REW": -1
}

EVENT_TYPES = [
    # Tomato events
    'tomato_pickup',
    'useful_tomato_pickup',
    'tomato_drop',
    'useful_tomato_drop',
    'potting_tomato',

    # Onion events
    'onion_pickup',
    'useful_onion_pickup',
    'onion_drop',
    'useful_onion_drop',
    'potting_onion',

    # Dish events
    'dish_pickup',
    'useful_dish_pickup',
    'dish_drop',
    'useful_dish_drop',

    # Soup events
   'soup_pickup',
   'soup_delivery',
   'soup_drop',

    # Potting events
    'optimal_onion_potting',
    'optimal_tomato_potting',
    'viable_onion_potting',
    'viable_tomato_potting',
    'catastrophic_onion_potting',
    'catastrophic_tomato_potting',
    'useless_onion_potting',
    'useless_tomato_potting'
]

POTENTIAL_CONSTANTS = {
    'default' : {
       'max_delivery_steps' : 10,
       'max_pickup_steps' : 10,
        'pot_onion_steps' : 10,
        'pot_tomato_steps' : 10
    },
   'mdp_test_tomato' : {
       'max_delivery_steps' : 4,
       'max_pickup_steps' : 4,
        'pot_onion_steps' : 5,
        'pot_tomato_steps' : 6
    }
}

# WARNING: Behavior with multiple sparse rewards active at once is undefined.
#          Some of the sparse reward settings below have side-effects that must be considered.
DEFAULT_SPARSE_REWARD_OPTS = {
    'deliver_soup': 20,
    'add_onion_to_pot': 0,
    'pickup_onion': 0,
    'add_soup_to_plate': 0
}

class OvercookedGridworld(object):
    """
    An MDP grid world based off of the Overcooked game.
    TODO: clean the organization of this class further.
    """


    #########################
    # INSTANTIATION METHODS #
    #########################

    def __init__(self, terrain, start_player_positions, start_bonus_orders=[], sparse_reward_opts=DEFAULT_SPARSE_REWARD_OPTS, rew_shaping_params=None, layout_name="unnamed_layout", start_all_orders=[], num_items_for_soup=3, order_bonus=2, start_state=None, **kwargs):
        """
        terrain: a matrix of strings that encode the MDP layout
        layout_name: string identifier of the layout
        start_player_positions: tuple of positions for both players' starting positions
        start_bonus_orders: List of recipes dicts that are worth a bonus 
        rew_shaping_params: reward given for completion of specific subgoals
        all_orders: List of all available order dicts the players can make, defaults to all possible recipes if empy list provided
        num_items_for_soup: Maximum number of ingredients that can be placed in a soup
        order_bonus: Multiplicative factor for serving a bonus recipe
        start_state: Default start state returned by get_standard_start_state
        """
        self._configure_recipes(start_all_orders, num_items_for_soup, **kwargs)
        self.start_all_orders = [r.to_dict() for r in Recipe.ALL_RECIPES] if not start_all_orders else start_all_orders
        self.height = len(terrain)
        self.width = len(terrain[0])
        self.shape = (self.width, self.height)
        self.terrain_mtx = terrain
        self.terrain_pos_dict = self._get_terrain_type_pos_dict()
        self.start_player_positions = start_player_positions
        self.num_players = len(start_player_positions)
        self.start_bonus_orders = start_bonus_orders
        self.reward_shaping_params = BASE_REW_SHAPING_PARAMS if rew_shaping_params is None else rew_shaping_params
        self.layout_name = layout_name
        self.order_bonus = order_bonus
        self.start_state = start_state
        self._opt_recipe_discount_cache = {}
        self._opt_recipe_cache = {}
        self._prev_potential_params = {}
        self.sparse_reward_opts = sparse_reward_opts


    @staticmethod
    def from_layout_name(layout_name, **params_to_overwrite):
        """
        Generates a OvercookedGridworld instance from a layout file.

        One can overwrite the default mdp configuration using partial_mdp_config.
        """
        params_to_overwrite = params_to_overwrite.copy()
        base_layout_params = read_layout_dict(layout_name)

        grid = base_layout_params['grid']
        del base_layout_params['grid']
        base_layout_params['layout_name'] = layout_name
        if'start_state' in base_layout_params:
            base_layout_params['start_state'] = OvercookedState.from_dict(base_layout_params['start_state'])

        # Clean grid
        grid = [layout_row.strip() for layout_row in grid.split("\n")]
        return OvercookedGridworld.from_grid(grid, base_layout_params, params_to_overwrite)

    @staticmethod
    def from_grid(layout_grid, base_layout_params={}, params_to_overwrite={}, debug=False):
        """
        Returns instance of Overcooked