├── requirements.txt ├── gymecs ├── __init__.py ├── togym.py ├── fromgym.py ├── raylib.py └── core.py ├── setup.py ├── LICENSE ├── README.md ├── gymecs_examples ├── maze │ ├── gym_multiagent_maze_with_goal_singleagent_pointofview.py │ ├── gym_multiagent_maze_with_goal_allagents_pointofview.py │ ├── maze.py │ ├── maze_with_goal.py │ └── multiagent_maze_with_goal.py └── raylibmaze │ └── multiagent_maze_with_goal.py └── .gitignore /requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /gymecs/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import * 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | 4 | with open("requirements.txt") as f: 5 | reqs = [line.strip() for line in f] 6 | 7 | setup( 8 | name="gymecs", 9 | version="0.01", 10 | python_requires=">=3.7", 11 | packages=find_packages(), 12 | install_requires=reqs, 13 | ) 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ludovic Denoyer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gymecs 2 | 3 | GymECS is a simple python ECS system developped to facilitate the implementation and experimentation of deep RL techniques in video games. 4 | 5 | The ECS is associated with a blog that explains step by step how to create such a software. The repository will thus evolve at the same speed than the blog -- see [https://ludc.github.io/video_games_and_deep_reinforcement_learning/](https://ludc.github.io/video_games_and_deep_reinforcement_learning/) 6 | 7 | Each branch of the repository is associated with a particular blog post, and then `main` branch correspond to the most recent post. 8 | 9 | ## Installation 10 | 11 | Just `pip install -e .` would work, on Linux, Windows and MacOS. 12 | 13 | ## Citation 14 | 15 | If you are using the ECS, please refer to this website or use the following citation: 16 | 17 | ``` 18 | @misc{gymecs, 19 | author = {Ludovic Denoyer}, 20 | title = {gymECS: An entity component system for deep reinforcement learning}, 21 | year = {2022}, 22 | howpublished = {\url{https://github.com/ludc/gymecs}}, 23 | } 24 | 25 | ``` 26 | 27 | ## Stages 28 | 29 | * [Post 1](https://ludc.github.io/video_games_and_deep_reinforcement_learning/2022/10/14/a-first-Entity-Component-System-to-replace-Gym.html): A simple ECS to replace gym : [Branch](https://github.com/ludc/gymecs/tree/post1) 30 | 31 | 32 | -------------------------------------------------------------------------------- /gymecs_examples/maze/gym_multiagent_maze_with_goal_singleagent_pointofview.py: -------------------------------------------------------------------------------- 1 | from gymecs import Component,Entity,System,Game,World 2 | from gymecs.togym import GymEnv 3 | from gymecs_examples.maze.maze import RandomPlayer 4 | from gymecs_examples.maze.multiagent_maze_with_goal import MultiAgentMazeGame,Action,RandomPlayer 5 | import numpy 6 | 7 | class SingleAgentPointOfView(GymEnv): 8 | """ A game as a gym environnement focused on one particular agent 9 | 10 | """ 11 | def __init__(self,game,idx_agent): 12 | super().__init__(game=game) 13 | self._idx_agent=idx_agent 14 | 15 | def _get_observation(self): 16 | agent=self._world.get_entity("agent#"+str(self._idx_agent)) 17 | return [agent.position.x,agent.position.y] 18 | 19 | def _get_reward(self): 20 | agent=self._world.get_entity("agent#"+str(self._idx_agent)) 21 | if agent.state.collide_wall: return -0.1 22 | if agent.state.on_goal: return 10.0 23 | if not agent.state.collide_with is None: return -0.5 24 | return -1.0 25 | 26 | def _get_done(self): 27 | agent=self._world.get_entity("agent#"+str(self._idx_agent)) 28 | return agent.state.on_goal 29 | 30 | def _update_with_action(self, action): 31 | self._world.set_component("agent#"+str(self._idx_agent),"action",Action(action=action)) 32 | 33 | 34 | if __name__=="__main__": 35 | idx_agent=0 36 | n_agents=4 37 | game=MultiAgentMazeGame(size=(21,21),wall_density=0.01,n_agents=4) 38 | 39 | for i in range(n_agents): 40 | if not i==idx_agent: 41 | game.add_player(RandomPlayer("agent#"+str(i))) 42 | 43 | gymenv=SingleAgentPointOfView(game,idx_agent) 44 | gymenv.seed(0) 45 | gymenv.reset() 46 | gymenv.render() 47 | while True: 48 | action=numpy.random.randint(4) 49 | observation,reward,done,_=gymenv.step(action) 50 | print(observation,reward,done) 51 | gymenv.render() 52 | if done: break 53 | 54 | 55 | -------------------------------------------------------------------------------- /gymecs_examples/maze/gym_multiagent_maze_with_goal_allagents_pointofview.py: -------------------------------------------------------------------------------- 1 | from gymecs import Component,Entity,System,Game,World 2 | from gymecs.togym import GymEnv 3 | from gymecs_examples.maze.maze import RandomPlayer 4 | from gymecs_examples.maze.multiagent_maze_with_goal import MultiAgentMazeGame,Action,RandomPlayer,E_Agent 5 | import numpy 6 | 7 | class AllAgentsPointOfView(GymEnv): 8 | """ A game as a gym environnement focused on all the agents 9 | 10 | """ 11 | def __init__(self,game): 12 | super().__init__(game=game) 13 | 14 | def _get_observation(self): 15 | results={} 16 | for name_agent,agent in self._world.get_entities_by_type(E_Agent): 17 | results[name_agent]=[agent.position.x,agent.position.y] 18 | return results 19 | 20 | def _get_single_reward(self,idx_agent): 21 | agent=self._world.get_entity("agent#"+str(idx_agent)) 22 | if agent.state.collide_wall: return -0.1 23 | if agent.state.on_goal: return 10.0 24 | if not agent.state.collide_with is None: return -0.5 25 | return -1.0 26 | 27 | def _get_reward(self): 28 | return sum([self._get_single_reward(k) for k in range(self._game._n_agents)]) 29 | 30 | def _get_done(self): 31 | for name_agent,agent in self._world.get_entities_by_type(E_Agent): 32 | if agent.state.on_goal: return True 33 | return False 34 | 35 | def _update_with_action(self, action): 36 | for k in range(self._game._n_agents): 37 | self._world.set_component("agent#"+str(k),"action",Action(action=action[k])) 38 | 39 | if __name__=="__main__": 40 | n_agents=4 41 | game=MultiAgentMazeGame(size=(21,21),wall_density=0.01,n_agents=4) 42 | 43 | 44 | gymenv=AllAgentsPointOfView(game) 45 | gymenv.seed(0) 46 | gymenv.reset() 47 | gymenv.render() 48 | while True: 49 | action=[numpy.random.randint(4) for _ in range(n_agents)] 50 | observation,reward,done,_=gymenv.step(action) 51 | print(observation,reward,done) 52 | gymenv.render() 53 | if done: break 54 | 55 | 56 | -------------------------------------------------------------------------------- /gymecs/togym.py: -------------------------------------------------------------------------------- 1 | import gym 2 | from gymecs.core import Game 3 | import numpy 4 | 5 | class GymEnv(gym.Env): 6 | def _get_observation(self): 7 | raise NotImplementedError 8 | 9 | def _get_reward(self): 10 | raise NotImplementedError 11 | 12 | def _get_done(self): 13 | raise NotImplemented 14 | 15 | def _update_with_action(self, action): 16 | raise NotImplementedError 17 | 18 | def __init__(self, game:Game): 19 | super().__init__() 20 | self._game = game 21 | self._seed = None 22 | self._world = None 23 | 24 | def reset(self): 25 | assert not self._seed is None 26 | self._world = self._game.reset(self._seed) 27 | return self._get_observation() 28 | 29 | def step(self, action): 30 | self._update_with_action(action) 31 | self._game.step() 32 | reward = self._get_reward() 33 | done = self._get_done() 34 | obs = self._get_observation() 35 | return obs, reward, done, {} 36 | 37 | def seed(self, seed): 38 | self._seed = seed 39 | 40 | def render(self, **arguments): 41 | self._game.render(**arguments) 42 | 43 | if __name__=="__main__": 44 | from gymecs_examples.maze.maze import SingleMazeGame,Action 45 | 46 | class MazeGymEnv(GymEnv): 47 | def __init__(self,size=(21,21)): 48 | super().__init__(game=SingleMazeGame(size)) 49 | 50 | def _get_observation(self): 51 | agent=self._world.get_entity("agent") 52 | return [agent.position.x,agent.position.y] 53 | 54 | def _get_reward(self): 55 | return -1.0 56 | 57 | def _get_done(self): 58 | return False 59 | 60 | def _update_with_action(self, action): 61 | self._world.set_component("agent","action",Action(action=action)) 62 | 63 | gymenv=MazeGymEnv() 64 | gymenv.seed(0) 65 | gymenv.reset() 66 | gymenv.render() 67 | while True: 68 | action=numpy.random.randint(4) 69 | observation,reward,done,_=gymenv.step(action) 70 | gymenv.render() 71 | if done: break 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /gymecs/fromgym.py: -------------------------------------------------------------------------------- 1 | import gym 2 | from gymecs.core import Game,Component,Entity,World,System 3 | import numpy 4 | 5 | class AgentObservation(Component): 6 | env_obs = None 7 | reward: float = None 8 | 9 | class AgentAction(Component): 10 | action = None 11 | 12 | class E_Agent(Entity): 13 | observation: AgentObservation = AgentObservation() 14 | action: AgentAction = AgentAction() 15 | 16 | class GameState(Component): 17 | done: bool = False 18 | timestep: int = 0 19 | 20 | class GameEnv(Component): 21 | gym_env: gym.Env = None 22 | 23 | class Game(Entity): 24 | state: GameState = GameState() 25 | env: GameEnv = GameEnv() 26 | 27 | 28 | class Step(System): 29 | def __call__(self,world,**arguments): 30 | agent = world.get_entity("agent") 31 | game=world.get_entity("game") 32 | action = agent.action.action 33 | obs, reward, done, _ = game.env.gym_env.step(action) 34 | entity = E_Agent( 35 | observation=AgentObservation(env_obs=obs, reward=reward), 36 | ) 37 | world.set_entity("agent", entity) 38 | world.set_component("game", "state",GameState(done=done,timestep=game.state.timestep+1)) 39 | 40 | class GameFromGym(Game): 41 | def __init__(self, env_name,env_arguments={}): 42 | self._env_name=env_name 43 | self._env_arguments=env_arguments 44 | self._system=Step() 45 | 46 | def reset(self, seed, **arguments): 47 | env= gym.make(self._env_name,**self._env_arguments) 48 | env.seed(seed) 49 | 50 | self._world = World() 51 | obs = env.reset() 52 | entity = E_Agent(observation=AgentObservation(env_obs=obs)) 53 | self._world.set_entity("agent", entity) 54 | self._world.set_entity("game", Game(state=GameState(done=False),env=GameEnv(gym_env=env))) 55 | return self._world 56 | 57 | def step(self, **arguments): 58 | self._system(self._world,**arguments) 59 | 60 | def is_done(self): 61 | return self._world.get_component("game", "state").done 62 | 63 | def render(self, **kargs): 64 | self._world.get_entity("game").env.gym_env.render(**kargs) 65 | 66 | class RandomMountainCarPlayer(System): 67 | def __call__(self,world,**arguments): 68 | action=numpy.random.randint(2) 69 | world.set_component("agent","action",AgentAction(action=action)) 70 | 71 | def reset(self,seed): 72 | pass 73 | 74 | if __name__=="__main__": 75 | game=GameFromGym("CartPole-v0") 76 | world=game.reset(0) 77 | player=RandomMountainCarPlayer() 78 | player.reset(0) 79 | player(world) 80 | game.render() 81 | while not game.is_done(): 82 | game.step() 83 | if not game.is_done(): player(world) 84 | game.render() 85 | print("End at timestep = ",world.get_entity("game").state.timestep) 86 | -------------------------------------------------------------------------------- /gymecs/raylib.py: -------------------------------------------------------------------------------- 1 | from gymecs import Component,Entity,System 2 | from raylib import ffi, rl, colors 3 | import pyray as pr 4 | import collections 5 | 6 | class RayLibDrawComponent(Component): 7 | """ A component that can execute raylib commands 8 | 9 | Args: 10 | Component (_type_): _description_ 11 | """ 12 | draw_layer:int=0 # The draw layer to define execution order 13 | activated:bool=True # If False, the component is not executed 14 | 15 | def __init__(self,**values): 16 | super().__init__(**values) 17 | 18 | def _draw(self,entity): 19 | raise NotImplementedError 20 | 21 | class RayLibRectangle(RayLibDrawComponent): 22 | """ Draw a cube based on the position value. 23 | """ 24 | size:list=[0.0,0.0,0.0] 25 | color:list=[255,255,255,255] 26 | position:list=[0.0,0.0,0.0] 27 | 28 | def _draw(self,entity): 29 | position = pr.Vector3(*self.position) 30 | color = pr.Color(*self.color) 31 | rl.DrawCube(position, self.size[0],self.size[1],self.size[2], color) 32 | 33 | class RayLibBackground(RayLibDrawComponent): 34 | color=colors.WHITE 35 | draw_layer=-1000 36 | 37 | def _draw(self,entity): 38 | rl.ClearBackground(self.color) 39 | 40 | class RayLibCamera(RayLibDrawComponent): 41 | position:list=[-30.0,-30.0,18.0] 42 | target:list=[0.0,0.0,0.0] 43 | up:list=[0.0, 0.0, 1.0] 44 | fovy= 45.0 45 | projection=rl.CAMERA_PERSPECTIVE 46 | camera=None 47 | draw_layer=-10 48 | 49 | def _draw(self,entity): 50 | if self.camera is None: 51 | self.camera = pr.Camera3D(self.position,self.target,self.up,self.fovy, self.projection) 52 | pr.set_camera_mode(self.camera, pr.CAMERA_CUSTOM) 53 | else: 54 | self.camera.position=pr.Vector3(*self.position) 55 | self.camera.target=pr.Vector3(*self.target) 56 | self.camera.up=pr.Vector3(*self.up) 57 | pr.begin_mode_3d(self.camera) 58 | 59 | class RayLibSystem(System): 60 | def __init__(self,screen_size,screen_title="gymECS",camera_mode=rl.CAMERA_PERSPECTIVE,debug=False): 61 | self._screen_size=screen_size 62 | self._screen_title=screen_title 63 | self._first_run=True 64 | self._camera_mode=camera_mode 65 | self._debug=debug 66 | self.__memory_game_dt=None 67 | 68 | def _first_execution(self,world,_game_dt=None,**arguments): 69 | rl.InitWindow(self._screen_size[0], self._screen_size[1], bytes(self._screen_title, 'utf-8')) 70 | self.__memory_game_dt=_game_dt 71 | if not _game_dt is None and _game_dt>0.0: 72 | print("Fixing FPS at: ",_game_dt) 73 | rl.SetTargetFPS(int(1.0/_game_dt)) 74 | self._first_run=False 75 | 76 | def __call__(self,world,_game_dt=None,**arguments): 77 | """ Draw the raylib components 78 | 79 | Args: 80 | world (World): the world to draw 81 | _game_dt (float, optional): tme in seconds between two frames 82 | """ 83 | if not self.__memory_game_dt is None: 84 | assert _game_dt is None or self.__memory_game_dt==_game_dt,"Cannot change FPS during game." 85 | 86 | if self._first_run: 87 | self._first_execution(world,_game_dt,**arguments) 88 | rl.BeginDrawing() 89 | entity_component_names=[k for k in world.get_components_by_type(RayLibDrawComponent)] 90 | 91 | # Order by draw_layer 92 | by_layer={} 93 | for ((ename,entity),(cname,component)) in entity_component_names: 94 | layer=component.draw_layer 95 | if not layer in by_layer: by_layer[layer]=[] 96 | by_layer[layer].append(((ename,entity),(cname,component)) ) 97 | ordered = collections.OrderedDict(sorted(by_layer.items())) 98 | 99 | for l,c in ordered.items(): 100 | for (ename,entity),(cname,component) in c: 101 | component._draw(entity) 102 | if self._debug: print("Draw: ",ename,cname) 103 | pr.end_mode_3d() 104 | #rl.EndMode3D() 105 | rl.EndDrawing() 106 | 107 | -------------------------------------------------------------------------------- /gymecs_examples/maze/maze.py: -------------------------------------------------------------------------------- 1 | from gymecs import Component,Entity,System,Game,World 2 | import numpy 3 | 4 | ## Stage 1 : Components and Entities 5 | class Position(Component): 6 | x:int=0 7 | y:int=0 8 | 9 | 10 | class Action(Component): 11 | action:int=0 12 | 13 | class Size(Component): 14 | size_x:int=0 15 | size_y:int=0 16 | 17 | class MazeMap(Component): 18 | map:numpy.ndarray=None 19 | 20 | class E_Agent(Entity): 21 | position:Position=Position() 22 | action:Action=Action() 23 | 24 | class E_Maze(Entity): 25 | map:MazeMap=MazeMap() 26 | size:Size=Size() 27 | 28 | 29 | my_agent=E_Agent(position=Position(x=5,y=3),action=Action(action=0)) 30 | 31 | 32 | 33 | ##Stage 2: Systems 34 | 35 | class MoveAgent(System): 36 | def __call__(self,world,**arguments): 37 | agent,maze=world.get_entities("agent","maze") 38 | mazemap=maze.map.map 39 | action=agent.action.action 40 | x,y=agent.position.x,agent.position.y 41 | nx,ny=x,y 42 | 43 | if action==0: 44 | nx-=1 45 | elif action==1: 46 | nx+=1 47 | elif action==2: 48 | ny-=1 49 | elif action==3: 50 | ny+=1 51 | if mazemap[nx,ny]==0.0: 52 | x=nx 53 | y=ny 54 | 55 | new_position=Position(x=x,y=y) 56 | world.set_component("agent","position",new_position) 57 | 58 | class RandomPlayer(System): 59 | def __init__(self): 60 | super().__init__() 61 | 62 | def __call__(self,world,**arguments): 63 | agent=world.get_entity("agent") 64 | action=numpy.random.randint(4) 65 | component=Action(action=action) 66 | world.set_component("agent","action",component) 67 | 68 | ## Stage 3: Game 69 | 70 | class SingleMazeGame(Game): 71 | def __init__(self,size=(21,21)): 72 | super().__init__() 73 | if isinstance(size,str): size=eval(size) 74 | self._size=size 75 | self._moving_system=MoveAgent() 76 | 77 | def reset(self,seed): 78 | self._world=World() 79 | 80 | sx,sy=self._size 81 | np_map=numpy.zeros(self._size) 82 | np_map[0,:]=1.0 83 | np_map[sx-1,:]=1.0 84 | np_map[:,0]=1.0 85 | np_map[:,sy-1]=1.0 86 | maze=E_Maze(map=MazeMap(map=np_map),size=Size(size_x=sx,size_y=sy)) 87 | self._world.set_entity("maze",maze) 88 | 89 | x=numpy.random.randint(sx-2)+1 90 | y=numpy.random.randint(sy-2)+1 91 | agent=E_Agent(position=Position(x=x,y=y),action=Action()) 92 | self._world.set_entity("agent",agent) 93 | return self._world 94 | 95 | def step(self,**arguments): 96 | self._moving_system(self._world,**arguments) 97 | 98 | def is_done(self): 99 | return False 100 | 101 | def render(self): 102 | screen_width,screen_height=640,480 103 | step_x=screen_width/self._size[0] 104 | step_y=screen_height/self._size[0] 105 | 106 | try: 107 | import pygame 108 | from pygame import gfxdraw 109 | except ImportError: 110 | raise DependencyNotInstalled( 111 | "pygame is not installed, run `pip install gym[classic_control]`" 112 | ) 113 | 114 | if not "screen" in dir(self): 115 | pygame.init() 116 | pygame.display.init() 117 | self.screen = pygame.display.set_mode( 118 | (screen_width, screen_height) 119 | ) 120 | 121 | self.surf = pygame.Surface((screen_width, screen_height),depth=24) 122 | self.surf.fill((255, 255, 255)) 123 | 124 | mazemap=self._world.get_entity("maze").map.map 125 | for x in range(self._size[0]): 126 | for y in range(self._size[1]): 127 | if mazemap[x][y]==1.0: 128 | x1=int(x*step_x) 129 | x2=int((x+1)*step_x) 130 | y1=int(y*step_y) 131 | y2=int((y+1)*step_y) 132 | rect = pygame.Rect(x1,y1,int(step_x)+1,int(step_y)+1) 133 | gfxdraw.box(self.surf, rect, (0, 0, 0)) 134 | 135 | x=self._world.get_entity("agent").position.x 136 | y=self._world.get_entity("agent").position.y 137 | x1=int(x*step_x) 138 | x2=int((x+1)*step_x) 139 | y1=int(y*step_y) 140 | y2=int((y+1)*step_y) 141 | coords = [(x1,y1), (x1, y2), (x2, y2), (x2, y1)] 142 | gfxdraw.filled_polygon(self.surf, coords, (255 ,0 , 0)) 143 | 144 | self.screen.blit(self.surf, (0, 0)) 145 | pygame.display.flip() 146 | 147 | if __name__=="__main__": 148 | game=SingleMazeGame((21,21)) 149 | player=RandomPlayer() 150 | 151 | world=game.reset(0) 152 | player.reset(0) 153 | player(world) 154 | while not game.is_done(): 155 | game.step() 156 | player(world) 157 | game.render() -------------------------------------------------------------------------------- /gymecs/core.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | def dir_fields(c): 4 | return [m for m in dir(c) if not m.startswith("_")] 5 | 6 | 7 | class Component: 8 | def __init__(self, **values): 9 | fields = dir_fields(self) 10 | for f in values: 11 | assert f in fields, str(f) + " // " + str(fields) 12 | setattr(self, f, values[f]) 13 | 14 | def _structureclone(self, **args): 15 | c = self.__class__(**{k: getattr(self, k) for k in dir_fields(self)}) 16 | for k, v in args.items(): 17 | setattr(c, k, v) 18 | return c 19 | 20 | def _deepclone(self,**args): 21 | results = {k: copy.deepcopy(getattr(self, k)) for k in dir_fields(self) if not k in args} 22 | for k in args: 23 | results[k] = args[k] 24 | return self.__class__(**results) 25 | 26 | class Entity: 27 | def __init__(self, **components): 28 | fields = dir_fields(self) 29 | for name, c in components.items(): 30 | assert name in fields, ( 31 | str(type(self)) 32 | + " => " 33 | + name 34 | + " vs " 35 | + str(fields) 36 | + " for " 37 | + str(type(self)) 38 | ) 39 | setattr(self, name, c) 40 | 41 | def _structureclone(self, **args): 42 | results = {k: getattr(self, k) for k in dir_fields(self) if not k in args} 43 | for k in args: 44 | results[k] = args[k] 45 | return self.__class__(**results) 46 | 47 | def _deepclone(self, **args): 48 | results = {k: copy.deepcopy(getattr(self, k)) for k in dir_fields(self) if not k in args} 49 | for k in args: 50 | results[k] = args[k] 51 | return self.__class__(**results) 52 | 53 | 54 | class World: 55 | def __init__(self): 56 | self._entities = {} 57 | 58 | def set_entity(self, name:str, entity:Entity): 59 | """ Put an entity in the world (and overwrite) 60 | 61 | Args: 62 | name (str): the name of the entity 63 | entity (str): the entity 64 | """ 65 | self._entities[name] = entity 66 | 67 | def del_entity(self, name:str): 68 | """ Remove an entity, assuming that it exists 69 | 70 | Args: 71 | name (str): then entity name 72 | """ 73 | del self._entities[name] 74 | 75 | def set_component(self, name:str, name_component:str, component:Component): 76 | """ Put a component (and overwrite) in the world 77 | 78 | Args: 79 | name (str): name of the entity 80 | name_component (str): name of the component 81 | component (Component): the component 82 | """ 83 | entity = self._entities[name] 84 | setattr(entity, name_component, component) 85 | 86 | def get_component(self, name:str, name_component:str)->Component: 87 | """ Get a component 88 | 89 | Args: 90 | name (str): name of the entity 91 | name_component (_type_): name of the component 92 | 93 | Returns: 94 | Component: the corresponding component 95 | """ 96 | return getattr(self.get_entity(name), name_component) 97 | 98 | def get_entity(self, name:str)->Entity: 99 | """ Get an entity 100 | 101 | Args: 102 | name (str): name of the entity 103 | 104 | Returns: 105 | Entity: the corresponding entity 106 | """ 107 | return self._entities[name] 108 | 109 | def get_entities(self,*names)->list[Entity]: 110 | """ Get multiple entities 111 | 112 | Args: 113 | names (list of str): the names of the entities 114 | Returns: 115 | list[Entity]: the list of corresponding entities 116 | """ 117 | return [self.get_entity(name) for name in names] 118 | 119 | def keys(self): 120 | """ Iterator over the entities names 121 | 122 | Yields: 123 | str: name of entity 124 | """ 125 | for name in self._entities: 126 | yield name 127 | 128 | def __iter__(self): 129 | return self.keys() 130 | 131 | def items(self): 132 | """ Iterator over (name,Entity) 133 | 134 | Yields: 135 | tuple[str,Entity]: the tuple of name+Entity 136 | """ 137 | for name, entity in self._entities.items(): 138 | yield name, entity 139 | 140 | def __contains__(self, key:str)->bool: 141 | """ Check if an entity exists 142 | 143 | Args: 144 | key (str): the name of the entity 145 | 146 | Returns: 147 | bool: True if the entity exists 148 | """ 149 | return key in self._entities 150 | 151 | def get_components_by_type(self,type): 152 | """ Returns an iterator over ((entity name,entity),(component name,component)) such that the component is of type 'type' 153 | 154 | Args: 155 | type (python class): the type of the component to find 156 | """ 157 | results=[] 158 | for entity_name,entity in self._entities.items(): 159 | for component_name in dir_fields(entity): 160 | component=getattr(entity,component_name) 161 | if isinstance(component,type): 162 | yield ((entity_name,entity),(component_name,component)) 163 | 164 | def get_entities_by_type(self,type): 165 | """ returns an iterator (entity_name,entity) over entities of a given type 166 | 167 | Args: 168 | type (python class): the type to match 169 | 170 | """ 171 | for ne,e in self._entities.items(): 172 | if isinstance(e,type): yield ne,e 173 | 174 | class System: 175 | def __call__(self, world, **arguments): 176 | raise NotImplementedError 177 | 178 | def reset(self, seed): 179 | pass 180 | 181 | class Game: 182 | def reset(self, seed, **arguments)->World: 183 | raise NotImplementedError 184 | 185 | def step(self, **arguments): 186 | raise NotImplementedError 187 | 188 | def is_done(self): 189 | raise NotImplementedError 190 | 191 | def render(self, **arguments): 192 | pass 193 | 194 | -------------------------------------------------------------------------------- /gymecs_examples/maze/maze_with_goal.py: -------------------------------------------------------------------------------- 1 | from gymecs import Component,Entity,System,Game,World 2 | import numpy 3 | 4 | ## Stage 1 : Components and Entities 5 | class Position(Component): 6 | x:int=0 7 | y:int=0 8 | 9 | 10 | class Action(Component): 11 | action:int=0 12 | 13 | class Size(Component): 14 | size_x:int=0 15 | size_y:int=0 16 | 17 | class MazeMap(Component): 18 | map:numpy.ndarray=None 19 | 20 | class AgentState(Component): 21 | collide_wall:bool=False 22 | on_goal:bool=False 23 | 24 | class E_Agent(Entity): 25 | position:Position=Position() 26 | action:Action=Action() 27 | state:AgentState=AgentState() 28 | 29 | class E_Maze(Entity): 30 | map:MazeMap=MazeMap() 31 | size:Size=Size() 32 | 33 | class E_Goal(Entity): 34 | position:Position=Position() 35 | 36 | class GameInfos(Component): 37 | timestep:int=0 38 | 39 | class E_GameState(Entity): 40 | infos:GameInfos()=GameInfos() 41 | 42 | ##Stage 2: Systems 43 | 44 | class MoveAgent(System): 45 | def __call__(self,world,**arguments): 46 | agent,maze,goal=world.get_entities("agent","maze","goal") 47 | state=agent.state 48 | mazemap=maze.map.map 49 | action=agent.action.action 50 | x,y=agent.position.x,agent.position.y 51 | nx,ny=x,y 52 | 53 | if action==0: 54 | nx-=1 55 | elif action==1: 56 | nx+=1 57 | elif action==2: 58 | ny-=1 59 | elif action==3: 60 | ny+=1 61 | if mazemap[nx,ny]==0.0: 62 | state=state._structureclone(collide_wall=False) 63 | x=nx 64 | y=ny 65 | if goal.position.x==x and goal.position.y==y: 66 | state=state._structureclone(on_goal=True) 67 | else: 68 | state=state._structureclone(on_goal=False) 69 | else: 70 | state=state._structureclone(collide_wall=True) 71 | 72 | position=Position(x=x,y=y) 73 | world.set_component("agent","position",position) 74 | world.set_component("agent","state",state) 75 | 76 | class UpdateGameState(System): 77 | def __call__(self,world,**arguments): 78 | game=world.get_entity("game") 79 | infos=game.infos 80 | infos=infos._structureclone(timestep=infos.timestep+1) 81 | world.set_component("game","infos",infos) 82 | 83 | 84 | class RandomPlayer(System): 85 | def __init__(self): 86 | super().__init__() 87 | 88 | def __call__(self,world,**arguments): 89 | agent=world.get_entity("agent") 90 | action=numpy.random.randint(4) 91 | component=Action(action=action) 92 | world.set_component("agent","action",component) 93 | 94 | ## Stage 3: Game 95 | 96 | class SingleMazeGameWithGoal(Game): 97 | def __init__(self,size=(21,21),wall_density=0.1): 98 | super().__init__() 99 | if isinstance(size,str): size=eval(size) 100 | self._size=size 101 | self._wall_density=wall_density 102 | self._moving_system=MoveAgent() 103 | self._update_game_system=UpdateGameState() 104 | 105 | def _generate_map(self): 106 | sx,sy=self._size 107 | np_map=numpy.zeros(self._size) 108 | np_map[0,:]=1.0 109 | np_map[sx-1,:]=1.0 110 | np_map[:,0]=1.0 111 | np_map[:,sy-1]=1.0 112 | 113 | for x in range(1,sx-1): 114 | for y in range(1,sy-1): 115 | if numpy.random.rand()