├── .gitignore ├── .gitmodules ├── LICENSE ├── NOTICE.txt ├── README.md ├── SECURITY.md ├── assets ├── default_textures.png ├── textures-1.png └── textures-2.png ├── baseline_experiments ├── eval_maps │ ├── Eval110x10_notextures │ │ ├── 10x10.cfg │ │ ├── 10x10.wad │ │ └── 10x10_MAP01.txt │ ├── Eval120x20_notextures │ │ ├── 20x20.cfg │ │ ├── 20x20.wad │ │ └── 20x20_MAP01.txt │ ├── Eval210x10_notextures │ │ ├── 10x10.cfg │ │ ├── 10x10.wad │ │ └── 10x10_MAP01.txt │ ├── Eval220x20_notextures │ │ ├── 20x20.cfg │ │ ├── 20x20.wad │ │ └── 20x20_MAP01.txt │ ├── Eval310x10_notextures │ │ ├── 10x10.cfg │ │ ├── 10x10.wad │ │ └── 10x10_MAP01.txt │ └── Eval320x20_notextures │ │ ├── 20x20.cfg │ │ ├── 20x20.wad │ │ └── 20x20_MAP01.txt ├── evaluator.py ├── experiment.py └── experiments.md ├── example.py ├── mazeexplorer ├── __init__.py ├── compile_acs.py ├── config_template.txt ├── content │ ├── acs_template.txt │ └── doom_textures.txt ├── maze.py ├── mazeexplorer.py ├── script_manipulator.py ├── vizdoom_gym.py └── wad.py ├── setup.py ├── test.sh └── tests ├── mazes ├── test.cfg └── test.wad ├── test_compile_acs.py ├── test_mazeexplorer.py └── test_vizdoom_gym.py /.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 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # Custom gitignore 107 | .idea/ 108 | .DS_Store 109 | mazeexplorer.egg-info/ 110 | outputs/ 111 | mazes/* 112 | mazeexplorer/content/maze.acs 113 | _vizdoom.ini -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "mazeexplorer/acc"] 2 | path = mazeexplorer/acc 3 | url = https://github.com/rheit/acc 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 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 -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | NOTICES AND INFORMATION 2 | Do Not Translate or Localize 3 | 4 | This software incorporates material from third parties. Microsoft makes certain 5 | open source code available at http://3rdpartysource.microsoft.com, or you may 6 | send a check or money order for US $5.00, including the product name, the open 7 | source component name, and version number, to: 8 | 9 | Source Code Compliance Team 10 | Microsoft Corporation 11 | One Microsoft Way 12 | Redmond, WA 98052 13 | USA 14 | 15 | Notwithstanding any other terms, you may reverse engineer this software to the 16 | extent required to debug changes to any libraries licensed under the GNU Lesser 17 | General Public License. 18 | 19 | --- 20 | 21 | NavDoom 22 | MIT License 23 | Copyright (c) 2018 Jae 24 | Permission is hereby granted, free of charge, to any person obtaining a copy 25 | of this software and associated documentation files (the "Software"), to deal 26 | in the Software without restriction, including without limitation the rights 27 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 28 | copies of the Software, and to permit persons to whom the Software is 29 | furnished to do so, subject to the following conditions: 30 | The above copyright notice and this permission notice shall be included in all 31 | copies or substantial portions of the Software. 32 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 33 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 34 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 35 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 36 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 37 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 38 | SOFTWARE. 39 | https://github.com/agiantwhale/NavDoom -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MazeExplorer 2 | 3 | MazeExplorer is a customisable 3D benchmark for assessing generalisation in Reinforcement Learning. 4 | 5 | Simply put, MazeExplorer makes it easy to create separate training and test environments for your agents. 6 | 7 | It is based on the 3D first-person game [Doom](https://en.wikipedia.org/wiki/Doom_(1993_video_game)) and the open-source 8 | environment [VizDoom](https://github.com/mwydmuch/ViZDoom). 9 | 10 | This repository contains the code for the MazeExplorer Gym Environment along with the scripts to generate baseline results. The paper can be found [here](http://ieee-cog.org/papers/paper_210.pdf). 11 | 12 | By Luke Harries*, Sebastian Lee*, Jaroslaw Rzepecki, Katja Hofmann, and Sam Devlin. 13 | \* Joint first author 14 | 15 | ![Default textures](https://github.com/microsoft/MazeExplorer/raw/master/assets/default_textures.png) ![Random Textures](https://github.com/microsoft/MazeExplorer/raw/master/assets/textures-1.png) ![Random Textures](https://github.com/microsoft/MazeExplorer/raw/master/assets/textures-2.png) 16 | 17 | # The Mission 18 | 19 | The goal is to navigate a procedurally generated maze and collect a set number of keys. 20 | 21 | The environment is highly customisable, allowing you to create different training and test environments. 22 | 23 | The following features of the environment can be configured: 24 | - Unique or repeated maps 25 | - Number of maps 26 | - Map Size (X, Y) 27 | - Maze complexity 28 | - Maze density 29 | - Random/Fixed keys 30 | - Random/Fixed textures 31 | - Random/Fixed spawn 32 | - Number of keys 33 | - Environment Seed 34 | - Episode timeout 35 | - Reward clipping 36 | - Frame stack 37 | - Resolution 38 | - Action frame repeat 39 | - Actions space 40 | - Specific textures (Wall, 41 | ceiling, floor) 42 | - Data Augmentation 43 | 44 | # Example Usage 45 | 46 | ```python 47 | from mazeexplorer import MazeExplorer 48 | 49 | train_env = MazeExplorer(number_maps=1, 50 | size=(15, 15), 51 | random_spawn=True, 52 | random_textures=False, 53 | keys=6) 54 | 55 | test_env = MazeExplorer(number_maps=1, 56 | size=(15, 15), 57 | random_spawn=True, 58 | random_textures=False, 59 | keys=6) 60 | 61 | # training 62 | for _ in range(1000): 63 | obs, rewards, dones, info = train_env.step(train_env.action_space.sample()) 64 | 65 | 66 | # testing 67 | for _ in range(1000): 68 | obs, rewards, dones, info = test_env.step(test_env.action_space.sample()) 69 | ``` 70 | 71 | # Installation 72 | 73 | 1. Install the dependencies for VizDoom: [Linux](https://github.com/mwydmuch/ViZDoom/blob/master/doc/Building.md#-linux), [MacOS](https://github.com/mwydmuch/ViZDoom/blob/master/doc/Building.md#-linux) or [Windows](https://github.com/mwydmuch/ViZDoom/blob/master/doc/Building.md#-windows). 74 | 1. `pip3 install virtualenv pytest` 75 | 1. Create a virtualenv and activate it 76 | 1. `virtualenv mazeexplorer-env` 77 | 1. `source maze-env/bin/activate` 78 | 1. Git clone this repo `git clone https://github.com/microsoft/MazeExplorer` 79 | 1. cd into the repo: `cd MazeExplorer` 80 | 1. Pull the submodules with `git submodule update --init --recursive` 81 | 1. Install the dependencies: `pip3 install -e .` 82 | 1. Run the tests: `bash test.sh` 83 | 84 | # Baseline experiments 85 | 86 | The information to reproduce the baseline experiments is shown in `baseline_experiments/experiments.md`. 87 | 88 | # Citation 89 | 90 | If you use this environment please cite the following: 91 | 92 | ``` 93 | @article{harrieslee2019, title={MazeExplorer: A Customisable 3D Benchmark for Assessing Generalisation in Reinforcement Learning}, author={Harries*, Luke and Lee*, Sebastian and Rzepecki, Jaroslaw and Hofmann, Katja and Devlin, Sam}, journal={In Proc. IEEE Conference on Games}, year={2019} } 94 | ``` 95 | 96 | # Contributing 97 | 98 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 99 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 100 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 101 | 102 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 103 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 104 | provided by the bot. You will only need to do this once across all repos using our CLA. 105 | 106 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 107 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 108 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 109 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /assets/default_textures.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/MazeExplorer/94931e5c7bddb006eeea09e39e9d91deb8387af1/assets/default_textures.png -------------------------------------------------------------------------------- /assets/textures-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/MazeExplorer/94931e5c7bddb006eeea09e39e9d91deb8387af1/assets/textures-1.png -------------------------------------------------------------------------------- /assets/textures-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/MazeExplorer/94931e5c7bddb006eeea09e39e9d91deb8387af1/assets/textures-2.png -------------------------------------------------------------------------------- /baseline_experiments/eval_maps/Eval110x10_notextures/10x10.cfg: -------------------------------------------------------------------------------- 1 | available_buttons = {MOVE_FORWARD TURN_LEFT TURN_RIGHT} 2 | doom_scenario_path = 10x10.wad 3 | 4 | screen_resolution = RES_160X120 5 | screen_format = CRCGCB 6 | depth_buffer_enabled = true 7 | labels_buffer_enabled = false 8 | automap_buffer_enabled = false 9 | render_hud = false 10 | render_minimal_hud = false 11 | render_crosshair = false 12 | render_weapon = false 13 | render_decals = false 14 | render_particles = false 15 | render_effects_sprites = false 16 | episode_timeout = 1500 17 | window_visible = false 18 | available_game_variables = {POSITION_X POSITION_Y POSITION_Z VELOCITY_X VELOCITY_Y VELOCITY_Z ANGLE} 19 | game_args += +cl_run 1 20 | game_args += +vid_aspect 3 21 | game_args += +cl_spreaddecals 0 22 | game_args += +hud_scale 1 23 | game_args += +cl_bloodtype 2 24 | game_args += +cl_pufftype 1 25 | game_args += +cl_missiledecals 0 26 | game_args += +cl_bloodsplats 0 27 | game_args += +cl_showmultikills 0 28 | game_args += +cl_showsprees 0 29 | game_args += +con_midtime 0 30 | game_args += +con_notifytime 0 31 | game_args += +am_textured 1 32 | game_args += +am_showtime 0 33 | game_args += +am_showmonsters 0 34 | game_args += +am_showsecrets 0 35 | -------------------------------------------------------------------------------- /baseline_experiments/eval_maps/Eval110x10_notextures/10x10.wad: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/MazeExplorer/94931e5c7bddb006eeea09e39e9d91deb8387af1/baseline_experiments/eval_maps/Eval110x10_notextures/10x10.wad -------------------------------------------------------------------------------- /baseline_experiments/eval_maps/Eval110x10_notextures/10x10_MAP01.txt: -------------------------------------------------------------------------------- 1 | XXXXXXXXXXX 2 | X X 3 | X X XXX X 4 | X X X X X 5 | X X X XXX X 6 | X X X 7 | X XXXXX X X 8 | X X X X X 9 | X X X X X 10 | X X X 11 | XXXXXXXXXXX -------------------------------------------------------------------------------- /baseline_experiments/eval_maps/Eval120x20_notextures/20x20.cfg: -------------------------------------------------------------------------------- 1 | available_buttons = {MOVE_FORWARD TURN_LEFT TURN_RIGHT} 2 | doom_scenario_path = 20x20.wad 3 | 4 | screen_resolution = RES_160X120 5 | screen_format = CRCGCB 6 | depth_buffer_enabled = true 7 | labels_buffer_enabled = false 8 | automap_buffer_enabled = false 9 | render_hud = false 10 | render_minimal_hud = false 11 | render_crosshair = false 12 | render_weapon = false 13 | render_decals = false 14 | render_particles = false 15 | render_effects_sprites = false 16 | episode_timeout = 1500 17 | window_visible = false 18 | available_game_variables = {POSITION_X POSITION_Y POSITION_Z VELOCITY_X VELOCITY_Y VELOCITY_Z ANGLE} 19 | game_args += +cl_run 1 20 | game_args += +vid_aspect 3 21 | game_args += +cl_spreaddecals 0 22 | game_args += +hud_scale 1 23 | game_args += +cl_bloodtype 2 24 | game_args += +cl_pufftype 1 25 | game_args += +cl_missiledecals 0 26 | game_args += +cl_bloodsplats 0 27 | game_args += +cl_showmultikills 0 28 | game_args += +cl_showsprees 0 29 | game_args += +con_midtime 0 30 | game_args += +con_notifytime 0 31 | game_args += +am_textured 1 32 | game_args += +am_showtime 0 33 | game_args += +am_showmonsters 0 34 | game_args += +am_showsecrets 0 35 | -------------------------------------------------------------------------------- /baseline_experiments/eval_maps/Eval120x20_notextures/20x20.wad: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/MazeExplorer/94931e5c7bddb006eeea09e39e9d91deb8387af1/baseline_experiments/eval_maps/Eval120x20_notextures/20x20.wad -------------------------------------------------------------------------------- /baseline_experiments/eval_maps/Eval120x20_notextures/20x20_MAP01.txt: -------------------------------------------------------------------------------- 1 | XXXXXXXXXXXXXXXXXXXXX 2 | X X X 3 | X XXXXX XXXXX X 4 | X X X X 5 | X X X XXXXX X X X 6 | X X X X X X X 7 | X X X X X XXX X X 8 | X X X X X X X X 9 | X X XXX X X XXX X 10 | X X X X X X X X 11 | X X X X X XXXXX X X X 12 | X X X X X X X X X 13 | X X X X X X XXXXX X 14 | X X X X X X X 15 | X X X X X X X 16 | X X X X X X 17 | X X X X X X X 18 | X X X X X X X 19 | X X X X XXX X 20 | X X 21 | XXXXXXXXXXXXXXXXXXXXX -------------------------------------------------------------------------------- /baseline_experiments/eval_maps/Eval210x10_notextures/10x10.cfg: -------------------------------------------------------------------------------- 1 | available_buttons = {MOVE_FORWARD TURN_LEFT TURN_RIGHT} 2 | doom_scenario_path = 10x10.wad 3 | 4 | screen_resolution = RES_160X120 5 | screen_format = CRCGCB 6 | depth_buffer_enabled = true 7 | labels_buffer_enabled = false 8 | automap_buffer_enabled = false 9 | render_hud = false 10 | render_minimal_hud = false 11 | render_crosshair = false 12 | render_weapon = false 13 | render_decals = false 14 | render_particles = false 15 | render_effects_sprites = false 16 | episode_timeout = 1500 17 | window_visible = false 18 | available_game_variables = {POSITION_X POSITION_Y POSITION_Z VELOCITY_X VELOCITY_Y VELOCITY_Z ANGLE} 19 | game_args += +cl_run 1 20 | game_args += +vid_aspect 3 21 | game_args += +cl_spreaddecals 0 22 | game_args += +hud_scale 1 23 | game_args += +cl_bloodtype 2 24 | game_args += +cl_pufftype 1 25 | game_args += +cl_missiledecals 0 26 | game_args += +cl_bloodsplats 0 27 | game_args += +cl_showmultikills 0 28 | game_args += +cl_showsprees 0 29 | game_args += +con_midtime 0 30 | game_args += +con_notifytime 0 31 | game_args += +am_textured 1 32 | game_args += +am_showtime 0 33 | game_args += +am_showmonsters 0 34 | game_args += +am_showsecrets 0 35 | -------------------------------------------------------------------------------- /baseline_experiments/eval_maps/Eval210x10_notextures/10x10.wad: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/MazeExplorer/94931e5c7bddb006eeea09e39e9d91deb8387af1/baseline_experiments/eval_maps/Eval210x10_notextures/10x10.wad -------------------------------------------------------------------------------- /baseline_experiments/eval_maps/Eval210x10_notextures/10x10_MAP01.txt: -------------------------------------------------------------------------------- 1 | XXXXXXXXXXX 2 | X X 3 | X X XXX X 4 | X X X X X 5 | X X X X X 6 | X X X X 7 | X X X X 8 | X X X X 9 | X XXXXX X 10 | X X 11 | XXXXXXXXXXX -------------------------------------------------------------------------------- /baseline_experiments/eval_maps/Eval220x20_notextures/20x20.cfg: -------------------------------------------------------------------------------- 1 | available_buttons = {MOVE_FORWARD TURN_LEFT TURN_RIGHT} 2 | doom_scenario_path = 20x20.wad 3 | 4 | screen_resolution = RES_160X120 5 | screen_format = CRCGCB 6 | depth_buffer_enabled = true 7 | labels_buffer_enabled = false 8 | automap_buffer_enabled = false 9 | render_hud = false 10 | render_minimal_hud = false 11 | render_crosshair = false 12 | render_weapon = false 13 | render_decals = false 14 | render_particles = false 15 | render_effects_sprites = false 16 | episode_timeout = 1500 17 | window_visible = false 18 | available_game_variables = {POSITION_X POSITION_Y POSITION_Z VELOCITY_X VELOCITY_Y VELOCITY_Z ANGLE} 19 | game_args += +cl_run 1 20 | game_args += +vid_aspect 3 21 | game_args += +cl_spreaddecals 0 22 | game_args += +hud_scale 1 23 | game_args += +cl_bloodtype 2 24 | game_args += +cl_pufftype 1 25 | game_args += +cl_missiledecals 0 26 | game_args += +cl_bloodsplats 0 27 | game_args += +cl_showmultikills 0 28 | game_args += +cl_showsprees 0 29 | game_args += +con_midtime 0 30 | game_args += +con_notifytime 0 31 | game_args += +am_textured 1 32 | game_args += +am_showtime 0 33 | game_args += +am_showmonsters 0 34 | game_args += +am_showsecrets 0 35 | -------------------------------------------------------------------------------- /baseline_experiments/eval_maps/Eval220x20_notextures/20x20.wad: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/MazeExplorer/94931e5c7bddb006eeea09e39e9d91deb8387af1/baseline_experiments/eval_maps/Eval220x20_notextures/20x20.wad -------------------------------------------------------------------------------- /baseline_experiments/eval_maps/Eval220x20_notextures/20x20_MAP01.txt: -------------------------------------------------------------------------------- 1 | XXXXXXXXXXXXXXXXXXXXX 2 | X X 3 | X X XXX X XXXXX X X X 4 | X X X X X X X X X 5 | X XXX XXX XXX X X X X 6 | X X X X X X X X 7 | X XXXXX XXX X X X X X 8 | X X X X X X X 9 | X X X X X X X X X 10 | X X X X X X X X 11 | X X X X X X X X X 12 | X X X X X X X X 13 | X X X XXX X X X X 14 | X X X X X X 15 | X XXXXXXX X XXXXX X X 16 | X X X X 17 | X X XXX XXX XXX XXX X 18 | X X X X X X X X X 19 | X X X XXXXXXX XXX X X 20 | X X 21 | XXXXXXXXXXXXXXXXXXXXX -------------------------------------------------------------------------------- /baseline_experiments/eval_maps/Eval310x10_notextures/10x10.cfg: -------------------------------------------------------------------------------- 1 | available_buttons = {MOVE_FORWARD TURN_LEFT TURN_RIGHT} 2 | doom_scenario_path = 10x10.wad 3 | 4 | screen_resolution = RES_160X120 5 | screen_format = CRCGCB 6 | depth_buffer_enabled = true 7 | labels_buffer_enabled = false 8 | automap_buffer_enabled = false 9 | render_hud = false 10 | render_minimal_hud = false 11 | render_crosshair = false 12 | render_weapon = false 13 | render_decals = false 14 | render_particles = false 15 | render_effects_sprites = false 16 | episode_timeout = 1500 17 | window_visible = false 18 | available_game_variables = {POSITION_X POSITION_Y POSITION_Z VELOCITY_X VELOCITY_Y VELOCITY_Z ANGLE} 19 | game_args += +cl_run 1 20 | game_args += +vid_aspect 3 21 | game_args += +cl_spreaddecals 0 22 | game_args += +hud_scale 1 23 | game_args += +cl_bloodtype 2 24 | game_args += +cl_pufftype 1 25 | game_args += +cl_missiledecals 0 26 | game_args += +cl_bloodsplats 0 27 | game_args += +cl_showmultikills 0 28 | game_args += +cl_showsprees 0 29 | game_args += +con_midtime 0 30 | game_args += +con_notifytime 0 31 | game_args += +am_textured 1 32 | game_args += +am_showtime 0 33 | game_args += +am_showmonsters 0 34 | game_args += +am_showsecrets 0 35 | -------------------------------------------------------------------------------- /baseline_experiments/eval_maps/Eval310x10_notextures/10x10.wad: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/MazeExplorer/94931e5c7bddb006eeea09e39e9d91deb8387af1/baseline_experiments/eval_maps/Eval310x10_notextures/10x10.wad -------------------------------------------------------------------------------- /baseline_experiments/eval_maps/Eval310x10_notextures/10x10_MAP01.txt: -------------------------------------------------------------------------------- 1 | XXXXXXXXXXX 2 | X X X 3 | X XXX X X 4 | X X X X X 5 | X X X XXX X 6 | X X X X X 7 | X X XXX X 8 | X X X 9 | X X X X 10 | X X X 11 | XXXXXXXXXXX -------------------------------------------------------------------------------- /baseline_experiments/eval_maps/Eval320x20_notextures/20x20.cfg: -------------------------------------------------------------------------------- 1 | available_buttons = {MOVE_FORWARD TURN_LEFT TURN_RIGHT} 2 | doom_scenario_path = 20x20.wad 3 | 4 | screen_resolution = RES_160X120 5 | screen_format = CRCGCB 6 | depth_buffer_enabled = true 7 | labels_buffer_enabled = false 8 | automap_buffer_enabled = false 9 | render_hud = false 10 | render_minimal_hud = false 11 | render_crosshair = false 12 | render_weapon = false 13 | render_decals = false 14 | render_particles = false 15 | render_effects_sprites = false 16 | episode_timeout = 1500 17 | window_visible = false 18 | available_game_variables = {POSITION_X POSITION_Y POSITION_Z VELOCITY_X VELOCITY_Y VELOCITY_Z ANGLE} 19 | game_args += +cl_run 1 20 | game_args += +vid_aspect 3 21 | game_args += +cl_spreaddecals 0 22 | game_args += +hud_scale 1 23 | game_args += +cl_bloodtype 2 24 | game_args += +cl_pufftype 1 25 | game_args += +cl_missiledecals 0 26 | game_args += +cl_bloodsplats 0 27 | game_args += +cl_showmultikills 0 28 | game_args += +cl_showsprees 0 29 | game_args += +con_midtime 0 30 | game_args += +con_notifytime 0 31 | game_args += +am_textured 1 32 | game_args += +am_showtime 0 33 | game_args += +am_showmonsters 0 34 | game_args += +am_showsecrets 0 35 | -------------------------------------------------------------------------------- /baseline_experiments/eval_maps/Eval320x20_notextures/20x20.wad: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/MazeExplorer/94931e5c7bddb006eeea09e39e9d91deb8387af1/baseline_experiments/eval_maps/Eval320x20_notextures/20x20.wad -------------------------------------------------------------------------------- /baseline_experiments/eval_maps/Eval320x20_notextures/20x20_MAP01.txt: -------------------------------------------------------------------------------- 1 | XXXXXXXXXXXXXXXXXXXXX 2 | X X 3 | X X X X X X 4 | X X X X X X 5 | X X X XXXXX XXXXX X 6 | X X X X X X 7 | X X X X X X XXX X 8 | X X X X X X X 9 | X X X XXX XXX XXX X X 10 | X X X X X X X X X 11 | X X X X X XXXXX X X X 12 | X X X X X X X X X 13 | X X X X X X X X X 14 | X X X X X X X X 15 | X X X X XXX X X 16 | X X X X X 17 | X X X X XXXXX X X 18 | X X X X X X X X 19 | X X XXXXX X XXX X 20 | X X 21 | XXXXXXXXXXXXXXXXXXXXX -------------------------------------------------------------------------------- /baseline_experiments/evaluator.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License 3 | 4 | import os 5 | 6 | import numpy as np 7 | from stable_baselines.common.vec_env import DummyVecEnv, SubprocVecEnv, VecFrameStack 8 | from tensorboardX import SummaryWriter 9 | 10 | from mazeexplorer import MazeExplorer 11 | 12 | 13 | def load_stable_baselines_env(cfg_path, vector_length, mp, n_stack, number_maps, action_frame_repeat, 14 | scaled_resolution): 15 | env_fn = lambda: MazeExplorer.load_vizdoom_env(cfg_path, number_maps, action_frame_repeat, scaled_resolution) 16 | 17 | if mp: 18 | env = SubprocVecEnv([env_fn for _ in range(vector_length)]) 19 | else: 20 | env = DummyVecEnv([env_fn for _ in range(vector_length)]) 21 | 22 | if n_stack > 0: 23 | env = VecFrameStack(env, n_stack=n_stack) 24 | 25 | return env 26 | 27 | 28 | class Evaluator: 29 | def __init__(self, mazes_path, tensorboard_dir, vector_length, mp, n_stack, action_frame_repeat=4, 30 | scaled_resolution=(42, 42)): 31 | 32 | self.tensorboard_dir = tensorboard_dir 33 | 34 | mazes_folders = [(x, os.path.join(mazes_path, x)) for x in os.listdir(mazes_path)] 35 | get_cfg = lambda x: os.path.join(x, [cfg for cfg in sorted(os.listdir(x)) if cfg.endswith('.cfg')][0]) 36 | self.eval_cfgs = [(x[0], get_cfg(x[1])) for x in mazes_folders] 37 | 38 | if not len(self.eval_cfgs): 39 | raise FileNotFoundError("No eval cfgs found") 40 | 41 | number_maps = 1 # number of maps inside each eval map path 42 | 43 | self.eval_envs = [(name, load_stable_baselines_env(cfg_path, vector_length, mp, n_stack, number_maps, 44 | action_frame_repeat, scaled_resolution)) 45 | for name, cfg_path in self.eval_cfgs] 46 | 47 | self.vector_length = vector_length 48 | self.mp = mp 49 | self.n_stack = n_stack 50 | 51 | self.eval_summary_writer = SummaryWriter(tensorboard_dir) 52 | 53 | def evaluate(self, model, n_training_steps, desired_episode_count, save=False): 54 | eval_means = [] 55 | eval_scores = [] 56 | 57 | for eval_name, eval_env in self.eval_envs: 58 | eval_episode_rewards = [] 59 | total_rewards = [0] * self.vector_length 60 | 61 | episode_count = 0 62 | 63 | obs = eval_env.reset() 64 | 65 | while episode_count < desired_episode_count: 66 | action, _states = model.predict(obs) 67 | obs, rewards, dones, info = eval_env.step(action) 68 | total_rewards += rewards 69 | 70 | for idx, done in enumerate(dones): 71 | if done: 72 | eval_episode_rewards.append(total_rewards[idx]) 73 | total_rewards[idx] = 0 74 | episode_count += 1 75 | 76 | mean = sum(eval_episode_rewards) / len(eval_episode_rewards) 77 | self.eval_summary_writer.add_scalar('eval/' + eval_name + '_reward', mean, n_training_steps) 78 | eval_means.append(mean) 79 | eval_scores.append(eval_episode_rewards) 80 | 81 | self.eval_summary_writer.add_scalar('eval/' + 'mean_reward', sum(eval_means) / len(eval_means), 82 | n_training_steps) 83 | 84 | if save: 85 | np.save(os.path.join(self.tensorboard_dir, "eval_" + str(n_training_steps)), eval_scores) 86 | -------------------------------------------------------------------------------- /baseline_experiments/experiment.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License 3 | 4 | import argparse 5 | import datetime 6 | import os 7 | import time 8 | 9 | from evaluator import Evaluator 10 | from stable_baselines import PPO2, A2C 11 | from stable_baselines.common.policies import CnnPolicy, CnnLstmPolicy 12 | from stable_baselines.common.vec_env import DummyVecEnv, SubprocVecEnv, VecFrameStack 13 | 14 | from mazeexplorer import MazeExplorer 15 | 16 | parser = argparse.ArgumentParser(description='Stable Baseline MazeExplorer Experiment Specification') 17 | 18 | parser.add_argument('--algorithm', type=str, default="ppo", 19 | help='name of algorithm to be run') 20 | parser.add_argument('--number_maps', type=int, 21 | help='number of maps to train on', default=1) 22 | parser.add_argument('--random_spawn', type=int, 23 | help='whether or not to have agent respawn randomly every new episode', default=0) 24 | parser.add_argument('--random_textures', type=int, 25 | help='whether or not floor, wall, ceilling textures should be randomly sampled', default=0) 26 | parser.add_argument('--experiment_name', type=str, 27 | help='name of the experiment', default=None) 28 | parser.add_argument('--lstm', type=int, 29 | help='whether to use an lstm with the algorithm specified', default=1) 30 | parser.add_argument('--random_keys', type=int, 31 | help='whether or not to fix keys within a particular generated map', default=0) 32 | parser.add_argument('--keys', type=int, 33 | help='number of keys to place in each map', default=9) 34 | parser.add_argument('--x', type=int, 35 | help='x dimension of map', default=10) 36 | parser.add_argument('--y', type=int, 37 | help='y dimension of map', default=10) 38 | parser.add_argument('--cpu', type=int, 39 | help='number of cpus', default=20) 40 | parser.add_argument('--steps', type=int, 41 | help='number of steps to train agent', default=10000000) 42 | parser.add_argument('--eval_occurrence', type=int, 43 | help='number of steps training until running eval', default=200000) 44 | parser.add_argument('--eval_episodes', type=int, 45 | help='number of steps to train agent', default=100) 46 | parser.add_argument('--eval', type=int, 47 | help='whether to use the eval loop', default=1) 48 | parser.add_argument('--n_stack', type=int, 49 | help='number of frames to stack (use 0 for no frame stack)', default=4) 50 | parser.add_argument('--clip', type=int, 51 | help='whether or not to clip environment reward', default=1) 52 | parser.add_argument('--mp', type=int, 53 | help='whether or not to use multiprocessing', default=0) 54 | parser.add_argument('--env_seed', type=int, 55 | help='the seed to use for map generation and spawns', default=None) 56 | parser.add_argument('--alg_seed', type=int, 57 | help='the seed to use for learning initialisation', default=None) 58 | parser.add_argument('--complexity', type=float, 59 | help='complexity for maze generation', default=.7) 60 | parser.add_argument('--density', type=float, 61 | help='density for maze generation', default=.7) 62 | parser.add_argument('--gpu_id', type=float, 63 | help='specify which gpu to use', default=0) 64 | parser.add_argument('--episode_timeout', type=int, 65 | help='maximum epsiode length', default=2100) 66 | 67 | DIR_PATH = os.path.dirname(os.path.realpath(__file__)) 68 | 69 | 70 | def stable_baseline_training(algorithm, steps, number_maps, random_spawn, random_textures, lstm, 71 | random_keys, keys, dimensions, num_cpus, n_stack, clip, complexity, density, 72 | mp, eval_occurrence, eval_episodes, eval, experiment_name, env_seed, alg_seed, 73 | episode_timeout): 74 | """ 75 | Runs OpenAI stable baselines implementation of specified algorithm on specified environment with specified training configurations. 76 | Note: For scenarios not using MazeExplorer but using the vizdoom wrapper, the .T transpose on the array being fed into the process image method needs to be removed. 77 | Note: Ensure relevant maps are in specified paths under mazes folder for simpler and manual scenarios. 78 | 79 | :param algorithm: which algorithm to run (currently support for PPO and A2C) 80 | :param steps: number of steps to run training 81 | :param number_maps: number of maps to generate and train on 82 | :param random_spawn: whether or not to randomise the spawn position of the agent 83 | :param random_textures: whether or not to randomise textures in generated maps 84 | :param lstm: whether or not to add an lstm to the network 85 | :param random_keys: whether to randmise key placement upon each new training episode in a given map 86 | :param keys: number of keys to place in generated maps 87 | :param dimensions: x, y dimensions of maps to be generated 88 | :param num_cpus: number of environments in which to train 89 | :param n_stack: number of frames to stack to feed as a state to the agent 90 | :param clip: whether or not to clip rewards from the environment 91 | :param complexity: float between 0 and 1 describing the complexity of the generated mazes 92 | :param density: float between 0 and 1 describing the density of the generated mazes 93 | :param mp: whether or not to use multiprocessing for workers 94 | :param eval_occurrence: parameter specifying period of running evaluation 95 | :param eval_episodes: number of times to perform episode rollout during evaluation 96 | :param eval: whether or not to use evaluation during training 97 | :param experiment_name: name of experiment for use in logging and file saving 98 | :param env_seed: seed to be used for environment generation 99 | :param alg_seed: seed to be used for stable-baseline algorithms 100 | :param episode_timeout: number of steps after which to terminate episode 101 | """ 102 | 103 | timestamp = datetime.datetime.fromtimestamp(time.time()).strftime('%Y-%m-%d-%H-%M-%S') 104 | 105 | # generate a file in log directory containing training configuration information. 106 | experiment_name = experiment_name + "/" if experiment_name else "" 107 | OUTPUT_PATH = os.path.join(DIR_PATH, 'results', experiment_name, timestamp) 108 | os.makedirs(OUTPUT_PATH, exist_ok=True) 109 | with open(os.path.join(OUTPUT_PATH, 'params.txt'), 'w+') as f: 110 | f.write(str({'algorithm': algorithm, 'number_maps': number_maps, 'random_spawn': random_spawn, 111 | 'random_textures': random_textures, 'lstm': lstm, 'random_keys': random_keys, 112 | 'keys': keys, 'dimensions': dimensions, 'num_cpus': num_cpus, 113 | 'clip': clip, 'mp': mp, 'n_stack': n_stack, 'env_seed': env_seed, 'alg_seed': alg_seed, 114 | 'experiment_name': experiment_name, 115 | "eval_occurrence": eval_occurrence, "eval_episodes": eval_episodes, 116 | "episode_timeout": episode_timeout})) 117 | 118 | if clip: 119 | clip_range = (-1, 1) 120 | else: 121 | clip_range = False 122 | 123 | mazeexplorer_env = MazeExplorer(number_maps=number_maps, random_spawn=random_spawn, random_textures=random_textures, 124 | random_key_positions=random_keys, keys=keys, size=dimensions, clip=clip_range, 125 | seed=env_seed, 126 | complexity=complexity, density=density) 127 | 128 | if mp: 129 | env = SubprocVecEnv([mazeexplorer_env.create_env() for _ in range(num_cpus)]) 130 | else: 131 | env = DummyVecEnv([mazeexplorer_env.create_env() for _ in range(num_cpus)]) # vectorise env 132 | 133 | if n_stack > 0: 134 | env = VecFrameStack(env, n_stack=n_stack) 135 | 136 | if algorithm == 'ppo': 137 | algo = PPO2 138 | elif algorithm == 'a2c': 139 | algo = A2C 140 | else: 141 | raise NotImplementedError("Only supports PPO and A2C") 142 | 143 | if lstm: 144 | model = algo(CnnLstmPolicy, env, verbose=1, tensorboard_log=OUTPUT_PATH) 145 | else: 146 | model = algo(CnnPolicy, env, verbose=1, tensorboard_log=OUTPUT_PATH) 147 | 148 | if eval: 149 | evaluator = Evaluator(os.path.join(DIR_PATH, "eval_maps"), OUTPUT_PATH, num_cpus, mp, n_stack) 150 | 151 | steps_taken = 0 152 | 153 | print("Training started...") 154 | 155 | while steps_taken < steps: 156 | print("Training...") 157 | model.learn(total_timesteps=min(eval_occurrence, (steps - steps_taken)), reset_num_timesteps=False, 158 | seed=alg_seed) 159 | 160 | steps_taken += eval_occurrence 161 | 162 | print("Evaluating...") 163 | 164 | evaluator.evaluate(model, steps_taken, eval_episodes, save=True) # do 100 rollouts and save scores 165 | 166 | print("Training completed.") 167 | 168 | else: 169 | model.learn(total_timesteps=steps, seed=alg_seed) 170 | 171 | 172 | if __name__ == "__main__": 173 | args = parser.parse_args() 174 | 175 | os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu_id) 176 | 177 | stable_baseline_training(algorithm=args.algorithm, steps=args.steps, 178 | number_maps=args.number_maps, random_spawn=bool(args.random_spawn), 179 | random_textures=bool(args.random_textures), lstm=bool(args.lstm), 180 | random_keys=bool(args.random_keys), 181 | keys=args.keys, dimensions=(args.x, args.y), num_cpus=args.cpu, n_stack=args.n_stack, 182 | clip=bool(args.clip), mp=bool(args.mp), eval_occurrence=args.eval_occurrence, 183 | eval_episodes=args.eval_episodes, eval=bool(args.eval), 184 | experiment_name=args.experiment_name, 185 | env_seed=args.env_seed, alg_seed=args.alg_seed, complexity=args.complexity, 186 | density=args.density, 187 | episode_timeout=args.episode_timeout) 188 | -------------------------------------------------------------------------------- /baseline_experiments/experiments.md: -------------------------------------------------------------------------------- 1 | # Baseline experiments 2 | 3 | 1. python experiment.py --number_maps 1 --seed 42 --algorithm ppo 4 | 1. python experiment.py --number_maps 10 --seed 42 --algorithm ppo 5 | 1. python experiment.py --number_maps 100 --seed 42 --algorithm ppo 6 | 1. python experiment.py --number_maps 1 --seed 42 --algorithm ppo --random_spawn 1 7 | 1. python experiment.py --number_maps 10 --seed 42 --algorithm ppo --random_spawn 1 8 | 1. python experiment.py --number_maps 100 --seed 42 --algorithm ppo --random_spawn 1 9 | 1. python experiment.py --number_maps = 1 --seed 42 --algorithm a2c 10 | 1. python experiment.py --number_maps = 10 --seed 42 --algorithm a2c 11 | 1. python experiment.py --number_maps = 100 --seed 42 --algorithm a2c 12 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License 3 | 4 | from mazeexplorer import MazeExplorer 5 | 6 | env_train = MazeExplorer(number_maps=10, keys=9, size=(10, 10), random_spawn=True, random_textures=True, seed=42) 7 | env_test = MazeExplorer(number_maps=10, keys=9, size=(10, 10), random_spawn=True, random_textures=True, seed=43) 8 | -------------------------------------------------------------------------------- /mazeexplorer/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License 3 | 4 | name = "mazeexplorer" 5 | 6 | from .mazeexplorer import MazeExplorer 7 | from .vizdoom_gym import VizDoom 8 | -------------------------------------------------------------------------------- /mazeexplorer/compile_acs.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License 3 | 4 | # This script uses acc (from https://github.com/rheit/acc) to compile the acs scripts. 5 | import os 6 | import subprocess 7 | 8 | dir_path = os.path.dirname(os.path.realpath(__file__)) 9 | 10 | 11 | def compile_acs(mazes_path): 12 | os.makedirs(os.path.join(mazes_path, "outputs", "sources")) 13 | os.makedirs(os.path.join(mazes_path, "outputs", "images")) 14 | 15 | acc_path = os.path.join(dir_path, "acc/acc") 16 | 17 | if not os.path.isfile(acc_path): 18 | print("Compiling ACC as File not does exist: ", acc_path, "") 19 | subprocess.call(["make", "-C", os.path.join(dir_path, "acc")]) 20 | 21 | maze_acs_path = os.path.join(dir_path, "content", "maze.acs") 22 | output_file_path = os.path.join(mazes_path, "outputs", "maze.o") 23 | subprocess.call([acc_path, "-i", "./acc", maze_acs_path, output_file_path]) 24 | -------------------------------------------------------------------------------- /mazeexplorer/config_template.txt: -------------------------------------------------------------------------------- 1 | available_buttons = {$actions} 2 | doom_scenario_path = $mission_wad 3 | 4 | screen_resolution = RES_160X120 5 | screen_format = CRCGCB 6 | depth_buffer_enabled = true 7 | labels_buffer_enabled = false 8 | automap_buffer_enabled = false 9 | render_hud = false 10 | render_minimal_hud = false 11 | render_crosshair = false 12 | render_weapon = false 13 | render_decals = false 14 | render_particles = false 15 | render_effects_sprites = false 16 | episode_timeout = $episode_timeout 17 | window_visible = false 18 | available_game_variables = {POSITION_X POSITION_Y POSITION_Z VELOCITY_X VELOCITY_Y VELOCITY_Z ANGLE} 19 | game_args += +cl_run 1 20 | game_args += +vid_aspect 3 21 | game_args += +cl_spreaddecals 0 22 | game_args += +hud_scale 1 23 | game_args += +cl_bloodtype 2 24 | game_args += +cl_pufftype 1 25 | game_args += +cl_missiledecals 0 26 | game_args += +cl_bloodsplats 0 27 | game_args += +cl_showmultikills 0 28 | game_args += +cl_showsprees 0 29 | game_args += +con_midtime 0 30 | game_args += +con_notifytime 0 31 | game_args += +am_textured 1 32 | game_args += +am_showtime 0 33 | game_args += +am_showmonsters 0 34 | game_args += +am_showsecrets 0 35 | -------------------------------------------------------------------------------- /mazeexplorer/content/acs_template.txt: -------------------------------------------------------------------------------- 1 | #include "zcommon.acs" 2 | 3 | #define TARGET_ID_START 1000 4 | #define GOAL_TID 999 5 | 6 | int TARGET_ID_END = TARGET_ID_START; 7 | 8 | int target_id = 10; 9 | 10 | global int 0:reward; 11 | global int 1:goal_x; 12 | global int 2:goal_y; 13 | global int 3:goal_z; 14 | global int 4:map_level; 15 | global int 5:reward_check; 16 | 17 | int number_keys = $number_keys; 18 | bool random_spawn = $random_spawn; 19 | bool random_textures = $random_textures; 20 | bool random_keys = $random_key_positions; 21 | int xmin = $xmin; 22 | int ymin = $ymin; 23 | int xmax = $xmax; 24 | int ymax = $ymax; 25 | int offset = 48.0; 26 | 27 | str floor_texture = "$floor_texture"; 28 | str ceilling_texture = "$ceilling_texture"; 29 | str wall_texture = "$wall_texture"; 30 | 31 | int SPAWN_LOC_ID = 0; 32 | 33 | int keys_spawn[ $number_keys_maps ] = { $keys_spawn }; 34 | 35 | int keys_spawn_offset_x[ $number_keys_maps ] = { $keys_spawn_offset_x }; 36 | int keys_spawn_offset_y[ $number_keys_maps ] = { $keys_spawn_offset_y }; 37 | 38 | int spawns[ $number_maps ] = { $spawns }; 39 | int spawns_offset_x[ $number_maps ] = { $spawns_offset_x }; 40 | int spawns_offset_y[ $number_maps ] = { $spawns_offset_y }; 41 | int spawns_angle[ $number_maps ] = { $spawns_angle }; 42 | 43 | str texturesToRandomize[246] = $textures 44 | 45 | function str GetRandomTexture(void) 46 | { 47 | return texturesToRandomize[Random(0, 245)]; 48 | } 49 | 50 | function void RandomTextures(void) 51 | { 52 | ReplaceTextures("CEIL5_2", GetRandomTexture()); 53 | ReplaceTextures("CEIL5_1", GetRandomTexture()); 54 | ReplaceTextures("STONE2", GetRandomTexture()); 55 | Light_ChangeToValue(0, Random(150, 255)); 56 | } 57 | 58 | function void SpawnKeyRandom(void) 59 | { 60 | TARGET_ID_END = TARGET_ID_START; 61 | while(IsTIDUsed(TARGET_ID_END + 1)) 62 | { 63 | TARGET_ID_END += 1; 64 | } 65 | 66 | SPAWN_LOC_ID = random(TARGET_ID_START, TARGET_ID_END); 67 | 68 | Spawn("RedCard", GetActorX(SPAWN_LOC_ID) + random(-offset, offset), GetActorY(SPAWN_LOC_ID) + random(-offset, offset),0.0,target_id,128); 69 | SetThingSpecial(target_id, ACS_ExecuteAlways, 5); 70 | } 71 | 72 | function void SpawnKey(int i) 73 | { 74 | TARGET_ID_END = TARGET_ID_START; 75 | while(IsTIDUsed(TARGET_ID_END + 1)) 76 | { 77 | TARGET_ID_END += 1; 78 | } 79 | 80 | int TARGET_ID_START_float = TARGET_ID_START << 16; 81 | int TARGET_ID_END_float = TARGET_ID_END << 16; 82 | 83 | int SPAWN_LOC_ID_float = FixedMul (keys_spawn[i], (TARGET_ID_END_float - TARGET_ID_START_float)) + TARGET_ID_START_float; 84 | SPAWN_LOC_ID = SPAWN_LOC_ID_float >> 16; 85 | 86 | Spawn("RedCard", GetActorX(SPAWN_LOC_ID) + keys_spawn_offset_x[i], GetActorY(SPAWN_LOC_ID) + keys_spawn_offset_y[i], 0.0, target_id, 128); 87 | SetThingSpecial(target_id, ACS_ExecuteAlways, 5); 88 | } 89 | 90 | script 1 OPEN 91 | { 92 | 93 | } 94 | 95 | script 2 ENTER 96 | { 97 | map_level = GetLevelInfo (LEVELINFO_LEVELNUM); 98 | if (random_keys) 99 | { 100 | for (int i=0; i> 16; 153 | 154 | SetActorPosition(0, GetActorX(SPAWN_LOC_ID) + spawns_offset_x[map_level], GetActorY(SPAWN_LOC_ID) + spawns_offset_y[map_level], 0.0, 0); 155 | SetActorAngle(0, spawns_angle[map_level]); 156 | } 157 | } 158 | 159 | script 5 (void) 160 | { 161 | reward = reward + 1.0; 162 | reward_check = reward_check + 1; 163 | if (reward_check == number_keys) 164 | { 165 | Exit_Normal(0); 166 | restart; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /mazeexplorer/content/doom_textures.txt: -------------------------------------------------------------------------------- 1 | {"ASHWALL2", "ASHWALL3", "ASHWALL4", "ASHWALL6", "ASHWALL7", "BFALL1", "BFALL2", "BFALL3", "BFALL4", "BIGBRIK1", "BIGBRIK2", "BIGBRIK3", "BIGDOOR2", "BIGDOOR3", "BIGDOOR4", "BIGDOOR5", "BLAKWAL1", "BLAKWAL2", "BRICK1", "BRICK2", "BRICK3", "BRICK4", "BRICK5", "BRICK6", "BRICK7", "BRICK8", "BRICK9", "BRICK10", "BRICK11", "BRICK12", "BRICKLIT", "BRONZE1", "BRONZE2", "BRONZE3", "BRONZE4", "BROVINE2", "BROWN1", "BROWN144", "BROWN96", "BROWNGRN", "BROWNHUG", "BROWNPIP", "BRWINDOW", "BSTONE1", "BSTONE2", "BSTONE3", "CEMENT1", "CEMENT2", "CEMENT3", "CEMENT4", "CEMENT5", "CEMENT6", "CEMENT7", "CEMENT9", "COMPBLUE", "COMPSPAN", "COMPSTA1", "COMPSTA2", "COMPTALL", "COMPWERD", "CRACKLE2", "CRACKLE4", "CRATE1", "CRATE2", "CRATE3", "CRATELIT", "CRATWIDE", "DBRAIN1", "DBRAIN2", "DBRAIN3", "DOORBLU", "DOORRED", "DOORSTOP", "DOORTRAK", "DOORYEL", "FIREWALA", "FIREWALB", "FIREWALL", "GRAY1", "GRAY2", "GRAY4", "GRAY5", "GRAYBIG", "GRAYVINE", "GSTONE1", "GSTONE2", "GSTVINE1", "GSTVINE2", "ICKWALL1", "ICKWALL2", "ICKWALL3", "LITE3", "LITE5", "LITEBLU1", "LITEBLU4", "MARBGRAY", "MARBLE1", "MARBLE2", "MARBLE3", "MARBLOD1", "METAL", "METAL1", "METAL2", "METAL3", "METAL4", "METAL5", "METAL6", "METAL7", "MODWALL1", "MODWALL2", "MODWALL4", "NUKE24", "NUKEDGE1", "PANBOOK", "PANBORD1", "PANBORD2", "PANCASE1", "PANCASE2", "PANEL1", "PANEL2", "PANEL4", "PANEL5", "PANEL6", "PANEL7", "PANEL8", "PANEL9", "PIPE1", "PIPE2", "PIPE4", "PIPE6", "PIPEWAL1", "PIPEWAL2", "PLAT1", "REDWALL", "ROCK1", "ROCK2", "ROCK3", "ROCK4", "ROCK5", "ROCKRED1", "ROCKRED2", "SFALL1", "SFALL2", "SFALL3", "SFALL4", "SHAWN2", "SILVER1", "SILVER2", "SILVER3", "SK_LEFT", "SK_RIGHT", "SKIN2", "SLADWALL", "SP_HOT1", "SPACEW2", "SPACEW3", "SPACEW4", "SPCDOOR1", "SPCDOOR2", "SPCDOOR3", "SPCDOOR4", "STARBR2", "STARG1", "STARG2", "STARG3", "STARGR1", "STARGR2", "STARTAN2", "STARTAN3", "STEPLAD1", "STEPTOP", "STONE", "STONE2", "STONE3", "STONE4", "STONE5", "STONE6", "STONE7", "STUCCO", "STUCCO1", "SUPPORT2", "SUPPORT3", "TANROCK2", "TANROCK3", "TANROCK4", "TANROCK5", "TANROCK7", "TANROCK8", "TEKBRON1", "TEKBRON2", "TEKGREN1", "TEKGREN2", "TEKGREN3", "TEKGREN4", "TEKGREN5", "TEKLITE", "TEKLITE2", "TEKWALL1", "TEKWALL4", "TEKWALL6", "WOOD1", "WOOD3", "WOOD5", "WOOD6", "WOOD7", "WOOD8", "WOOD9", "WOOD12", "WOODMET1", "WOODVERT", "ZDOORB1", "ZDOORF1", "ZELDOOR", "ZIMMER2", "ZIMMER5", "ZIMMER7", "ZIMMER8", "ZZWOLF1", "ZZWOLF5", "ZZWOLF9", "ZZWOLF10", "ZZWOLF11", "ZZZFACE6", "ZZZFACE7", "ZZZFACE8", "ZZZFACE9"}; -------------------------------------------------------------------------------- /mazeexplorer/maze.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License 3 | 4 | from __future__ import print_function 5 | 6 | import os 7 | import random 8 | 9 | import numpy as np 10 | 11 | WALL_TYPE = np.int8 12 | WALL = 0 13 | EMPTY = 1 14 | 15 | 16 | class Maze: 17 | def __init__(self, rows, columns): 18 | if rows < 1 or columns < 1: 19 | raise ValueError("Invalid number rows or columns, must be greater than 1.") 20 | 21 | self.nrows = rows 22 | self.ncolumns = columns 23 | self.board = np.zeros((rows, columns), dtype=WALL_TYPE) 24 | self.board.fill(EMPTY) 25 | 26 | def __str__(self): 27 | return os.linesep.join(''.join('X' if self.is_wall(i, j) else ' ' 28 | for j in range(self.ncolumns)) 29 | for i in range(self.nrows)) 30 | 31 | def __hash__(self): 32 | return hash(self.board.tostring()) 33 | 34 | def __eq__(self, other): 35 | return np.array_equal(self.board, other.board) 36 | 37 | def set_borders(self): 38 | self.board[0, :] = self.board[-1, :] = WALL 39 | self.board[:, 0] = self.board[:, -1] = WALL 40 | 41 | def is_wall(self, x, y): 42 | assert self.in_maze(x, y) 43 | return self.board[x][y] == WALL 44 | 45 | def set_wall(self, x, y): 46 | assert self.in_maze(x, y) 47 | self.board[x][y] = WALL 48 | 49 | def remove_wall(self, x, y): 50 | assert self.in_maze(x, y) 51 | self.board[x][y] = EMPTY 52 | 53 | def in_maze(self, x, y): 54 | return 0 <= x < self.nrows and 0 <= y < self.ncolumns 55 | 56 | def write_to_file(self, filename): 57 | f = open(filename, 'w') 58 | f.write(str(self)) 59 | f.close() 60 | 61 | @staticmethod 62 | def create_maze(rows, columns, seed=None, complexity=.7, density=.8): 63 | rows = (rows // 2) * 2 + 1 64 | columns = (columns // 2) * 2 + 1 65 | 66 | if seed is not None: 67 | np.random.seed(seed) 68 | 69 | # Adjust complexity and density relative to maze size 70 | complexity = int(complexity * (5 * (rows + columns))) 71 | density = int(density * ((rows // 2) * (columns // 2))) 72 | 73 | maze = Maze(rows, columns) 74 | maze.set_borders() 75 | 76 | # Make aisles 77 | for i in range(density): 78 | x = np.random.random_integers(0, rows // 2) * 2 79 | y = np.random.random_integers(0, columns // 2) * 2 80 | maze.set_wall(x, y) 81 | 82 | for j in range(complexity): 83 | neighbours = [] 84 | 85 | if maze.in_maze(x - 2, y): 86 | neighbours.append((x - 2, y)) 87 | 88 | if maze.in_maze(x + 2, y): 89 | neighbours.append((x + 2, y)) 90 | 91 | if maze.in_maze(x, y - 2): 92 | neighbours.append((x, y - 2)) 93 | 94 | if maze.in_maze(x, y + 2): 95 | neighbours.append((x, y + 2)) 96 | 97 | if len(neighbours): 98 | next_x, next_y = neighbours[np.random.randint( 99 | 0, 100 | len(neighbours) - 1)] 101 | 102 | if not maze.is_wall(next_x, next_y): 103 | maze.set_wall(next_x, next_y) 104 | maze.set_wall(next_x + (x - next_x) // 2, 105 | next_y + (y - next_y) // 2) 106 | x, y = next_x, next_y 107 | 108 | return maze 109 | 110 | 111 | def generate_mazes(maze_id, num, rows=9, columns=9, seed=None, complexity=.7, density=.7): 112 | """ 113 | args: 114 | 115 | maze_id: 116 | num: (int) number of maps to generate (default: 10) 117 | rows: (int) maps row size (default: 9) 118 | columns: (int) maps column size (default: 9) 119 | seed: seed of the maze to generate 120 | """ 121 | counter = 0 122 | mazes = set() 123 | 124 | if seed: 125 | random.seed(seed) 126 | 127 | while len(mazes) < num: 128 | if counter > 5: 129 | raise ValueError('Unable to create the desired number of unique maps') 130 | 131 | map_seed = random.randint(0, 2147483647) 132 | 133 | maze = Maze.create_maze(columns + 1, rows + 1, map_seed, complexity=complexity, density=density) 134 | 135 | if maze in mazes: 136 | counter += 1 137 | else: 138 | counter = 0 139 | mazes.add(maze) 140 | 141 | for idx, maze in enumerate(sorted(mazes, key=lambda x: hash(x))): 142 | # prefix = 'TRAIN' if idx in train_indices else 'TEST' 143 | 144 | maze.write_to_file("{}_MAP{:02d}.txt".format( 145 | maze_id, idx + 1)) 146 | 147 | return mazes 148 | -------------------------------------------------------------------------------- /mazeexplorer/mazeexplorer.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License 3 | 4 | import datetime 5 | import os 6 | import shutil 7 | import tempfile 8 | from pathlib import Path 9 | 10 | import numpy as np 11 | 12 | import cv2 13 | 14 | from .maze import generate_mazes 15 | from .script_manipulator import write_config, write_acs 16 | from .vizdoom_gym import VizDoom 17 | from .wad import generate_wads 18 | from .compile_acs import compile_acs 19 | 20 | dir_path = os.path.dirname(os.path.realpath(__file__)) 21 | 22 | 23 | class MazeExplorer(VizDoom): 24 | def __init__(self, unique_maps=False, number_maps=1, keys=9, size=(10, 10), random_spawn=False, random_textures=False, 25 | random_key_positions=False, seed=None, clip=(-1, 1), 26 | floor_texture="CEIL5_2", ceilling_texture="CEIL5_1", wall_texture="STONE2", 27 | action_frame_repeat=4, actions="MOVE_FORWARD TURN_LEFT TURN_RIGHT", scaled_resolution=(42, 42), 28 | episode_timeout=1500, complexity=.7, density=.7, data_augmentation=False, mazes_path=None): 29 | """ 30 | Gym environment where the goal is to collect a preset number of keys within a procedurally generated maze. 31 | 32 | MazeExplorer is a customisable 3D benchmark for assessing generalisation in Reinforcement Learning. 33 | 34 | :params unique_maps: if set, every map will only be seen once. cfg files will be recreated after all its maps have been seen. 35 | :param number_maps: number of maps which are contained within the cfg file. If unique maps is set, this acts like a cache of maps 36 | :param keys: number of keys which need to be collected for each episode 37 | :param size: the size of generated mazes in the format (width, height) 38 | :param random_spawn: whether to randomise the spawn each time the environment is reset 39 | :param random_textures: whether to randomise the textures for each map each time the environment is reset 40 | :param random_key_positions: whether to randomise the position of the keys each time the environment is reset 41 | :param seed: seed for random, used to determine the other that the doom maps should be shown. 42 | :param clip: how much the reward returned on each step should be clipped to 43 | :param floor_texture: the texture to use for the floor, options are in mazeexplorer/content/doom_textures.acs 44 | Only used when random_textures=False 45 | :param ceilling_texture: the texture to use for the ceiling, options are in mazeexplorer/content/doom_textures.acs 46 | Only used when random_textures=False 47 | :param wall_texture: the texture to use for the walls, options are in mazeexplorer/content/doom_textures.acs 48 | Only used when random_textures=False 49 | :param action_frame_repeat: how many game tics should an action be active 50 | :param actions: the actions which can be performed by the agent 51 | :param scaled_resolution: resolution (height, width) of the observation to be returned with each step 52 | :param episode_timeout: the number of ticks in the environment before it time's out 53 | :param complexity: float between 0 and 1 describing the complexity of the generated mazes 54 | :param density: float between 0 and 1 describing the density of the generated mazes 55 | :param data_augmentation: bool to determine whether or not to use data augmentation 56 | (adding randomly colored, randomly sized boxes to observation) 57 | :type mazes_path: path to where to save the mazes 58 | """ 59 | self.unique_maps = unique_maps 60 | self.number_maps = number_maps 61 | self.keys = keys 62 | self.size = size 63 | self.random_spawn = random_spawn 64 | self.random_textures = random_textures 65 | self.random_key_positions = random_key_positions 66 | self.seed = seed 67 | self.clip = clip 68 | self.actions = actions 69 | self.mazes = None 70 | self.action_frame_repeat = action_frame_repeat 71 | self.scaled_resolution = scaled_resolution 72 | self.episode_timeout = episode_timeout 73 | self.complexity = complexity 74 | self.density = density 75 | self.data_augmentation = data_augmentation 76 | 77 | # The mazeexplorer textures to use if random textures is set to False 78 | self.wall_texture = wall_texture 79 | self.floor_texture = floor_texture 80 | self.ceilling_texture = ceilling_texture 81 | 82 | self.mazes_path = mazes_path if mazes_path is not None else tempfile.mkdtemp() 83 | # create new maps and corresponding config 84 | shutil.rmtree(self.mazes_path, ignore_errors=True) 85 | os.mkdir(self.mazes_path) 86 | 87 | self.cfg_path = self.generate_mazes() 88 | 89 | # start map with -1 since it will always be reseted one time. 90 | self.current_map = -1 91 | super().__init__(self.cfg_path, number_maps=self.number_maps, scaled_resolution=self.scaled_resolution, 92 | action_frame_repeat=self.action_frame_repeat, seed=seed, 93 | data_augmentation=self.data_augmentation) 94 | 95 | def generate_mazes(self): 96 | """ 97 | Generate the maze cfgs and wads and place them in self.mazes_path 98 | 99 | :return: path to the maze_cfg 100 | """ 101 | # edit base acs template to reflect user specification 102 | write_acs(self.keys, self.random_spawn, self.random_textures, self.random_key_positions, map_size=self.size, 103 | number_maps=self.number_maps, floor_texture=self.floor_texture, 104 | ceilling_texture=self.ceilling_texture, 105 | wall_texture=self.wall_texture, seed=self.seed) 106 | 107 | compile_acs(self.mazes_path) 108 | 109 | # generate .txt maze files 110 | self.mazes = generate_mazes(self.mazes_path + "/" + str(self.size[0]) + "x" + str(self.size[1]), 111 | self.number_maps, self.size[0], 112 | self.size[1], 113 | seed=self.seed, complexity=self.complexity, density=self.density) 114 | 115 | outputs = os.path.join(self.mazes_path, "outputs/") 116 | 117 | # convert .txt mazes to wads and link acs scripts 118 | try: 119 | generate_wads(self.mazes_path + "/" + str(self.size[0]) + "x" + str(self.size[1]), 120 | self.mazes_path + "/" + str(self.size[0]) + "x" + str(self.size[1]) + ".wad", 121 | outputs + "maze.o") 122 | except FileNotFoundError as e: 123 | raise FileNotFoundError(e.strerror + "\n" 124 | "Have you pulled the required submodules?\n" 125 | "If not, use the line:\n\n\t" 126 | "git submodule update --init --recursive") 127 | cfg = write_config(self.mazes_path + "/" + str(self.size[0]) + "x" + str(self.size[1]), 128 | self.actions, episode_timeout=self.episode_timeout) 129 | 130 | return cfg 131 | 132 | def reset(self): 133 | """Resets environment to start a new mission. 134 | 135 | If `unique_maps` is set and and all cached maps have been seen, it wil also generate 136 | new maps using the ACC script. Otherwise if there is more than one maze 137 | it will randomly select a new maze for the list. 138 | 139 | :return: initial observation of the environment as an rgb array in the format (rows, columns, channels) """ 140 | if not self.unique_maps: 141 | return super().reset() 142 | else: 143 | self.current_map += 1 144 | if self.current_map > self.number_maps: 145 | print("Generating new maps") 146 | 147 | if self.seed is not None: 148 | np.random.seed(self.seed) 149 | 150 | self.seed = np.random.randint(np.iinfo(np.int32).max) 151 | 152 | # create new maps and corresponding config 153 | shutil.rmtree(self.mazes_path) 154 | 155 | os.mkdir(self.mazes_path) 156 | self.cfg_path = self.generate_mazes() 157 | 158 | # reset the underlying DoomGame class 159 | self.env.load_config(self.cfg_path) 160 | self.env.init() 161 | self.current_map = 0 162 | 163 | self.doom_map = "map" + str(self.current_map).zfill(2) 164 | self.env.set_doom_map(self.doom_map) 165 | self.env.new_episode() 166 | self._rgb_array = self.env.get_state().screen_buffer 167 | observation = self._process_image() 168 | return observation 169 | 170 | 171 | 172 | def save(self, destination_dir): 173 | """ 174 | Save the maze files to a directory 175 | :param destination_dir: the path of where to save the maze files 176 | """ 177 | shutil.copytree(self.mazes_path, destination_dir) 178 | 179 | @staticmethod 180 | def load_vizdoom_env(mazes_path, number_maps, action_frame_repeat=4, scaled_resolution=(42, 42)): 181 | """ 182 | Takes the path to a maze cfg or a folder of mazes created by mazeexplorer.save() and returns a vizdoom environment 183 | using those mazes 184 | :param mazes_path: path to a .cfg file or a folder containg the cfg file 185 | :param number_maps: number of maps contained within the wad file 186 | :param action_frame_repeat: how many game tics should an action be active 187 | :param scaled_resolution: resolution (height, width) of the observation to be returned with each step 188 | :return: VizDoom gym env 189 | """ 190 | if str(mazes_path).endswith(".cfg"): 191 | return VizDoom(mazes_path, number_maps=number_maps, scaled_resolution=scaled_resolution, 192 | action_frame_repeat=action_frame_repeat) 193 | else: 194 | cfg_paths = list(Path(mazes_path).glob("*.cfg")) 195 | if len(cfg_paths) != 1: 196 | raise ValueError("Invalid number of cfgs within the mazes path: ", len(cfg_paths)) 197 | return VizDoom(cfg_paths[0], number_maps=number_maps, scaled_resolution=scaled_resolution, 198 | action_frame_repeat=action_frame_repeat) 199 | 200 | @staticmethod 201 | def generate_video(images_path, movie_path): 202 | """ 203 | Generates a video of the agent from the images saved using record_path 204 | 205 | Example: 206 | ```python 207 | images_path = "path/to/save_images_dir" 208 | movie_path = "path/to/movie.ai" 209 | 210 | env = MazeNavigator(record_path=images_path) 211 | env.reset() 212 | for _ in range(100): 213 | env.step_record(env.action_space.sample(), record_path=images_path) 214 | MazeNavigator.generate_video(images_path, movie_path) 215 | ``` 216 | 217 | :param images_path: path of the folder containg the generated images 218 | :param movie_path: file path ending with .avi to where the movie should be outputted to. 219 | """ 220 | 221 | if not movie_path.endswith(".avi"): 222 | raise ValueError("movie_path must end with .avi") 223 | 224 | images = sorted([img for img in os.listdir(images_path) if img.endswith(".png")]) 225 | 226 | if not len(images): 227 | raise FileNotFoundError("Not png images found within the images path") 228 | 229 | frame = cv2.imread(os.path.join(images_path, images[0])) 230 | height, width, _ = frame.shape 231 | 232 | video = cv2.VideoWriter(movie_path, 0, 30, (width, height)) 233 | 234 | for image in images: 235 | video.write(cv2.imread(os.path.join(images_path, image))) 236 | 237 | cv2.destroyAllWindows() 238 | video.release() 239 | 240 | -------------------------------------------------------------------------------- /mazeexplorer/script_manipulator.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License 3 | 4 | from string import Template 5 | import os 6 | import random 7 | 8 | dir_path = os.path.dirname(os.path.realpath(__file__)) 9 | 10 | 11 | def write_config(wad, actions, episode_timeout): 12 | """ 13 | args: 14 | 15 | wad: (str) name of corresponding wad file 16 | actions: (str) list of available actions (default: "MOVE_FORWARD TURN_LEFT TURN_RIGHT") 17 | """ 18 | # open the file 19 | filein = open(os.path.join(dir_path, 'config_template.txt')) 20 | # read it 21 | src = Template(filein.read()) 22 | 23 | mission_wad = os.path.splitext(os.path.basename(wad))[0] 24 | d = {'actions': actions, 'mission_wad': mission_wad, 'episode_timeout': episode_timeout} 25 | 26 | # do the substitution 27 | result = src.substitute(d) 28 | 29 | f = open(wad + ".cfg", "w+") 30 | f.write(result) 31 | f.close() 32 | 33 | return wad + ".cfg" 34 | 35 | 36 | def write_acs(keys, random_spawn, random_textures, random_key_positions, map_size, number_maps, 37 | floor_texture, ceilling_texture, wall_texture, seed=None): 38 | """ 39 | args: 40 | 41 | keys: (int) number of keys to place in maze 42 | random_spawn: (bool) whether or not agent should be randomly placed in maze at spawn time 43 | random_textures: (bool) whether or not textures (walls, floors etc.) should be randomised. 44 | """ 45 | 46 | BLOCK_SIZE = 96 47 | 48 | xmin = BLOCK_SIZE / 2 49 | ymin = BLOCK_SIZE / 2 50 | xmax = BLOCK_SIZE / 2 + 2 * round(map_size[0] / 2) * BLOCK_SIZE 51 | ymax = BLOCK_SIZE / 2 + 2 * round(map_size[1] / 2) * BLOCK_SIZE 52 | 53 | if seed: 54 | random.seed(seed) 55 | 56 | maze_acs = os.path.join(dir_path, "content/maze.acs") 57 | if os.path.exists(maze_acs): 58 | os.remove(maze_acs) 59 | 60 | # open the file 61 | filein = open(os.path.join(dir_path, 'content/acs_template.txt')) 62 | # read it 63 | src = Template(filein.read()) 64 | 65 | with open(os.path.join(dir_path, 'content/doom_textures.txt')) as textures: 66 | doom_textures = textures.read().replace(';\n', '') 67 | 68 | number_key_positions = keys * number_maps 69 | 70 | offset = 48 71 | 72 | keys_spawn = ", ".join(['%.4f' % (random.random()) for _ in range(number_key_positions)]) 73 | 74 | keys_spawn_offset_x = ", ".join(['%.3f' % (random.randint(-offset, offset)) for _ in range(number_key_positions)]) 75 | keys_spawn_offset_y = ", ".join(['%.3f' % (random.randint(-offset, offset)) for _ in range(number_key_positions)]) 76 | 77 | spawns = ", ".join(['%.4f' % (random.random()) for _ in range(number_maps)]) 78 | spawns_offset_x = ", ".join(['%.3f' % (random.randint(-offset, offset)) for _ in range(number_maps)]) 79 | spawns_offset_y = ", ".join(['%.3f' % (random.randint(-offset, offset)) for _ in range(number_maps)]) 80 | spawns_angle = ", ".join(['%.2f' % (random.random()) for _ in range(number_maps)]) 81 | 82 | d = {'number_keys': keys, 'random_spawn': random_spawn, 'random_textures': random_textures, 83 | 'textures': doom_textures, 'xmin': xmin, 'ymin': ymin, 'xmax': xmax, 'ymax': ymax, 84 | 'floor_texture': floor_texture, 'ceilling_texture': ceilling_texture, 'wall_texture': wall_texture, 85 | 'random_key_positions': random_key_positions, 86 | 'number_key_positions': number_key_positions, 'spawns': spawns, 'number_maps': number_maps, 87 | 'spawns_offset_x': spawns_offset_x, 'spawns_offset_y': spawns_offset_y, 'spawns_angle': spawns_angle, 88 | "keys_spawn": keys_spawn, "keys_spawn_offset_x": keys_spawn_offset_x, 89 | "keys_spawn_offset_y": keys_spawn_offset_y, 'number_keys_maps': number_key_positions 90 | } 91 | 92 | # do the substitution 93 | result = src.substitute(d) 94 | 95 | f = open(os.path.join(dir_path, "content/maze.acs"), "w+") 96 | f.write(result) 97 | f.close() 98 | -------------------------------------------------------------------------------- /mazeexplorer/vizdoom_gym.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License 3 | 4 | """Module containing VizDoom environment wrapper.""" 5 | 6 | import logging 7 | import os 8 | import datetime 9 | 10 | import gym 11 | import gym.spaces 12 | import numpy as np 13 | import random 14 | import imageio 15 | 16 | from vizdoom import DoomGame, Button 17 | 18 | try: 19 | import cv2 20 | 21 | OPENCV_AVAILABLE = True 22 | logging.info('OpenCV found, setting as default backend.') 23 | except ImportError: 24 | pass 25 | 26 | try: 27 | import PIL 28 | 29 | PILLOW_AVAILABLE = True 30 | 31 | if not OPENCV_AVAILABLE: 32 | logging.info('Pillow found, setting as default backend.') 33 | except ImportError: 34 | pass 35 | 36 | 37 | class VizDoom(gym.Env): 38 | """ 39 | Wraps a VizDoom environment 40 | """ 41 | 42 | def __init__(self, cfg_path, number_maps, scaled_resolution=(42, 42), action_frame_repeat=4, clip=(-1, 1), 43 | seed=None, data_augmentation=False): 44 | """ 45 | Gym environment for training reinforcement learning agents. 46 | 47 | :param cfg_path: name of the mission (.cfg) to run 48 | :param number_maps: number of maps which are contained within the cfg file 49 | :param scaled_resolution: resolution (height, width) of the observation to be returned with each step 50 | :param action_frame_repeat: how many game tics should an action be active 51 | :param clip: how much the reward returned on each step should be clipped to 52 | :param seed: seed for random, used to determine the other that the doom maps should be shown. 53 | :param data_augmentation: bool to determine whether or not to use data augmentation 54 | (adding randomly colored, randomly sized boxes to observation) 55 | """ 56 | 57 | self.cfg_path = str(cfg_path) 58 | if not os.path.exists(self.cfg_path): 59 | raise ValueError("Cfg file not found", cfg_path) 60 | 61 | if not self.cfg_path.endswith('.cfg'): 62 | raise ValueError("cfg_path must end with .cfg") 63 | 64 | self.number_maps = number_maps 65 | self.scaled_resolution = scaled_resolution 66 | self.action_frame_repeat = action_frame_repeat 67 | self.clip = clip 68 | self.data_augmentation = data_augmentation 69 | 70 | if seed: 71 | random.seed(seed) 72 | 73 | super(VizDoom, self).__init__() 74 | self._logger = logging.getLogger(__name__) 75 | self._logger.info("Creating environment: VizDoom (%s)", self.cfg_path) 76 | 77 | # Create an instace on VizDoom game, initalise it from a scenario config file 78 | self.env = DoomGame() 79 | self.env.load_config(self.cfg_path) 80 | self.env.init() 81 | 82 | # Perform config validation: 83 | # Only RGB format with a seperate channel per colour is supported 84 | # assert self.env.get_screen_format() == ScreenFormat.RGB24 85 | # Only discreete actions are supported (no delta actions) 86 | available_actions = self.env.get_available_buttons() 87 | not_supported_actions = [Button.LOOK_UP_DOWN_DELTA, Button.TURN_LEFT_RIGHT_DELTA, 88 | Button.MOVE_LEFT_RIGHT_DELTA, Button.MOVE_UP_DOWN_DELTA, 89 | Button.MOVE_FORWARD_BACKWARD_DELTA] 90 | assert len((set(available_actions) - set(not_supported_actions))) == len(available_actions) 91 | 92 | # Allow only one button to be pressed at a given step 93 | self.action_space = gym.spaces.Discrete(self.env.get_available_buttons_size()) 94 | 95 | rows = scaled_resolution[1] 96 | columns = scaled_resolution[0] 97 | self.observation_space = gym.spaces.Box(0.0, 98 | 255.0, 99 | shape=(columns, rows, 3), 100 | dtype=np.float32) 101 | self._rgb_array = None 102 | self.reset() 103 | 104 | def _process_image(self, shape=None): 105 | """ 106 | Convert the vizdoom environment observation numpy are into the desired resolution and shape 107 | :param shape: desired shape in the format (rows, columns) 108 | :return: resized and rescaled image in the format (rows, columns, channels) 109 | """ 110 | if shape is None: 111 | rows, columns, _ = self.observation_space.shape 112 | else: 113 | rows, columns = shape 114 | # PIL resize has indexing opposite to numpy array 115 | img = VizDoom._resize(self._rgb_array.transpose(1, 2, 0), (columns, rows)) 116 | return img 117 | 118 | @staticmethod 119 | def _augment_data(img): 120 | """ 121 | Augment input image with N randomly colored boxes of dimension x by y 122 | where N is randomly sampled between 0 and 6 123 | and x and y are randomly sampled from between 0.1 and 0.35 124 | :param img: input image to be augmented - format (rows, columns, channels) 125 | :return img: augmented image - format (rows, columns, channels) 126 | """ 127 | dimx = img.shape[0] 128 | dimy = img.shape[1] 129 | max_rand_dim = .25 130 | min_rand_dim = .1 131 | num_blotches = np.random.randint(0, 6) 132 | 133 | for _ in range(num_blotches): 134 | # locations in [0,1] 135 | rand = np.random.rand 136 | rx = rand() 137 | ry = rand() 138 | rdx = rand() * max_rand_dim + min_rand_dim 139 | rdy = rand() * max_rand_dim + min_rand_dim 140 | 141 | rx, rdx = [round(r * dimx) for r in (rx, rdx)] 142 | ry, rdy = [round(r * dimy) for r in (ry, rdy)] 143 | for c in range(3): 144 | img[rx:rx + rdx, ry:ry + rdy, c] = np.random.randint(0, 255) 145 | return img 146 | 147 | @staticmethod 148 | def _resize(img, shape): 149 | """Resize the specified image. 150 | 151 | :param img: image to resize 152 | :param shape: desired shape in the format (rows, columns) 153 | :return: resized image 154 | """ 155 | if not (OPENCV_AVAILABLE or PILLOW_AVAILABLE): 156 | raise ValueError('No image library backend found.'' Install either ' 157 | 'OpenCV or Pillow to support image processing.') 158 | 159 | if OPENCV_AVAILABLE: 160 | return cv2.resize(img, shape, interpolation=cv2.INTER_AREA) 161 | 162 | if PILLOW_AVAILABLE: 163 | return np.array(PIL.Image.fromarray(img).resize(shape)) 164 | 165 | raise NotImplementedError 166 | 167 | def reset(self): 168 | """ 169 | Resets environment to start a new mission. 170 | 171 | If there is more than one maze it will randomly select a new maze. 172 | 173 | :return: initial observation of the environment as an rgb array in the format (rows, columns, channels) 174 | """ 175 | if self.number_maps is not 0: 176 | self.doom_map = random.choice(["map" + str(i).zfill(2) for i in range(self.number_maps)]) 177 | self.env.set_doom_map(self.doom_map) 178 | self.env.new_episode() 179 | self._rgb_array = self.env.get_state().screen_buffer 180 | observation = self._process_image() 181 | return observation 182 | 183 | def step(self, action): 184 | """Perform the specified action for the self.action_frame_repeat ticks within the environment. 185 | :param action: the index of the action to perform. The actions are specified when the cfg is created. The 186 | defaults are "MOVE_FORWARD TURN_LEFT TURN_RIGHT" 187 | :return: tuple following the gym interface, containing: 188 | - observation as a numpy array of shape (rows, height, channels) 189 | - scalar clipped reward 190 | - boolean which is true when the environment is done 191 | - {} 192 | """ 193 | one_hot_action = np.zeros(self.action_space.n, dtype=int) 194 | one_hot_action[action] = 1 195 | 196 | reward = self.env.make_action(list(one_hot_action), self.action_frame_repeat) 197 | done = self.env.is_episode_finished() 198 | # state is available only if the episode is still running 199 | if not done: 200 | self._rgb_array = self.env.get_state().screen_buffer 201 | observation = self._process_image() 202 | 203 | if self.data_augmentation: 204 | observation = VizDoom._augment_data(observation) 205 | 206 | if self.clip: 207 | reward = np.clip(reward, self.clip[0], self.clip[1]) 208 | 209 | return observation, reward, done, {} 210 | 211 | def step_record(self, action, record_path, record_shape=(120, 140)): 212 | """Perform the specified action for the self.action_frame_repeat ticks within the environment. 213 | :param action: the index of the action to perform. The actions are specified when the cfg is created. The 214 | defaults are "MOVE_FORWARD TURN_LEFT TURN_RIGHT" 215 | :param record_path: the path to save the image of the environment to 216 | :param record_shape: the shape of the image to save 217 | :return: tuple following the gym interface, containing: 218 | - observation as a numpy array of shape (rows, height, channels) 219 | - scalar clipped reward 220 | - boolean which is true when the environment is done 221 | - {} 222 | """ 223 | one_hot_action = np.zeros(self.action_space.n, dtype=int) 224 | one_hot_action[action] = 1 225 | 226 | reward = 0 227 | for _ in range(self.action_frame_repeat // 2): 228 | reward += self.env.make_action(list(one_hot_action), 2) 229 | env_state = self.env.get_state() 230 | if env_state: 231 | self._rgb_array = self.env.get_state().screen_buffer 232 | imageio.imwrite(os.path.join(record_path, str(datetime.datetime.now()) + ".png"), 233 | self._process_image(record_shape)) 234 | 235 | done = self.env.is_episode_finished() 236 | # state is available only if the episode is still running 237 | if not done: 238 | self._rgb_array = self.env.get_state().screen_buffer 239 | observation = self._process_image() 240 | 241 | if self.clip: 242 | reward = np.clip(reward, self.clip[0], self.clip[1]) 243 | 244 | return observation, reward, done, {} 245 | 246 | def close(self): 247 | """Close environment""" 248 | self.env.close() 249 | 250 | def render(self, mode='rgb_array'): 251 | """Render frame""" 252 | if mode == 'rgb_array': 253 | return self._rgb_array 254 | 255 | raise NotImplementedError 256 | 257 | def create_env(self): 258 | """ 259 | Returns a function to create an environment with the generated mazes. 260 | 261 | Used for vectorising the environment. For example as used by Stable Baselines 262 | 263 | :return: a function to create an environment with the generated mazes 264 | """ 265 | return lambda: VizDoom(self.cfg_path, number_maps=self.number_maps, scaled_resolution=self.scaled_resolution, 266 | action_frame_repeat=self.action_frame_repeat) 267 | -------------------------------------------------------------------------------- /mazeexplorer/wad.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License 3 | 4 | from omg import * 5 | 6 | 7 | def build_wall(maze, BLOCK_SIZE): 8 | things = [] 9 | linedefs = [] 10 | vertexes = [] 11 | v_indexes = {} 12 | 13 | max_w = len(maze[0]) - 1 14 | max_h = len(maze) - 1 15 | 16 | def __is_edge(w, h): 17 | return w in (0, max_w) or h in (0, max_h) 18 | 19 | def __add_start(w, h): 20 | x, y = w * BLOCK_SIZE, h * BLOCK_SIZE 21 | x += int(BLOCK_SIZE / 2) 22 | y += int(BLOCK_SIZE / 2) 23 | things.append(ZThing(*[len(things) + 1000, x, y, 0, 0, 9001, 22279])) 24 | 25 | def __add_vertex(w, h): 26 | if (w, h) in v_indexes: 27 | return 28 | 29 | x, y = w * BLOCK_SIZE, h * BLOCK_SIZE 30 | x += int(BLOCK_SIZE / 2) 31 | y += int(BLOCK_SIZE / 2) 32 | v_indexes[w, h] = len(vertexes) 33 | vertexes.append(Vertex(x, y)) 34 | 35 | def __add_line(start, end, edge=False): 36 | assert start in v_indexes 37 | assert end in v_indexes 38 | 39 | mask = 1 40 | left = right = 0 41 | if __is_edge(*start) and __is_edge(*end): 42 | if not edge: 43 | return 44 | else: 45 | # Changed the right side (one towards outside the map) 46 | # to be -1 (65535 for Doom) 47 | right = 65535 48 | mask = 15 49 | 50 | # Flipped end and start vertices to make lines "point" at right direction (mostly to see if it works) 51 | line_properties = [v_indexes[end], v_indexes[start], mask 52 | ] + [0] * 6 + [left, right] 53 | line = ZLinedef(*line_properties) 54 | linedefs.append(line) 55 | 56 | for h, row in enumerate(maze): 57 | for w, block in enumerate(row.strip()): 58 | if block == 'X': 59 | __add_vertex(w, h) 60 | else: 61 | pass 62 | 63 | corners = [(0, 0), (max_w, 0), (max_w, max_h), (0, max_h)] 64 | for v in corners: 65 | __add_vertex(*v) 66 | 67 | for i in range(len(corners)): 68 | if i != len(corners) - 1: 69 | __add_line(corners[i], corners[i + 1], True) 70 | else: 71 | __add_line(corners[i], corners[0], True) 72 | 73 | # Now connect the walls 74 | for h, row in enumerate(maze): 75 | 76 | for w, _ in enumerate(row): 77 | if (w, h) not in v_indexes: 78 | __add_start(w, h) 79 | continue 80 | 81 | if (w + 1, h) in v_indexes: 82 | __add_line((w, h), (w + 1, h)) 83 | 84 | if (w, h + 1) in v_indexes: 85 | __add_line((w, h), (w, h + 1)) 86 | 87 | return things, vertexes, linedefs 88 | 89 | 90 | def generate_wads(prefix, wad, behavior, BLOCK_SIZE=96, script=None): 91 | """ 92 | args: 93 | 94 | prefix: 95 | wad: 96 | behavior: path to compiled lump containing map behavior (default: None) 97 | script: path to script source lump containing map behavior (optional) 98 | """ 99 | 100 | new_wad = WAD() 101 | 102 | for map_index, file_name in enumerate( 103 | glob.glob('{}_*.txt'.format(prefix))): 104 | with open(file_name) as maze_source: 105 | maze = [line.strip() for line in maze_source.readlines()] 106 | maze = [line for line in maze if line] 107 | 108 | new_map = MapEditor() 109 | new_map.Linedef = ZLinedef 110 | new_map.Thing = ZThing 111 | new_map.behavior = Lump(from_file=behavior or None) 112 | new_map.scripts = Lump(from_file=script or None) 113 | things, vertexes, linedefs = build_wall(maze, BLOCK_SIZE) 114 | new_map.things = things + [ZThing(0, 0, 0, 0, 0, 1, 7)] 115 | new_map.vertexes = vertexes 116 | new_map.linedefs = linedefs 117 | new_map.sectors = [Sector(0, 128, 'CEIL5_2', 'CEIL5_1', 240, 0, 0)] 118 | new_map.sidedefs = [ 119 | Sidedef(0, 0, '-', '-', 'STONE2', 0), 120 | Sidedef(0, 0, '-', '-', '-', 0) 121 | ] 122 | new_wad.maps['MAP{:02d}'.format(map_index)] = new_map.to_lumps() 123 | 124 | new_wad.to_file(wad) 125 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License 3 | 4 | import setuptools 5 | import os 6 | 7 | if not os.path.isdir("mazeexplorer/acc") or not os.listdir("mazeexplorer/acc"): 8 | raise FileNotFoundError("\nacc files not found.\n" 9 | "Have you pulled the required submodules?\n" 10 | "If not, use the line:\n\n\t" 11 | "git submodule update --init --recursive\n") 12 | 13 | with open("README.md", "r") as fh: 14 | long_description = fh.read() 15 | 16 | setuptools.setup( 17 | name="mazeexplorer", 18 | version="1.0.5", 19 | author="Luke Harries, Sebastian Lee, Jaroslaw Rzepecki, Katya Hofmann, Sam Devlin", 20 | author_email="sam.devlin@microsoft.com", 21 | description="Customisable 3D benchmark for assessing generalisation in Reinforcement Learning.", 22 | long_description=long_description, 23 | long_description_content_type="text/markdown", 24 | url="https://github.com/microsoft/MazeExplorer", 25 | packages=setuptools.find_packages(), 26 | package_data={'mazeexplorer': ['content/*', 'acc/*', 'config_template.txt']}, 27 | classifiers=[ 28 | "Programming Language :: Python :: 3", 29 | "License :: OSI Approved :: MIT License", 30 | "Operating System :: OS Independent", 31 | ], 32 | install_requires=[ 33 | "vizdoom", 34 | "gym", 35 | "omgifol", 36 | "opencv-python", 37 | "imageio", 38 | "numpy", 39 | "tensorboardX", 40 | "pytest" 41 | ] 42 | ) 43 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | pytest tests -W ignore -------------------------------------------------------------------------------- /tests/mazes/test.cfg: -------------------------------------------------------------------------------- 1 | available_buttons = {MOVE_FORWARD TURN_LEFT TURN_RIGHT} 2 | doom_scenario_path = test.wad 3 | 4 | screen_resolution = RES_160X120 5 | screen_format = CRCGCB 6 | depth_buffer_enabled = true 7 | labels_buffer_enabled = false 8 | automap_buffer_enabled = false 9 | render_hud = false 10 | render_minimal_hud = false 11 | render_crosshair = false 12 | render_weapon = false 13 | render_decals = false 14 | render_particles = false 15 | render_effects_sprites = false 16 | episode_timeout = 1500 17 | window_visible = false 18 | available_game_variables = {POSITION_X POSITION_Y POSITION_Z VELOCITY_X VELOCITY_Y VELOCITY_Z ANGLE} 19 | game_args += +cl_run 1 20 | game_args += +vid_aspect 3 21 | game_args += +cl_spreaddecals 0 22 | game_args += +hud_scale 1 23 | game_args += +cl_bloodtype 2 24 | game_args += +cl_pufftype 1 25 | game_args += +cl_missiledecals 0 26 | game_args += +cl_bloodsplats 0 27 | game_args += +cl_showmultikills 0 28 | game_args += +cl_showsprees 0 29 | game_args += +con_midtime 0 30 | game_args += +con_notifytime 0 31 | game_args += +am_textured 1 32 | game_args += +am_showtime 0 33 | game_args += +am_showmonsters 0 34 | game_args += +am_showsecrets 0 35 | -------------------------------------------------------------------------------- /tests/mazes/test.wad: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/MazeExplorer/94931e5c7bddb006eeea09e39e9d91deb8387af1/tests/mazes/test.wad -------------------------------------------------------------------------------- /tests/test_compile_acs.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from mazeexplorer.compile_acs import compile_acs 4 | 5 | dir_path = os.path.dirname(os.path.realpath(__file__)) 6 | 7 | 8 | def test_compile_acs(tmpdir): 9 | compile_acs(tmpdir.strpath) 10 | assert os.path.isfile(os.path.join(tmpdir, "outputs", "maze.o")) 11 | assert os.path.getsize(os.path.join(tmpdir, "outputs", "maze.o")) > 0 12 | assert os.path.isdir(os.path.join(tmpdir, "outputs", "sources")) 13 | assert os.path.isdir(os.path.join(tmpdir, "outputs", "images")) 14 | -------------------------------------------------------------------------------- /tests/test_mazeexplorer.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | import numpy as np 5 | from vizdoom import GameVariable 6 | 7 | from mazeexplorer import MazeExplorer 8 | 9 | dir_path = os.path.dirname(os.path.realpath(__file__)) 10 | 11 | 12 | def test_create_mazes(tmpdir): 13 | env = MazeExplorer(mazes_path=tmpdir.strpath) 14 | 15 | files = os.listdir(env.mazes_path) 16 | 17 | required_files = ["10x10.cfg", "10x10.wad", "10x10_MAP01.txt", "outputs"] 18 | 19 | assert set(files) == set(required_files) 20 | 21 | env.reset() 22 | for _ in range(10): 23 | action = env.action_space.sample() 24 | observation, *_ = env.step(action) 25 | assert observation.shape == (42, 42, 3) 26 | 27 | 28 | def test_save_load(tmpdir): 29 | env = MazeExplorer(mazes_path=tmpdir.mkdir("maze").strpath) 30 | 31 | saved_mazes_destination = os.path.join(tmpdir, "test_mazes") 32 | 33 | env.save(saved_mazes_destination) 34 | 35 | required_files = ["10x10.cfg", "10x10.wad", "10x10_MAP01.txt", "outputs"] 36 | 37 | assert set(required_files) == set(os.listdir(saved_mazes_destination)) 38 | 39 | env = MazeExplorer.load_vizdoom_env(saved_mazes_destination, 1) 40 | 41 | for _ in range(10): 42 | action = env.action_space.sample() 43 | observation, *_ = env.step(action) 44 | assert observation.shape == (42, 42, 3) 45 | 46 | 47 | def test_step_record(tmpdir): 48 | env = MazeExplorer(random_textures=True, mazes_path=tmpdir.strpath) 49 | env.reset() 50 | for _ in range(3): 51 | action = env.action_space.sample() 52 | observation, *_ = env.step_record(action, tmpdir) 53 | assert observation.shape == (42, 42, 3) 54 | assert len(list(Path(tmpdir).glob("*.png"))) == 6 55 | 56 | 57 | def test_generate_video(tmpdir): 58 | record_path = tmpdir.mkdir("record") 59 | 60 | env = MazeExplorer(mazes_path=tmpdir.strpath) 61 | env.reset() 62 | for _ in range(10): 63 | action = env.action_space.sample() 64 | env.step_record(action, record_path=record_path) 65 | 66 | video_destination = os.path.join(record_path, "movie.avi") 67 | MazeExplorer.generate_video(record_path, video_destination) 68 | 69 | assert os.path.isfile(video_destination) 70 | assert os.path.getsize(video_destination) > 0 71 | 72 | 73 | def test_generate_with_seed(tmpdir): 74 | env = MazeExplorer(10, seed=5, mazes_path=tmpdir.mkdir("maze_1").strpath) 75 | assert len(set(env.mazes)) == 10 76 | 77 | same_env = MazeExplorer(10, seed=5, mazes_path=tmpdir.mkdir("maze_2").strpath) 78 | assert set(env.mazes) == set(same_env.mazes) 79 | 80 | different_env = MazeExplorer(10, seed=42, mazes_path=tmpdir.mkdir("maze_3").strpath) 81 | assert len(set(env.mazes) - set(different_env.mazes)) == 10 82 | 83 | 84 | def test_generate_with_seed_step(tmpdir): 85 | env = MazeExplorer(10, seed=5, mazes_path=tmpdir.mkdir("maze_1").strpath) 86 | env.reset() 87 | for _ in range(5): 88 | env.step(env.action_space.sample()) 89 | 90 | 91 | def test_fixed_keys_step(tmpdir): 92 | env = MazeExplorer(random_key_positions=False, mazes_path=tmpdir.strpath) 93 | env.reset() 94 | for _ in range(1000): 95 | action = env.action_space.sample() 96 | observation, *_ = env.step(action) 97 | assert observation.shape == (42, 42, 3) 98 | 99 | 100 | def test_generate_multiple_mazes(tmpdir): 101 | """ 102 | This function should test whether new episodes sample from the selection of map levels in the wad. 103 | The assertion could be that every map is used at least once but given the stochasticity, it becomes 104 | a trade off between how many episodes to sample and how certain one can be that the test will pass. 105 | For now this test is implemented with the weaker condition that more than one map is sampled. 106 | This ensures at least that the map level is not being fixed. 107 | """ 108 | for number_mazes in [1, 5, 10]: 109 | maps = set() 110 | env = MazeExplorer(number_maps=number_mazes, mazes_path=tmpdir.strpath) 111 | for _ in range(5000): 112 | *_, done, _ = env.step(env.action_space.sample()) 113 | if done: 114 | env.reset() 115 | map_id = int(env.env.get_game_variable(GameVariable.USER4)) 116 | maps.add(map_id) 117 | if number_mazes == 1: 118 | assert len(maps) == 1 119 | else: 120 | assert len(maps) > 1 121 | 122 | 123 | def test_reward_signal(tmpdir): 124 | env = MazeExplorer(seed=42, mazes_path=tmpdir.strpath) 125 | rews = [] 126 | rewards = [] 127 | for _ in range(1000): 128 | _, reward, done, _ = env.step(env.action_space.sample()) 129 | rewards.append(reward) 130 | if done: 131 | env.reset() 132 | 133 | assert sum(rewards) > 1 134 | 135 | return rews, rewards 136 | -------------------------------------------------------------------------------- /tests/test_vizdoom_gym.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | from mazeexplorer.vizdoom_gym import VizDoom 5 | 6 | dir_path = os.path.dirname(os.path.realpath(__file__)) 7 | 8 | 9 | def test_vizdoom_gym(): 10 | test_mazes = os.path.join(dir_path, "mazes", "test.cfg") 11 | env = VizDoom(test_mazes, number_maps=1) 12 | env.reset() 13 | for _ in range(1000): 14 | action = env.action_space.sample() 15 | observation, *_ = env.step(action) 16 | assert observation.shape == (42, 42, 3) 17 | 18 | 19 | def test_vizdoom_gym_step_record(tmpdir): 20 | test_mazes = os.path.join(dir_path, "mazes", "test.cfg") 21 | env = VizDoom(test_mazes, number_maps=1) 22 | env.reset() 23 | for _ in range(3): 24 | action = env.action_space.sample() 25 | observation, *_ = env.step_record(action, tmpdir) 26 | assert observation.shape == (42, 42, 3) 27 | assert len(list(Path(tmpdir).glob("*.png"))) == 6 28 | --------------------------------------------------------------------------------