Items and combination of item and dirt

This commit is contained in:
Steffen Illium
2021-08-23 09:51:35 +02:00
parent 244d4eed68
commit d5e4d44823
12 changed files with 647 additions and 445 deletions

View File

@ -1,3 +1,4 @@
import abc
import time
from pathlib import Path
from typing import List, Union, Iterable
@ -10,6 +11,7 @@ import yaml
from gym.wrappers import FrameStack
from environments.factory.base.shadow_casting import Map
from environments.factory.renderer import Renderer, RenderEntity
from environments.helpers import Constants as c, Constants
from environments import helpers as h
from environments.factory.base.objects import Slice, Agent, Tile, Action
@ -28,20 +30,7 @@ class BaseFactory(gym.Env):
@property
def observation_space(self):
if self.combin_agent_slices_in_obs and self.omit_agent_slice_in_obs:
if self.n_agents > 1:
slices = self._slices.n - (self._agents.n - 1)
else:
slices = self._slices.n - 1
elif self.combin_agent_slices_in_obs and not self.omit_agent_slice_in_obs:
slices = self._slices.n - (self._agents.n - 1)
elif not self.combin_agent_slices_in_obs and self.omit_agent_slice_in_obs:
slices = self._slices.n - self._agents.n
elif not self.combin_agent_slices_in_obs and not self.omit_agent_slice_in_obs:
slices = self._slices.n
else:
raise RuntimeError('This should not happen!')
slices = self._slices.n_observable_slices
level_shape = (self.pomdp_r * 2 + 1, self.pomdp_r * 2 + 1) if self.pomdp_r else self._level_shape
space = spaces.Box(low=0, high=1, shape=(slices, *level_shape), dtype=np.float32)
return space
@ -54,36 +43,6 @@ class BaseFactory(gym.Env):
def movement_actions(self):
return self._actions.movement_actions
@property
def additional_actions(self) -> Union[str, List[str]]:
"""
When heriting from this Base Class, you musst implement this methode!!!
:return: A list of Actions-object holding all additional actions.
:rtype: List[Action]
"""
raise NotImplementedError('Please register additional actions ')
@property
def additional_entities(self) -> Union[Entities, List[Entities]]:
"""
When heriting from this Base Class, you musst implement this methode!!!
:return: A single Entites collection or a list of such.
:rtype: Union[Entities, List[Entities]]
"""
raise NotImplementedError('Please register additional entities.')
@property
def additional_slices(self) -> Union[Slice, List[Slice]]:
"""
When heriting from this Base Class, you musst implement this methode!!!
:return: A list of Slice-objects.
:rtype: List[Slice]
"""
raise NotImplementedError('Please register additional slices.')
def __enter__(self):
return self if self.frames_to_stack == 0 else FrameStack(self, self.frames_to_stack)
@ -94,17 +53,20 @@ class BaseFactory(gym.Env):
movement_properties: MovementProperties = MovementProperties(), parse_doors=False,
combin_agent_slices_in_obs: bool = False, frames_to_stack=0, record_episodes=False,
omit_agent_slice_in_obs=False, done_at_collision=False, cast_shadows=True,
verbose=False, doors_have_area=True, **kwargs):
verbose=False, doors_have_area=True, env_seed=time.time_ns(), **kwargs):
assert frames_to_stack != 1 and frames_to_stack >= 0, "'frames_to_stack' cannot be negative or 1."
# Attribute Assignment
self._base_rng = np.random.default_rng(kwargs.get('seed', default=time.time_ns()))
self.env_seed = env_seed
self._base_rng = np.random.default_rng(self.env_seed)
self.movement_properties = movement_properties
self.level_name = level_name
self._level_shape = None
self.verbose = verbose
self._renderer = None # expensive - don't use it when not required !
self.n_agents = n_agents
self.max_steps = max_steps
self.pomdp_r = pomdp_r
self.combin_agent_slices_in_obs = combin_agent_slices_in_obs
@ -132,25 +94,37 @@ class BaseFactory(gym.Env):
# Level
level_filepath = Path(__file__).parent.parent / h.LEVELS_DIR / f'{self.level_name}.txt'
parsed_level = h.parse_level(level_filepath)
level = [Slice(c.LEVEL.name, h.one_hot_level(parsed_level), is_blocking_light=True)]
level = [Slice(c.LEVEL, h.one_hot_level(parsed_level), is_blocking_light=True)]
self._level_shape = level[0].shape
# Doors
parsed_doors = h.one_hot_level(parsed_level, c.DOOR)
if parsed_doors.any():
doors = [Slice(c.DOORS.name, parsed_doors, is_blocking_light=True)]
doors = [Slice(c.DOORS, parsed_doors, is_blocking_light=True)]
else:
doors = []
# Agents
agents = []
for i in range(self.n_agents):
agents.append(Slice(f'{c.AGENT.name}#{i}', np.zeros_like(level[0].slice, dtype=np.float32)))
state_slices.register_additional_items(level+doors+agents)
agent_names = [f'{c.AGENT.value}#{i}' for i in range(self.n_agents)]
# Additional Slices from SubDomains
if additional_slices := self.additional_slices:
state_slices.register_additional_items(additional_slices)
if self.combin_agent_slices_in_obs and self.omit_agent_slice_in_obs:
if self.n_agents == 1:
observables = [False]
else:
observables = [True] + ([False] * (self.n_agents - 1))
elif self.combin_agent_slices_in_obs and not self.omit_agent_slice_in_obs:
observables = [True] + ([False] * (self.n_agents - 1))
elif not self.combin_agent_slices_in_obs and self.omit_agent_slice_in_obs:
observables = [False] + ([True] * (self.n_agents - 1))
elif not self.combin_agent_slices_in_obs and not self.omit_agent_slice_in_obs:
observables = [True] * self.n_agents
else:
raise RuntimeError('This should not happen!')
for observable, agent_name in zip(observables, agent_names):
agents.append(Slice(agent_name, np.zeros_like(level[0].slice, dtype=np.float32), is_observable=observable))
state_slices.register_additional_items(level+doors+agents+self.additional_slices)
return state_slices
def _init_obs_cube(self) -> np.ndarray:
@ -198,18 +172,6 @@ class BaseFactory(gym.Env):
obs = self._get_observations()
return obs
def pre_step(self) -> None:
pass
def do_additional_reset(self) -> None:
pass
def do_additional_step(self) -> dict:
return {}
def post_step(self) -> dict:
return {}
def step(self, actions):
actions = [actions] if isinstance(actions, int) or np.isscalar(actions) else actions
assert isinstance(actions, Iterable), f'"actions" has to be in [{int, list}]'
@ -217,31 +179,22 @@ class BaseFactory(gym.Env):
done = False
# Pre step Hook for later use
self.pre_step()
self.hook_pre_step()
# Move this in a seperate function?
for action, agent in zip(actions, self._agents):
agent.clear_temp_sate()
action_name = self._actions[action]
if self._actions.is_moving_action(action):
valid = self._move_or_colide(agent, action_name)
elif self._actions.is_no_op(action):
action_obj = self._actions[action]
if self._actions.is_moving_action(action_obj):
valid = self._move_or_colide(agent, action_obj)
elif self._actions.is_no_op(action_obj):
valid = c.VALID.value
elif self._actions.is_door_usage(action):
# Check if agent really is standing on a door:
if self.doors_have_area:
door = self._doors.get_near_position(agent.pos)
else:
door = self._doors.by_pos(agent.pos)
if door is not None:
door.use()
valid = c.VALID.value
# When he doesn't...
else:
valid = c.NOT_VALID.value
elif self._actions.is_door_usage(action_obj):
valid = self._handle_door_interaction(agent)
else:
valid = self.do_additional_actions(agent, action)
agent.temp_action = action
valid = self.do_additional_actions(agent, action_obj)
assert valid is not None, 'This should not happen, every Action musst be detected correctly!'
agent.temp_action = action_obj
agent.temp_valid = valid
# In-between step Hook for later use
@ -275,12 +228,25 @@ class BaseFactory(gym.Env):
info.update(self._summarize_state())
# Post step Hook for later use
info.update(self.post_step())
info.update(self.hook_post_step())
obs = self._get_observations()
return obs, reward, done, info
def _handle_door_interaction(self, agent):
# Check if agent really is standing on a door:
if self.doors_have_area:
door = self._doors.get_near_position(agent.pos)
else:
door = self._doors.by_pos(agent.pos)
if door is not None:
door.use()
return c.VALID.value
# When he doesn't...
else:
return c.NOT_VALID.value
def _flush_state(self):
self._obs_cube[np.arange(len(self._slices)) != self._slices.get_idx(c.LEVEL)] = c.FREE_CELL.value
if self.parse_doors:
@ -291,7 +257,7 @@ class BaseFactory(gym.Env):
self._obs_cube[self._slices.get_idx(c.DOORS)][door.pos] = c.CLOSED_DOOR.value
for agent in self._agents:
self._obs_cube[self._slices.get_idx_by_name(agent.name)][agent.pos] = c.OCCUPIED_CELL.value
if agent.last_pos != h.NO_POS:
if agent.last_pos != c.NO_POS:
self._obs_cube[self._slices.get_idx_by_name(agent.name)][agent.last_pos] = c.FREE_CELL.value
def _get_observations(self) -> np.ndarray:
@ -318,8 +284,8 @@ class BaseFactory(gym.Env):
obs = self._obs_cube
if self.cast_shadows:
obs_block_light = [obs[idx] != c.OCCUPIED_CELL.value for idx, slice
in enumerate(self._slices) if slice.is_blocking_light]
obs_block_light = [obs[idx] != c.OCCUPIED_CELL.value for idx, obs_slice
in enumerate(self._slices) if obs_slice.is_blocking_light]
door_shadowing = False
if door := self._doors.by_pos(agent.pos):
if door.is_closed:
@ -332,6 +298,7 @@ class BaseFactory(gym.Env):
xs, ys = zip(*blocking)
else:
xs, ys = zip(*group)
# noinspection PyTypeChecker
obs_block_light[self._slices.get_idx(c.LEVEL)][xs, ys] = False
light_block_map = Map((np.prod(obs_block_light, axis=0) != True).astype(int))
@ -340,9 +307,14 @@ class BaseFactory(gym.Env):
else:
light_block_map = light_block_map.do_fov(*agent.pos, max(self._level_shape))
if door_shadowing:
# noinspection PyUnboundLocalVariable
light_block_map[xs, ys] = 0
agent.temp_light_map = light_block_map
obs = (obs * light_block_map) - ((1 - light_block_map) * obs[self._slices.get_idx(c.LEVEL)])
for obs_idx in range(obs.shape[0]):
if self._slices[obs_idx].can_be_shadowed:
obs[obs_idx] = (obs[obs_idx] * light_block_map) - (
(1 - light_block_map) * obs[self._slices.get_idx(c.LEVEL)]
)
if self.combin_agent_slices_in_obs and self.n_agents > 1:
agent_obs = np.sum(obs[[key for key, l_slice in self._slices.items() if c.AGENT.name in l_slice.name and
@ -357,9 +329,6 @@ class BaseFactory(gym.Env):
else:
return obs
def do_additional_actions(self, agent: Agent, action: int) -> bool:
raise NotImplementedError
def get_all_tiles_with_collisions(self) -> List[Tile]:
tiles_with_collisions = list()
for tile in self._tiles:
@ -392,7 +361,7 @@ class BaseFactory(gym.Env):
valid = c.VALID
return tile, valid
if self.parse_doors and agent.last_pos != h.NO_POS:
if self.parse_doors and agent.last_pos != c.NO_POS:
if door := self._doors.by_pos(new_tile.pos):
if door.can_collide:
return agent.tile, c.NOT_VALID
@ -416,10 +385,63 @@ class BaseFactory(gym.Env):
def calculate_reward(self) -> (int, dict):
# Returns: Reward, Info
raise NotImplementedError
info_dict = dict()
reward = 0
for agent in self._agents:
if self._actions.is_moving_action(agent.temp_action):
if agent.temp_valid:
# info_dict.update(movement=1)
reward -= 0.00
else:
# self.print('collision')
reward -= 0.01
self.print(f'{agent.name} just hit the wall at {agent.pos}.')
info_dict.update({f'{agent.name}_vs_LEVEL': 1})
elif self._actions.is_door_usage(agent.temp_action):
if agent.temp_valid:
self.print(f'{agent.name} did just use the door at {agent.pos}.')
info_dict.update(door_used=1)
else:
reward -= 0.01
self.print(f'{agent.name} just tried to use a door at {agent.pos}, but failed.')
info_dict.update({f'{agent.name}_failed_action': 1})
info_dict.update({f'{agent.name}_failed_door_open': 1})
elif self._actions.is_no_op(agent.temp_action):
info_dict.update(no_op=1)
reward -= 0.00
additional_reward, additional_info_dict = self.calculate_additional_reward(agent)
reward += additional_reward
info_dict.update(additional_info_dict)
for other_agent in agent.temp_collisions:
info_dict.update({f'{agent.name}_vs_{other_agent.name}': 1})
self.print(f"reward is {reward}")
return reward, info_dict
def render(self, mode='human'):
raise NotImplementedError
if not self._renderer: # lazy init
height, width = self._obs_cube.shape[1:]
self._renderer = Renderer(width, height, view_radius=self.pomdp_r, fps=5)
walls = [RenderEntity('wall', pos)
for pos in np.argwhere(self._slices.by_enum(c.LEVEL).slice == c.OCCUPIED_CELL.value)]
agents = []
for i, agent in enumerate(self._agents):
name, state = h.asset_str(agent)
agents.append(RenderEntity(name, agent.pos, 1, 'none', state, i + 1, agent.temp_light_map))
doors = []
if self.parse_doors:
for i, door in enumerate(self._doors):
name, state = 'door_open' if door.is_open else 'door_closed', 'blank'
doors.append(RenderEntity(name, door.pos, 1, 'none', state, i + 1))
additional_assets = self.render_additional_assets()
self._renderer.render(walls + doors + additional_assets + agents)
def save_params(self, filepath: Path):
# noinspection PyProtectedMember
@ -440,3 +462,66 @@ class BaseFactory(gym.Env):
def print(self, string):
if self.verbose:
print(string)
# Properties which are called by the base class to extend beyond attributes of the base class
@property
def additional_actions(self) -> Union[Action, List[Action]]:
"""
When heriting from this Base Class, you musst implement this methode!!!
:return: A list of Actions-object holding all additional actions.
:rtype: List[Action]
"""
return []
@property
def additional_entities(self) -> Union[Entities, List[Entities]]:
"""
When heriting from this Base Class, you musst implement this methode!!!
:return: A single Entites collection or a list of such.
:rtype: Union[Entities, List[Entities]]
"""
return []
@property
def additional_slices(self) -> Union[Slice, List[Slice]]:
"""
When heriting from this Base Class, you musst implement this methode!!!
:return: A list of Slice-objects.
:rtype: List[Slice]
"""
return []
# Functions which provide additions to functions of the base class
# Always call super!!!!!!
@abc.abstractmethod
def do_additional_reset(self) -> None:
pass
@abc.abstractmethod
def do_additional_step(self) -> dict:
return {}
@abc.abstractmethod
def do_additional_actions(self, agent: Agent, action: int) -> Union[None, bool]:
return None
@abc.abstractmethod
def calculate_additional_reward(self, agent: Agent) -> (int, dict):
return 0, {}
@abc.abstractmethod
def render_additional_assets(self):
return []
# Hooks for in between operations.
# Always call super!!!!!!
@abc.abstractmethod
def hook_pre_step(self) -> None:
pass
@abc.abstractmethod
def hook_post_step(self) -> dict:
return {}

View File

@ -1,8 +1,5 @@
import itertools
import networkx as nx
import numpy as np
from environments import helpers as h
from environments.helpers import Constants as c
import itertools
@ -16,35 +13,32 @@ class Object:
def __bool__(self):
return True
@property
def i(self):
return self._identifier
@property
def name(self):
return self._identifier
return self._name
def __init__(self, identifier, **kwargs):
self._identifier = identifier
def __init__(self, name, name_is_identifier=False, **kwargs):
name = name.name if hasattr(name, 'name') else name
self._name = f'{self.__class__.__name__}#{name}' if name_is_identifier else name
if kwargs:
print(f'Following kwargs were passed, but ignored: {kwargs}')
def __repr__(self):
return f'{self.__class__.__name__}({self._identifier})'
return f'{self.__class__.__name__}({self.name})'
class Action(Object):
@property
def name(self):
return self.i
def __init__(self, *args):
super(Action, self).__init__(*args)
class Slice(Object):
@property
def is_observable(self):
return self._is_observable
@property
def shape(self):
return self.slice.shape
@ -57,10 +51,16 @@ class Slice(Object):
def free_tiles(self):
return np.argwhere(self.slice == c.FREE_CELL.value)
def __init__(self, identifier, arrayslice, is_blocking_light=False):
def __init__(self, identifier, arrayslice, is_blocking_light=False, can_be_shadowed=True, is_observable=True):
super(Slice, self).__init__(identifier)
self.slice = arrayslice
self.is_blocking_light = is_blocking_light
self.can_be_shadowed = can_be_shadowed
self._is_observable = is_observable
def set_slice(self, new_slice: np.ndarray):
assert self.slice.shape == new_slice.shape
self.slice = new_slice
class Wall(Object):
@ -89,8 +89,8 @@ class Tile(Object):
def pos(self):
return self._pos
def __init__(self, i, pos):
super(Tile, self).__init__(i)
def __init__(self, i, pos, **kwargs):
super(Tile, self).__init__(i, **kwargs)
self._guests = dict()
self._pos = tuple(pos)
@ -164,7 +164,7 @@ class MoveableEntity(Entity):
if self._last_tile:
return self._last_tile.pos
else:
return h.NO_POS
return c.NO_POS
@property
def direction_of_view(self):
@ -206,8 +206,8 @@ class Door(Entity):
return [node for node in self.connectivity.nodes
if node not in range(len(self.connectivity_subgroups)) and node != self.pos]
def __init__(self, *args, context, closed_on_init=True, auto_close_interval=10, has_area=False):
super(Door, self).__init__(*args)
def __init__(self, *args, context, closed_on_init=True, auto_close_interval=10, has_area=False, **kwargs):
super(Door, self).__init__(*args, **kwargs)
self._state = c.CLOSED_DOOR
self.has_area = has_area
self.auto_close_interval = auto_close_interval
@ -270,8 +270,8 @@ class Door(Entity):
class Agent(MoveableEntity):
def __init__(self, *args):
super(Agent, self).__init__(*args)
def __init__(self, *args, **kwargs):
super(Agent, self).__init__(*args, **kwargs)
self.clear_temp_sate()
# noinspection PyAttributeOutsideInit
@ -280,5 +280,5 @@ class Agent(MoveableEntity):
# if attr.startswith('temp'):
self.temp_collisions = []
self.temp_valid = None
self.temp_action = -1
self.temp_action = None
self.temp_light_map = None

View File

@ -1,9 +1,7 @@
import itertools
import random
from enum import Enum
from typing import List, Union
import networkx as nx
import numpy as np
from environments.factory.base.objects import Entity, Tile, Agent, Door, Slice, Action
@ -16,11 +14,8 @@ class Register:
_accepted_objects = Entity
@classmethod
def from_argwhere_coordinates(cls, positions: (int, int), tiles):
entities = [cls._accepted_objects(i, tiles.by_pos(position)) for i, position in enumerate(positions)]
registered_obj = cls()
registered_obj.register_additional_items(entities)
return registered_obj
def from_argwhere_coordinates(cls, positions: [(int, int)], tiles):
return cls.from_tiles([tiles.by_pos(position) for position in positions])
@property
def name(self):
@ -72,8 +67,8 @@ class Register:
def by_name(self, item):
return self[self._names[item]]
def by_enum(self, enum: Enum):
return self[self._names[enum.name]]
def by_enum(self, enum_obj: Enum):
return self[self._names[enum_obj.name]]
def __repr__(self):
return f'{self.__class__.__name__}({self._register})'
@ -84,13 +79,13 @@ class Register:
def get_idx_by_name(self, item):
return self._names[item]
def get_idx(self, enum: Enum):
return self._names[enum.name]
def get_idx(self, enum_obj: Enum):
return self._names[enum_obj.name]
@classmethod
def from_tiles(cls, tiles, **kwargs):
entities = [cls._accepted_objects(f'{cls._accepted_objects.__name__.upper()}#{i}', tile, **kwargs)
for i, tile in enumerate(tiles)]
# objects_name = cls._accepted_objects.__name__
entities = [cls._accepted_objects(i, tile, name_is_identifier=True, **kwargs) for i, tile in enumerate(tiles)]
registered_obj = cls()
registered_obj.register_additional_items(entities)
return registered_obj
@ -98,14 +93,6 @@ class Register:
class EntityRegister(Register):
@classmethod
def from_argwhere_coordinates(cls, argwhere_coordinates, **kwargs):
tiles = cls()
tiles.register_additional_items(
[cls._accepted_objects(i, pos, **kwargs) for i, pos in enumerate(argwhere_coordinates)]
)
return tiles
def __init__(self):
super(EntityRegister, self).__init__()
self._tiles = dict()
@ -141,6 +128,15 @@ class Entities(Register):
class FloorTiles(EntityRegister):
_accepted_objects = Tile
@classmethod
def from_argwhere_coordinates(cls, argwhere_coordinates):
tiles = cls()
# noinspection PyTypeChecker
tiles.register_additional_items(
[cls._accepted_objects(i, pos, name_is_identifier=True) for i, pos in enumerate(argwhere_coordinates)]
)
return tiles
@property
def occupied_tiles(self):
tiles = [tile for tile in self if tile.is_occupied()]
@ -148,7 +144,7 @@ class FloorTiles(EntityRegister):
return tiles
@property
def empty_tiles(self):
def empty_tiles(self) -> List[Tile]:
tiles = [tile for tile in self if tile.is_empty()]
random.shuffle(tiles)
return tiles
@ -185,6 +181,7 @@ class Actions(Register):
def movement_actions(self):
return self._movement_actions
# noinspection PyTypeChecker
def __init__(self, movement_properties: MovementProperties, can_use_doors=False):
self.allow_no_op = movement_properties.allow_no_op
self.allow_diagonal_movement = movement_properties.allow_diagonal_movement
@ -193,43 +190,47 @@ class Actions(Register):
super(Actions, self).__init__()
if self.allow_square_movement:
self.register_additional_items([self._accepted_objects(direction) for direction in h.MANHATTAN_MOVES])
self.register_additional_items([self._accepted_objects(direction) for direction in h.ManhattanMoves])
if self.allow_diagonal_movement:
self.register_additional_items([self._accepted_objects(direction) for direction in h.DIAGONAL_MOVES])
self.register_additional_items([self._accepted_objects(direction) for direction in h.DiagonalMoves])
self._movement_actions = self._register.copy()
if self.can_use_doors:
self.register_additional_items([self._accepted_objects('use_door')])
self.register_additional_items([self._accepted_objects(h.EnvActions.USE_DOOR)])
if self.allow_no_op:
self.register_additional_items([self._accepted_objects('no-op')])
self.register_additional_items([self._accepted_objects(h.EnvActions.NOOP)])
def is_moving_action(self, action: Union[int]):
#if isinstance(action, Action):
# return (action.name in h.MANHATTAN_MOVES and self.allow_square_movement) or \
# (action.name in h.DIAGONAL_MOVES and self.allow_diagonal_movement)
#else:
return action in self.movement_actions.keys()
return action in self.movement_actions.values()
def is_no_op(self, action: Union[str, int]):
if isinstance(action, str):
action = self.by_name(action)
return self[action].name == 'no-op'
def is_no_op(self, action: Union[str, Action, int]):
if isinstance(action, int):
action = self[action]
if isinstance(action, Action):
action = action.name
return action == h.EnvActions.NOOP.name
def is_door_usage(self, action: Union[str, int]):
if isinstance(action, str):
action = self.by_name(action)
return self[action].name == 'use_door'
if isinstance(action, int):
action = self[action]
if isinstance(action, Action):
action = action.name
return action == h.EnvActions.USE_DOOR.name
class StateSlices(Register):
_accepted_objects = Slice
@property
def n_observable_slices(self):
return len([x for x in self if x.is_observable])
@property
def AGENTSTARTIDX(self):
if self._agent_start_idx:
return self._agent_start_idx
else:
self._agent_start_idx = min([idx for idx, x in self.items() if c.AGENT.name in x.name])
self._agent_start_idx = min([idx for idx, x in self.items() if c.AGENT.value in x.name])
return self._agent_start_idx
def __init__(self):