├── .github └── ISSUE_TEMPLATE │ └── issue.md ├── .gitignore ├── LICENSE.md ├── README.md ├── assets ├── algo_info.md ├── favicon.ico ├── help.md ├── privacy_policy.pdf ├── quickstart_usermode.md ├── style.css ├── thumb.png ├── webapp_help_dev.md └── webapp_info.md ├── block_definitions.json ├── build_main_webapp.py ├── common_atoms.json ├── configs.ini ├── docs └── pcgsepy │ ├── common │ ├── api_call.html │ ├── index.html │ ├── jsonifier.html │ ├── jsonrpc.html │ ├── regex_handler.html │ ├── str_utils.html │ └── vecs.html │ ├── config.html │ ├── evo │ ├── fitness.html │ ├── genops.html │ └── index.html │ ├── fi2pop │ ├── fi2pop.html │ ├── index.html │ ├── lgp.html │ └── utils.html │ ├── guis │ ├── index.html │ ├── main_webapp │ │ ├── index.html │ │ ├── modals_msgs.html │ │ └── webapp.html │ ├── ships_comparator │ │ ├── index.html │ │ ├── modals_msgs.html │ │ └── webapp.html │ ├── utils.html │ └── voxel.html │ ├── hullbuilder.html │ ├── index.html │ ├── lsystem │ ├── actions.html │ ├── constraints.html │ ├── constraints_funcs.html │ ├── index.html │ ├── lsystem.html │ ├── parser.html │ ├── rules.html │ ├── solution.html │ ├── solver.html │ └── structure_maker.html │ ├── mapelites │ ├── bandit.html │ ├── behaviors.html │ ├── bin.html │ ├── buffer.html │ ├── emitters.html │ ├── index.html │ └── map.html │ ├── nn │ ├── estimators.html │ └── index.html │ ├── setup_utils.html │ ├── stats │ ├── index.html │ ├── plots.html │ └── tests.html │ ├── structure.html │ └── xml_conversion.html ├── estimators ├── futo.pkl ├── mame.pkl ├── mami.pkl └── tovo.pkl ├── hl_atoms.json ├── hlrules ├── hlrules_sm ├── icmap-elites ├── .gitignore ├── README.md ├── cog_experiments.ipynb ├── configs.ini ├── estimators │ ├── futo.pkl │ ├── mame.pkl │ ├── mami.pkl │ └── tovo.pkl ├── hlrules ├── llrules ├── spaceships_picker.ipynb └── spaceships_spawner.ipynb ├── l-system ├── .gitignore ├── README.md ├── configs.ini ├── estimators │ ├── futo.pkl │ ├── mame.pkl │ ├── mami.pkl │ └── tovo.pkl ├── fi2pop-demo.ipynb ├── hlrules ├── l-system-demo.ipynb ├── llrules └── rules-extractor.ipynb ├── llrules ├── main_webapp_launcher.py ├── media ├── UI_comparator_preview.jpg ├── UI_devmode_preview.jpg ├── UI_usermode_preview.png ├── UI_userstudy_preview.jpg ├── pcgsepy_banner.png └── tilesmaker_preview.jpg ├── pcgsepy ├── .gitignore ├── __init__.py ├── common │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-38.pyc │ │ ├── api_call.cpython-38.pyc │ │ └── vecs.cpython-38.pyc │ ├── api_call.py │ ├── jsonifier.py │ ├── jsonrpc.py │ ├── regex_handler.py │ ├── str_utils.py │ └── vecs.py ├── config.py ├── evo │ ├── .gitignore │ ├── fitness.py │ └── genops.py ├── fi2pop │ ├── fi2pop.py │ ├── lgp.py │ └── utils.py ├── guis │ ├── assets │ │ ├── algo_info.md │ │ ├── favicon.ico │ │ ├── help.md │ │ ├── privacy_policy.pdf │ │ ├── quickstart.md │ │ ├── quickstart_usermode.md │ │ ├── style.css │ │ ├── thumb.png │ │ ├── webapp_help_dev.md │ │ └── webapp_info.md │ ├── main_webapp │ │ ├── modals_msgs.py │ │ └── webapp.py │ ├── utils.py │ └── voxel.py ├── hullbuilder.py ├── lsystem │ ├── actions.py │ ├── constraints.py │ ├── constraints_funcs.py │ ├── lsystem.py │ ├── parser.py │ ├── rules.py │ ├── solution.py │ ├── solver.py │ └── structure_maker.py ├── mapelites │ ├── bandit.py │ ├── behaviors.py │ ├── bin.py │ ├── buffer.py │ ├── emitters.py │ └── map.py ├── nn │ └── estimators.py ├── setup_utils.py ├── stats │ ├── plots.py │ └── tests.py ├── structure.py └── xml_conversion.py ├── requirements.txt ├── setup.py ├── steam-workshop-downloader ├── .gitignore ├── README.md ├── common_atoms.json ├── configs.ini ├── hl_atoms.json ├── hlrules ├── llrules ├── spaceships-analyzer.ipynb └── steam-ws-downloader.ipynb ├── tiles_maker.py └── tileset ├── CorridorWithCargo ├── bp.sbc └── thumb.png └── Thrusters ├── bp.sbc └── thumb.png /.github/ISSUE_TEMPLATE/issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue 3 | about: Create an issue to report a bug or add a feature request 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | *Issue type:* Choose from [ bug | feature request ] 11 | 12 | *Severity:* Suggest one from [low | medium | high (application-breaking) ] 13 | 14 | **Describe the issue** 15 | A clear and concise description of what the bug is. 16 | 17 | **To Reproduce** 18 | Steps to reproduce the behavior: 19 | 1. e.g. Go to '...' 20 | 2. Click on '....' 21 | 3. Scroll down to '....' 22 | 4. See error 23 | 24 | **Expected behavior** 25 | A clear and concise description of what you expected to happen. 26 | 27 | **Screenshots** 28 | If applicable, please add screenshots to help explain your problem, they tremendously help understanding the issue. 29 | 30 | **Log file** 31 | In case of bugs, please upload the relevant log file (log_{TIMESTAMP}.log) for inspection. The log file does not contain any user information if the user declined the privacy policy or have already completed the user study. 32 | 33 | **Desktop (please complete the following information):** 34 | - OS: [e.g. Windows] 35 | - Browser: [e.g. Chrome, Edge, Firefox] 36 | - .exe file name: [e.g. Spaceship.Generator.v1.0.1.exe] 37 | 38 | **Additional context** 39 | Add any other context about the problem here. 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/* 2 | .ipynb_checkpoints/* 3 | .virtual_documents/* 4 | .vscode/* 5 | *.jupyterlab-workspace 6 | *.pyc 7 | cppn-system/* 8 | custom-tests/* 9 | icmap-elites/estimators_perf.log 10 | PCGSEPy.egg-info/* 11 | *.spec 12 | build/* 13 | dist/* 14 | *.exe 15 | *.log -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ARAYA 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. -------------------------------------------------------------------------------- /assets/algo_info.md: -------------------------------------------------------------------------------- 1 | This application uses an [evolutionary algorithm](https://en.wikipedia.org/wiki/Evolutionary_algorithm) to generate spaceships. Evolutionary algorithms are a family of optimisation algorithms that use operations such as "mutation" and "crossover" on individuals within a population in order to construct new individuals to satisfy a given goal. In this case, the goal is to generate interesting spaceships that can be successfully piloted in-game. 2 | 3 | Each spaceship is defined by a string which describes the spaceship's tiles and rotations. This string is generated by an [L-system](https://wikipedia.org/wiki/L-system) and modified by the [FI-2Pop genetic algorithm](https://www.sciencedirect.com/science/article/abs/pii/S0377221707005668). The fitness of a solution (i.e.: how *good* it is) is based on four different measures we extracted from the most voted spaceships on the Steam Workshop. 4 | 5 | The spaceships are subdivided in groups according to their characteristics. The grid you see on the left is the *behavioral grid* of [MAP-Elites](https://arxiv.org/abs/1504.04909). The different configurations you will interact with during the user study rely on different *emitters*, which determine which group of spaceship to use during the automated steps. 6 | 7 | If you want to know more about this system, why not check out our previous publications? 8 | Our first paper introduces the L-system and FI-2Pop 9 | > Gallotta, R., Arulkumaran, K., & Soros, L. B. (2022). Evolving Spaceships with a Hybrid L-system Constrained Optimisation Evolutionary Algorithm. In Genetic and Evolutionary Computation Conference Companion. https://dl.acm.org/doi/abs/10.1145/3520304.3528775 10 | 11 | and our second paper explains how we improved the FI-2Pop algorithm to produce valid spaceships more reliably, as well as introducing the MAP-Elites' emitters in our domain 12 | > Gallotta, R., Arulkumaran, K., & Soros, L. B. (2022). Surrogate Infeasible Fitness Acquirement FI-2Pop for Procedural Content Generation. In IEEE Conference on Games. https://ieeexplore.ieee.org/document/9893592 13 | 14 | Finally, in our third paper we introduced the preference-learning emitters (PLE) framework, where different types of emitters were used to learn user preferences 15 | > Gallotta, R., Arulkumaran, K., & Soros, L. B. (2022). Preference-Learning Emitters for Mixed-Initiative Quality-Diversity Algorithms. arXiv preprint arXiv:2210.13839. https://arxiv.org/abs/2210.13839) -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/assets/favicon.ico -------------------------------------------------------------------------------- /assets/help.md: -------------------------------------------------------------------------------- 1 | How to use the program: 2 | 3 | ##### Load data 4 | Load the `.json` files you obtained from the previous step of the experiment here to preview the different spaceships. 5 | 6 | ##### Rank the spaceships 7 | Rank the different spaceships according to your preferences using the menus below the spaceship preview. Remember to rank them with different scores! 8 | 9 | ##### Saving scores 10 | Once you're happy with the scores, click the "Save" button to download the results! You will need to upload this file in the final part of the questionnaire. -------------------------------------------------------------------------------- /assets/privacy_policy.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/assets/privacy_policy.pdf -------------------------------------------------------------------------------- /assets/quickstart_usermode.md: -------------------------------------------------------------------------------- 1 | This application generates spaceships for Space Engineers using an evolutionary algorithm. New spaceships can continually be "evolved" from an initial population of spaceships. 2 | 3 | You can select a spaceship from the population on the left; all colored cells can be selected. The spaceship you selected will change its symbol to "☑". You can click the "**Evolve from selected spaceship**" button to generate new spaceships. After new spaceships are generated, the symbol "▣" will mark the cells in the grid with a new spaceship. 4 | 5 | When you select a spaceship, its main properties are displayed on the right, and an interactive preview of the spaceship is displayed in the middle of the application window. You can also download a spaceship by clicking the corresponding button next to the spaceship properties. You can also change the base color of the spaceship by picking it from the widget next to the spaceship properties. 6 | 7 | You can check system messages in the "**Log**" window on the right. 8 | 9 | You can reinitialize the population by clicking the corresponding button (beware: it may take a while for the reinitialization to complete). 10 | 11 | You can also toggle between safe and unsafe mode. In safe mode, we ensure thrusters are placed on all six sides of the spaceships, at the cost of less diversity. Switching between modes requires the population to be reinitialized. 12 | 13 | The "Evolution iterations" slider will allow you to change how many steps the automated evolution takes. While this will increase the chances of obtaining more spaceships, it also comes at the cost of additional time per iteration, so be mindful when setting it! 14 | 15 | You can also follow this video tutorial: 16 | 17 | [![Tutorial Video](https://img.youtube.com/vi/bVASWQj6DHc/0.jpg)](https://youtu.be/bVASWQj6DHc "Space Engineers AI Spaceship Generator] User-study Quick start") -------------------------------------------------------------------------------- /assets/style.css: -------------------------------------------------------------------------------- 1 | .title { 2 | padding: 20px; 3 | text-align: center; 4 | } 5 | .page-description { 6 | padding: 10px; 7 | text-align: left; 8 | } 9 | .header { 10 | background-image: linear-gradient(#464d55, #222222); 11 | color: rgb(238, 238, 238); 12 | } 13 | .footer { 14 | background-image: linear-gradient(#222222, #464d55); 15 | color: #EEEEEE; 16 | } 17 | .section-title { 18 | text-align: center; 19 | } 20 | .content-string-area { 21 | height: 200px; 22 | } 23 | .log-area { 24 | height: 200px; 25 | } 26 | .rules-area { 27 | height: 600px; 28 | } 29 | .button-fullsize { 30 | width: 100%; 31 | overflow: hidden; 32 | white-space: nowrap; 33 | display: block; 34 | text-overflow: ellipsis; 35 | text-align: center; 36 | } 37 | .spacer { 38 | margin-top: 1%; 39 | } 40 | .container { 41 | display: grid; 42 | } 43 | .content, .overlay { 44 | position: relative; 45 | grid-area: 1 / 1; 46 | } 47 | .help { 48 | cursor: help 49 | } 50 | .upload { 51 | text-align: center; 52 | display: inline-flex; 53 | flex-wrap: nowrap; 54 | flex-direction: column-reverse; 55 | justify-content: center; 56 | align-items: center; 57 | border: deepskyblue; 58 | border-width: thick; 59 | border-style: dotted; 60 | border-radius: 10%; 61 | } -------------------------------------------------------------------------------- /assets/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/assets/thumb.png -------------------------------------------------------------------------------- /assets/webapp_help_dev.md: -------------------------------------------------------------------------------- 1 | How to use the program: 2 | ##### Plot settings 3 | Here it is possible to change the data that is being visualized in the MAP-Elites plot (left) and content plot (right). 4 | 5 | It is possible to visualize behavior characteristics for both the feasible and infeasible population. It is possible to plot either the Fitness, the Age, and the Coverage metric for all solutions. Additionally, it is possible to plot metrics for only the elite or the bin population. 6 | 7 | By selecting a bin in the MAP-Elites plot, the elite content will be displayed in the content plot (right) and the content properties (spaceship size, number of blocks used, and string representation) will be shown next to it. Additionally, the selected spaceship can be downloaded by clicking the "DOWNLOAD CONTENT" button at any time. You can also change the base color of the spaceship by picking it from the widget next to the spaceship properties. 8 | 9 | ##### Experiment settings & control 10 | Here there are both information on the experiment and buttons that allow the user to interact with the evolution procedure. 11 | 12 | In particular: 13 | - **Experiment settings** shows the bins that are valid for evolution, the current generation, and the bins selected by the user. There are also different controls that the user can modify: 14 | - **Choose feature descriptors (X, Y)**: here you can select which behaviour characteristics to use in MAP-Elites. 15 | - **Toggle L-system modules**: here you can select which parts of the spaceship are allowed to mutate. 16 | - **Control fitness weights**: here you can choose how much impact each metric has on the overall fitness. Slider values go from 0.0 to 1.0, and the MAP-Elites preview is updated accordingly. 17 | - **Select emitter**: here you can select which emitter to use during the experiment. Note that changing the emitter will create a new one, so all emitter data collected thus far will be lost! 18 | 19 | - **Experiment controls** is comprised of different buttons: 20 | - **APPLY STEP**: Executes a step of FI-2Pop with the selected bin(s) populations. If no bin is selected, or if the selected bin(s) is invalid, an error is thrown and no step is executed. 21 | - **INITIALIZE/RESET**: Either initializes the entire population of solutions (if empty) or resets it. 22 | - **CLEAR SELECTION**: Clears the selected bin(s). 23 | - **TOGGLE BIN SELECTION**: Toggles evolution on a single bin or multiple bins. If toggled to false and more than one bin were selected, only the last bin will remain selected. 24 | - **SUBDIVIDE SELECTED BIN(S)**: Subdivides the selected bin(s) in half, reassigning the solutions to the correct bin. 25 | - **DOWNLOAD MAP-ELITES**: Downloads the MAP-Elites object. This is only possible after a certain number of generations has elapsed. 26 | 27 | ##### High-level rules 28 | Here it is possible to inspect and update the high-level rules used by the L-system. When updating a rule, a check is always performed to ensure the expansion probability of the left-hand side rule sums up to 1. 29 | 30 | ##### Log 31 | All log messages are relayed here. As some operations may take some time to complete, progress messages are also reported. -------------------------------------------------------------------------------- /assets/webapp_info.md: -------------------------------------------------------------------------------- 1 | How to use the program: 2 | 3 | ##### Plots 4 | There are two plots that you can interact with: 5 | 6 | * Collection plot: here you can select spaceships based on their characteristics; only colored cells be selected. The spaceship you selected will change its symbol to "☑". After new spaceships are generated, the symbol "▣" will mark the cells in the grid with a new spaceship. The color of the spaceship is determined by its *Fitness*, which is a measure of how "good" the spaceship is. You can zoom using the scrollwheel and pan by keeping the left mouse button pressed and dragging the plot around. 7 | * Spaceship plot: here you can explore a preview of the spaceship you selected. Each sphere in the plot represents a game block and you can tell which block it is simply by hovering over it with the mouse cursor. The color wheel on the right allows you to change the base color of the spaceship. 8 | 9 | ##### Properties & Download 10 | Once you select a spaceshiip, its properties are displayed in the table on the right. You can also download the currently selected spaceship as a `.zip` file by clicking the **Download** button. The compressed folder contains the files needed to load the spaceship in Space Engineers as a blueprint. Simply place the unzipped folder in `...\AppData\Roaming\SpaceEngineers\Blueprints\local` and load Space Engineers. In a scenario world, press `Ctrl+F10` to bring up the **Blueprints** window and you will see the spaceship listed among the local blueprints. 11 | 12 | ##### Generating new spaceships 13 | To generate new spaceships from the currently selected one, simply press the **Evolve from spaceship** button. A progress bar will appear on the left during the generation and will disappear once the process is completed. The new spaceships are automatically added to the "Collection plot" at the end of the generation. 14 | 15 | ##### Log 16 | All log messages are relayed here. As some operations may take some time to complete, progress messages are also reported. -------------------------------------------------------------------------------- /build_main_webapp.py: -------------------------------------------------------------------------------- 1 | import PyInstaller.__main__ as pyinst 2 | from datetime import date 3 | 4 | from pcgsepy.config import USE_TORCH 5 | 6 | 7 | app_name = f'AI Spaceship Generator ({"with" if USE_TORCH else "no"} Pytorch)_{date.today().strftime("%Y%m%d")}' 8 | 9 | pyi_args = ['main_webapp_launcher.py', 10 | '--clean', 11 | '--onefile', 12 | '--noconfirm', 13 | '--name', f"{app_name}", 14 | '--icon', 'assets\\favicon.ico', 15 | '--splash', 'assets\\thumb.png', 16 | '--add-data', './estimators;estimators', 17 | '--add-data', './assets;assets', 18 | # '--add-data', './block_definitions.json;.', 19 | # '--add-data', './common_atoms.json;.', 20 | # '--add-data', './configs.ini;.', 21 | # '--add-data', './hl_atoms.json;.', 22 | # '--add-data', './hlrules;.', 23 | # '--add-data', './hlrules_sm;.', 24 | # '--add-data', './llrules;.', 25 | '--collect-data=dash_daq', 26 | '--collect-data=scipy'] 27 | 28 | pyinst.run(pyi_args) 29 | 30 | pyi_args = [ 31 | 'tiles_maker.py', 32 | '--clean', 33 | '--onefile', 34 | '--noconfirm', 35 | '--name', 'TilesMaker', 36 | '--icon', 'assets\\favicon.ico', 37 | '--splash', 'assets\\thumb.png', 38 | ] 39 | 40 | pyinst.run(pyi_args) 41 | 42 | # TODO: automatically copy generated exe at same level as file -------------------------------------------------------------------------------- /common_atoms.json: -------------------------------------------------------------------------------- 1 | { 2 | "+": {"action": "move", "args": "R"}, 3 | "-": {"action": "move", "args": "L"}, 4 | "!": {"action": "move", "args": "U"}, 5 | "?": {"action": "move", "args": "D"}, 6 | ">": {"action": "move", "args": "F"}, 7 | "<": {"action": "move", "args": "B"}, 8 | "RotXcwY": {"action": "rotate", "args": "XcwY"}, 9 | "RotXcwZ": {"action": "rotate", "args": "XcwZ"}, 10 | "RotYcwX": {"action": "rotate", "args": "YcwX"}, 11 | "RotYcwZ": {"action": "rotate", "args": "YcwZ"}, 12 | "RotZcwX": {"action": "rotate", "args": "ZcwX"}, 13 | "RotZcwY": {"action": "rotate", "args": "ZcwY"}, 14 | "RotXccwY": {"action": "rotate", "args": "XccwY"}, 15 | "RotXccwZ": {"action": "rotate", "args": "XccwZ"}, 16 | "RotYccwX": {"action": "rotate", "args": "YccwX"}, 17 | "RotYccwZ": {"action": "rotate", "args": "YccwZ"}, 18 | "RotZccwX": {"action": "rotate", "args": "ZccwX"}, 19 | "RotZccwY": {"action": "rotate", "args": "ZccwY"}, 20 | "[": {"action": "push", "args": []}, 21 | "]": {"action": "pop", "args": []} 22 | } -------------------------------------------------------------------------------- /configs.ini: -------------------------------------------------------------------------------- 1 | [LIBRARY] 2 | use_torch = False 3 | # all possible loggers: webapp,mapelites,solver,bin,emitter,fi2pop,hullbuilder,lsystem,genops,xmlconversion,parser 4 | active_loggers = webapp,mapelites,solver,bin,emitter,fi2pop,hullbuilder,lsystem,genops,xmlconversion,parser 5 | [API] 6 | host = localhost 7 | port = 3333 8 | [L-SYSTEM] 9 | common_atoms = common_atoms.json 10 | hl_atoms = hl_atoms.json 11 | pl_range = 1, 3 12 | req_tiles = cockpit,corridor,thruster 13 | n_iterations = 5 14 | n_axioms_generated = 2 15 | [GENOPS] 16 | mutations_lower_bound = -2 17 | mutations_upper_bound = 2 18 | mutations_initial_p = 0.4 19 | mutations_decay = 0.005 20 | crossover_p = 0.4 21 | [FI2POP] 22 | population_size = 20 23 | n_initial_retries = 100 24 | n_generations = 50 25 | max_string_len = 1000 26 | gen_patience = 5 27 | [FITNESS] 28 | use_bounding_box = False 29 | bounding_box = 100.0,200.0,150.0 30 | # major axis / medium axis 31 | mame_mean = 1.77 32 | mame_std = 0.75 33 | # major axis / minimum axis 34 | mami_mean = 2.71 35 | mami_std = 1.24 36 | # functional blocks / total blocks 37 | futo_mean = 0.32 38 | futo_std = 0.1 39 | # total blocks / volume 40 | tovo_mean = 0.3 41 | tovo_std = 0.18 42 | [MAPELITES] 43 | bin_n = 10,10 44 | max_x_size = 1000 45 | max_y_size = 1000 46 | max_z_size = 1000 47 | bin_population = 5 48 | max_age = 5 49 | n_dimensions_reduced = 10 50 | max_possible_dimensions = 500000 51 | epsilon_fitness = 1e-5 52 | alignment_interval = 5 53 | rescale_infeas_fitness = True 54 | bin_min_resolution = .25 55 | use_linear_estimator = False 56 | n_epochs = 20 57 | x_range = 0,1000 58 | y_range = 0,1000 59 | z_range = 0,1000 60 | [EXPERIMENT] 61 | n_runs = 50 62 | exp_name = base-exp-name 63 | [USER-STUDY] 64 | n_emitter_steps = 3 65 | # ..., x axis size, y axis size, z axis size 66 | context_idxs = 4,5,6 67 | beta_a = 1 68 | beta_b = 1 -------------------------------------------------------------------------------- /docs/pcgsepy/evo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pcgsepy.evo API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Namespace pcgsepy.evo

23 |
24 |
25 |
26 |
27 |

Sub-modules

28 |
29 |
pcgsepy.evo.fitness
30 |
31 |
32 |
33 |
pcgsepy.evo.genops
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | 65 |
66 | 69 | 70 | -------------------------------------------------------------------------------- /docs/pcgsepy/fi2pop/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pcgsepy.fi2pop API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Namespace pcgsepy.fi2pop

23 |
24 |
25 |
26 |
27 |

Sub-modules

28 |
29 |
pcgsepy.fi2pop.fi2pop
30 |
31 |
32 |
33 |
pcgsepy.fi2pop.lgp
34 |
35 |
36 |
37 |
pcgsepy.fi2pop.utils
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | 70 |
71 | 74 | 75 | -------------------------------------------------------------------------------- /docs/pcgsepy/guis/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pcgsepy.guis API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Namespace pcgsepy.guis

23 |
24 |
25 |
26 |
27 |

Sub-modules

28 |
29 |
pcgsepy.guis.main_webapp
30 |
31 |
32 |
33 |
pcgsepy.guis.utils
34 |
35 |
36 |
37 |
pcgsepy.guis.voxel
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | 70 |
71 | 74 | 75 | -------------------------------------------------------------------------------- /docs/pcgsepy/guis/main_webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pcgsepy.guis.main_webapp API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Namespace pcgsepy.guis.main_webapp

23 |
24 |
25 |
26 |
27 |

Sub-modules

28 |
29 |
pcgsepy.guis.main_webapp.modals_msgs
30 |
31 |
32 |
33 |
pcgsepy.guis.main_webapp.webapp
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | 65 |
66 | 69 | 70 | -------------------------------------------------------------------------------- /docs/pcgsepy/guis/ships_comparator/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pcgsepy.guis.ships_comparator API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Namespace pcgsepy.guis.ships_comparator

23 |
24 |
25 |
26 |
27 |

Sub-modules

28 |
29 |
pcgsepy.guis.ships_comparator.modals_msgs
30 |
31 |
32 |
33 |
pcgsepy.guis.ships_comparator.webapp
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | 65 |
66 | 69 | 70 | -------------------------------------------------------------------------------- /docs/pcgsepy/guis/ships_comparator/modals_msgs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pcgsepy.guis.ships_comparator.modals_msgs API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module pcgsepy.guis.ships_comparator.modals_msgs

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
scores_different_error = """All scores must be different!
30 |                                    
31 | Please assign different scores for each spaceship before saving."""
32 | 
33 | rankings_assigned = """All rankings assigned!
34 |                                    
35 | Please proceed to the next section of the Google Form to complete the questionnaire."""
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | 60 |
61 | 64 | 65 | -------------------------------------------------------------------------------- /docs/pcgsepy/mapelites/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pcgsepy.mapelites API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Namespace pcgsepy.mapelites

23 |
24 |
25 |
26 |
27 |

Sub-modules

28 |
29 |
pcgsepy.mapelites.bandit
30 |
31 |
32 |
33 |
pcgsepy.mapelites.behaviors
34 |
35 |
36 |
37 |
pcgsepy.mapelites.bin
38 |
39 |
40 |
41 |
pcgsepy.mapelites.buffer
42 |
43 |
44 |
45 |
pcgsepy.mapelites.emitters
46 |
47 |
48 |
49 |
pcgsepy.mapelites.map
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | 85 |
86 | 89 | 90 | -------------------------------------------------------------------------------- /docs/pcgsepy/nn/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pcgsepy.nn API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Namespace pcgsepy.nn

23 |
24 |
25 |
26 |
27 |

Sub-modules

28 |
29 |
pcgsepy.nn.estimators
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | 60 |
61 | 64 | 65 | -------------------------------------------------------------------------------- /docs/pcgsepy/stats/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pcgsepy.stats API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Namespace pcgsepy.stats

23 |
24 |
25 |
26 |
27 |

Sub-modules

28 |
29 |
pcgsepy.stats.plots
30 |
31 |
32 |
33 |
pcgsepy.stats.tests
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | 65 |
66 | 69 | 70 | -------------------------------------------------------------------------------- /estimators/futo.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/estimators/futo.pkl -------------------------------------------------------------------------------- /estimators/mame.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/estimators/mame.pkl -------------------------------------------------------------------------------- /estimators/mami.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/estimators/mami.pkl -------------------------------------------------------------------------------- /estimators/tovo.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/estimators/tovo.pkl -------------------------------------------------------------------------------- /hl_atoms.json: -------------------------------------------------------------------------------- 1 | { 2 | "cockpit": { 3 | "dimensions": [25, 15, 25], 4 | "offset": 5 5 | }, 6 | "thrusters": { 7 | "dimensions": [25, 15, 35], 8 | "offset": 5 9 | }, 10 | "corridorsimple": { 11 | "dimensions": [25, 15, 25], 12 | "offset": 5 13 | }, 14 | "corridorreactors": { 15 | "dimensions": [25, 40, 45], 16 | "offset": 5 17 | }, 18 | "corridorcargo": { 19 | "dimensions": [25, 25, 35], 20 | "offset": 5 21 | }, 22 | "corridorgyros": { 23 | "dimensions": [25, 35, 45], 24 | "offset": 5 25 | }, 26 | "altthrusters": { 27 | "dimensions": [25, 15, 35], 28 | "offset": 5 29 | } 30 | } -------------------------------------------------------------------------------- /hlrules: -------------------------------------------------------------------------------- 1 | # -- CARGO SHIP HIGH LEVEL RULES 2 | # - head 3 | head 1 cockpit(1) 4 | # - tail 5 | tail 1 corridorsimple(1)thrusters(1) 6 | # - body 7 | body 1 corridorsimple(X) 8 | # extension 9 | corridorsimple(x) 0.25 corridorsimple(Y)corridorsimple(X) 10 | # introduce special blocks 11 | corridorsimple(x)[ 0.25 corridorreactors(X)corridorsimple(1)[ 12 | corridorsimple(x)[ 0.5 corridorcargo(X)corridorsimple(1)[ 13 | corridorsimple(x)[ 0.25 corridorgyros(X)corridorsimple(1)[ 14 | corridorsimple(x)] 0.15 corridorsimple(1)thrusters(1)] 15 | corridorsimple(x)] 0.15 corridorsimple(Y)corridorsimple(1)] 16 | corridorsimple(x)] 0.70 corridorsimple(x)] 17 | corridorsimple(x) 0.05 corridorsimple(1)corridorreactors(X) 18 | corridorsimple(x) 0.15 corridorsimple(1)corridorcargo(X) 19 | corridorsimple(x) 0.05 corridorsimple(1)corridorgyros(X) 20 | # rotate 21 | corridorsimple(x) 0.125 corridorsimple(Y)[RotYcwXcorridorsimple(X)] 22 | corridorsimple(x) 0.125 corridorsimple(Y)[RotYccwXcorridorsimple(X)] 23 | corridorsimple(x) 0.125 corridorsimple(Y)[RotYcwZcorridorsimple(X)] 24 | corridorsimple(x) 0.125 corridorsimple(Y)[RotYccwZcorridorsimple(X)] -------------------------------------------------------------------------------- /hlrules_sm: -------------------------------------------------------------------------------- 1 | # -- CARGO SHIP HIGH LEVEL RULES 2 | # - head 3 | head 0.5 cockpit(1)corridorsimple(1)[RotYcwXcorridorsimple(1)[RotYcwXcorridorsimple(1)thrusters(1)]][RotYccwXcorridorsimple(1)[RotYccwXcorridorsimple(1)thrusters(1)]] 4 | head 0.5 cockpit(1)corridorsimple(1)[RotYcwZcorridorsimple(1)[RotYcwZcorridorsimple(1)thrusters(1)]][RotYccwZcorridorsimple(1)[RotYccwZcorridorsimple(1)thrusters(1)]] 5 | # - tail 6 | tail 1 corridorsimple(1)[RotYcwXcorridorsimple(1)thrusters(1)][RotYccwXcorridorsimple(1)thrusters(1)][RotYcwZcorridorsimple(1)thrusters(1)][RotYccwZcorridorsimple(1)thrusters(1)]thrusters(1) 7 | # - body 8 | body 1 corridorsimple(X) 9 | # extension 10 | corridorsimple(x) 0.25 corridorsimple(Y)corridorsimple(X) 11 | # introduce special blocks 12 | corridorsimple(x)[ 0.25 corridorreactors(X)corridorsimple(1)[ 13 | corridorsimple(x)[ 0.5 corridorcargo(X)corridorsimple(1)[ 14 | corridorsimple(x)[ 0.25 corridorgyros(X)corridorsimple(1)[ 15 | corridorsimple(x)] 0.15 corridorsimple(1)thrusters(1)] 16 | corridorsimple(x)] 0.15 corridorsimple(Y)corridorsimple(1)] 17 | corridorsimple(x)] 0.70 corridorsimple(x)] 18 | corridorsimple(x) 0.05 corridorsimple(1)corridorreactors(X) 19 | corridorsimple(x) 0.15 corridorsimple(1)corridorcargo(X) 20 | corridorsimple(x) 0.05 corridorsimple(1)corridorgyros(X) 21 | # rotate 22 | corridorsimple(x) 0.125 corridorsimple(Y)[RotYcwXcorridorsimple(X)] 23 | corridorsimple(x) 0.125 corridorsimple(Y)[RotYccwXcorridorsimple(X)] 24 | corridorsimple(x) 0.125 corridorsimple(Y)[RotYcwZcorridorsimple(X)] 25 | corridorsimple(x) 0.125 corridorsimple(Y)[RotYccwZcorridorsimple(X)] -------------------------------------------------------------------------------- /icmap-elites/.gitignore: -------------------------------------------------------------------------------- 1 | .ipynb_checkpoints/* 2 | *-checkpoint* 3 | *.json 4 | *.log 5 | *.npz 6 | results/* 7 | spaceships/* 8 | tileset/* -------------------------------------------------------------------------------- /icmap-elites/README.md: -------------------------------------------------------------------------------- 1 | # ICMAP-Elites 2 | 3 | An Interactive Constrained MAP-Elites system. The scripts and notebooks in this directory are used for the IC MAP-Elites experiment presented in 4 | ``` 5 | Gallotta, Roberto, Kai Arulkumaran, and L. B. Soros. ‘Surrogate Infeasible Fitness Acquirement FI-2Pop for Procedural Content Generation’. In 2022 IEEE Conference on Games (CoG), ?-?, CASIA, Beijing, China, 2022. 10.48550/arXiv.2205.05834. 6 | ``` 7 | 8 | ## Building 9 | Toggle `use_torch` in the config file in order to reproduce the results of the paper before building the library. 10 | 11 | ## Notebooks 12 | The notebook `spaceships_spawner.ipynb` is used to directly spawn a spaceship in Space Engineers. 13 | 14 | The notebook `spaceships_picker.ipynb` is used to pick spaceships with similar behaviour characteristics across different experiment runs. 15 | 16 | The notebook `cog_experiments.ipynb` contains all the code needed to reproduce the results for the aforementioned paper. 17 | -------------------------------------------------------------------------------- /icmap-elites/configs.ini: -------------------------------------------------------------------------------- 1 | [LIBRARY] 2 | use_torch = False 3 | ; available loggers: webapp, mapelites, fi2pop, genops 4 | active_loggers = webapp 5 | [API] 6 | host = localhost 7 | port = 3333 8 | [L-SYSTEM] 9 | common_atoms = common_atoms.json 10 | hl_atoms = hl_atoms.json 11 | pl_range = 1, 3 12 | req_tiles = cockpit,corridor,thruster 13 | n_iterations = 5 14 | n_axioms_generated = 2 15 | [GENOPS] 16 | mutations_lower_bound = -2 17 | mutations_upper_bound = 2 18 | mutations_initial_p = 0.4 19 | mutations_decay = 0.005 20 | crossover_p = 0.4 21 | [FI2POP] 22 | population_size = 20 23 | n_initial_retries = 100 24 | n_generations = 50 25 | max_string_len = 500 26 | gen_patience = 5 27 | [FITNESS] 28 | use_bounding_box = False 29 | bounding_box = 100.0,200.0,150.0 30 | # major axis / medium axis 31 | mame_mean = 1.77 32 | mame_std = 0.75 33 | # major axis / minimum axis 34 | mami_mean = 2.71 35 | mami_std = 1.24 36 | # functional blocks / total blocks 37 | futo_mean = 0.32 38 | futo_std = 0.1 39 | # total blocks / volume 40 | tovo_mean = 0.3 41 | tovo_std = 0.18 42 | [MAPELITES] 43 | bin_n = 10,10 44 | max_x_size = 1000 45 | max_y_size = 1000 46 | max_z_size = 1000 47 | bin_population = 5 48 | max_age = 5 49 | n_dimensions_reduced = 10 50 | max_possible_dimensions = 500000 51 | epsilon_fitness = 1e-5 52 | alignment_interval = 3 53 | rescale_infeas_fitness = True 54 | bin_min_resolution = .25 55 | use_linear_estimator = False 56 | [EXPERIMENT] 57 | n_runs = 50 58 | exp_name = base-exp-name 59 | [USER-STUDY] 60 | emitters_list = mapelites_human,mapelites_random,mapelites_greedy,mapelites_contbandit 61 | n_generations_allowed = 6 62 | n_emitter_steps = 3 63 | # ..., x axis size, y axis size, z axis size 64 | context_idxs = 4,5,6 65 | beta_a = 0 66 | beta_b = 0 -------------------------------------------------------------------------------- /icmap-elites/estimators/futo.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/icmap-elites/estimators/futo.pkl -------------------------------------------------------------------------------- /icmap-elites/estimators/mame.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/icmap-elites/estimators/mame.pkl -------------------------------------------------------------------------------- /icmap-elites/estimators/mami.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/icmap-elites/estimators/mami.pkl -------------------------------------------------------------------------------- /icmap-elites/estimators/tovo.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/icmap-elites/estimators/tovo.pkl -------------------------------------------------------------------------------- /icmap-elites/hlrules: -------------------------------------------------------------------------------- 1 | # -- CARGO SHIP HIGH LEVEL RULES 2 | # - head 3 | head 1 cockpit 4 | # - tail 5 | tail 1 thrusters 6 | # - body 7 | body 1 corridorsimple(X) 8 | # extension 9 | corridorsimple(x) 0.25 corridorsimple(Y)corridorsimple(X) 10 | # introduce special blocks 11 | corridorsimple(x) 0.05 corridorsimple(Y)corridorreactors(X) 12 | corridorsimple(x) 0.15 corridorsimple(Y)corridorcargo(X) 13 | corridorsimple(x) 0.05 corridorsimple(Y)corridorgyros(X) 14 | corridorsimple(x)] 0.15 corridorsimple(Y)thrusters(1)] 15 | corridorsimple(x)] 0.85 corridorsimple(x)] 16 | # rotate 17 | corridorsimple(x) 0.125 corridorsimple(Y)[RotYcwXcorridorsimple(X)] 18 | corridorsimple(x) 0.125 corridorsimple(Y)[RotYccwXcorridorsimple(X)] 19 | corridorsimple(x) 0.125 corridorsimple(Y)[RotYcwZcorridorsimple(X)] 20 | corridorsimple(x) 0.125 corridorsimple(Y)[RotYccwZcorridorsimple(X)] -------------------------------------------------------------------------------- /icmap-elites/spaceships_picker.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Spaceships picker\n", 8 | "\n", 9 | "Pick spaceships with same (or similar) behavior characteristics, randomly." 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "## Imports" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": null, 22 | "metadata": {}, 23 | "outputs": [], 24 | "source": [ 25 | "import numpy as np\n", 26 | "import os\n", 27 | "\n", 28 | "from typing import List, Tuple" 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "metadata": {}, 34 | "source": [ 35 | "## MAP-Elites properties" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": null, 41 | "metadata": {}, 42 | "outputs": [], 43 | "source": [ 44 | "BIN_N = (32, 32)\n", 45 | "BIN_BOUNDS = (1, 1)" 46 | ] 47 | }, 48 | { 49 | "cell_type": "markdown", 50 | "metadata": {}, 51 | "source": [ 52 | "## Solutions parsing\n", 53 | "\n", 54 | "Solutions are high-level spaceships strings saved in a `.log` file as\n", 55 | "\n", 56 | "```\n", 57 | "{spaceship_string}\n", 58 | "{[fitness_1, fitness_2, ..., fitness_n]}\n", 59 | "\\r\n", 60 | "```" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "main_folder = 'results'\n", 70 | "\n", 71 | "methods_names = ['FI-2Pop', 'm-FI-2Pop', 'CMAP-Elites', 'Em-CMAP-Elites']\n", 72 | "folder_names = ['cog_experiment01', 'cog_experiment01', 'cog_experiment02', 'cog_experiment05']\n", 73 | "log_names = ['standard-fi2pop-elites_atoms.log', 'variant-fi2pop-min_merge-elites_atoms.log', 'standard-mapelites-elites_atoms.log', 'variant-optim-mapelites-min_merge-elites_atoms.log']\n", 74 | "\n", 75 | "def read_file(fname: str) -> Tuple[List[str], List[np.ndarray]]:\n", 76 | " strings, fitnesses = [], []\n", 77 | " \n", 78 | " with open(fname, 'r') as f:\n", 79 | " contents = f.readlines()\n", 80 | "\n", 81 | " i = 0\n", 82 | " while i < len(contents):\n", 83 | " strings.append(contents[i])\n", 84 | " fitnesses.append(np.asanyarray(list(map(float, contents[i+1].replace('Fitness: ', '').replace('[', '').replace(']', '').replace('\\n', '').split(',')))))\n", 85 | " i += 3\n", 86 | " \n", 87 | " return strings, fitnesses" 88 | ] 89 | }, 90 | { 91 | "cell_type": "markdown", 92 | "metadata": {}, 93 | "source": [ 94 | "Parse all `log` files:" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": null, 100 | "metadata": {}, 101 | "outputs": [], 102 | "source": [ 103 | "all_strings = {}\n", 104 | "all_fitnesses = {}\n", 105 | "\n", 106 | "for method, logname, foldername in zip(methods_names, log_names, folder_names):\n", 107 | " fname = os.path.join(main_folder, foldername)\n", 108 | " logloc = os.path.join(fname, logname)\n", 109 | " ss, fs = read_file(fname=logloc)\n", 110 | " all_strings[method] = ss\n", 111 | " all_fitnesses[method] = fs" 112 | ] 113 | }, 114 | { 115 | "cell_type": "markdown", 116 | "metadata": {}, 117 | "source": [ 118 | "## Bins selection" 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": null, 124 | "metadata": {}, 125 | "outputs": [], 126 | "source": [ 127 | "def to_grid_bin_idxs(fitness: np.ndarray) -> Tuple[int, int]:\n", 128 | " b1 = np.arange(0, BIN_BOUNDS[0], BIN_BOUNDS[0] / BIN_N[0])\n", 129 | " b2 = np.arange(0, BIN_BOUNDS[1], BIN_BOUNDS[1] / BIN_N[1]) \n", 130 | " bx = np.digitize(x=[fitness[0]], bins=b1, right=False)[0] - 1\n", 131 | " by = np.digitize(x=[fitness[1]], bins=b2, right=False)[0] - 1\n", 132 | " return (bx, by)\n", 133 | "\n", 134 | "def pick_random_viable_index(ref_bc: Tuple[int, int],\n", 135 | " fitnesses: List[np.ndarray]) -> int:\n", 136 | " bcs = [to_grid_bin_idxs(fitness=f) for f in fitnesses]\n", 137 | " if ref_bc in bcs:\n", 138 | " print(f'Found same BC: {ref_bc}')\n", 139 | " return bcs.index(ref_bc)\n", 140 | " else:\n", 141 | " scores = [np.abs(ref_bc[0] - b1) + np.abs(ref_bc[0] - b2) for (b1, b2) in bcs]\n", 142 | " closest = np.argmin(scores)\n", 143 | " print(f'Selecting closest BC: {bcs[closest]}')\n", 144 | " return closest" 145 | ] 146 | }, 147 | { 148 | "cell_type": "markdown", 149 | "metadata": {}, 150 | "source": [ 151 | "## Spaceships selection" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": null, 157 | "metadata": {}, 158 | "outputs": [], 159 | "source": [ 160 | "# Enforce selecting a specific bin by setting the value of ref_bc\n", 161 | "ref_bc = None\n", 162 | "# ref_bc = (28, 3)" 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": null, 168 | "metadata": {}, 169 | "outputs": [], 170 | "source": [ 171 | "chosen_spaceships = {}\n", 172 | "# pick a random spaceship for the first experiment\n", 173 | "if not ref_bc:\n", 174 | " ref_spaceship_idx = np.random.choice(np.arange(len(all_strings[list(all_strings.keys())[0]])))\n", 175 | " ref_bc = to_grid_bin_idxs(fitness=all_fitnesses[list(all_fitnesses.keys())[0]][ref_spaceship_idx])\n", 176 | "for k in all_strings.keys():\n", 177 | " print(f'Choosing spaceship for {k}...')\n", 178 | " i = pick_random_viable_index(ref_bc=ref_bc,\n", 179 | " fitnesses=all_fitnesses[k])\n", 180 | " print(f'Chosen spaceship is {all_strings[k][i]}')\n" 181 | ] 182 | } 183 | ], 184 | "metadata": { 185 | "kernelspec": { 186 | "display_name": "Python 3.8.13 ('pcg')", 187 | "language": "python", 188 | "name": "python3" 189 | }, 190 | "language_info": { 191 | "codemirror_mode": { 192 | "name": "ipython", 193 | "version": 3 194 | }, 195 | "file_extension": ".py", 196 | "mimetype": "text/x-python", 197 | "name": "python", 198 | "nbconvert_exporter": "python", 199 | "pygments_lexer": "ipython3", 200 | "version": "3.8.13" 201 | }, 202 | "orig_nbformat": 4, 203 | "vscode": { 204 | "interpreter": { 205 | "hash": "baec60536c6749885c57d3beb549b4412d50c1c1ea218f0ac711a9872f2242c3" 206 | } 207 | } 208 | }, 209 | "nbformat": 4, 210 | "nbformat_minor": 2 211 | } 212 | -------------------------------------------------------------------------------- /icmap-elites/spaceships_spawner.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Spaceship spawner\n", 8 | "\n", 9 | "Spawns a spaceship in Space Engineers." 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "## Imports and setup" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": null, 22 | "metadata": {}, 23 | "outputs": [], 24 | "source": [ 25 | "from pcgsepy.common.api_call import get_base_values, GameMode, toggle_gamemode\n", 26 | "from pcgsepy.common.vecs import Vec, Orientation\n", 27 | "from pcgsepy.lsystem.structure_maker import LLStructureMaker\n", 28 | "from pcgsepy.setup_utils import setup_matplotlib, get_default_lsystem\n", 29 | "from pcgsepy.common.api_call import place_blocks\n", 30 | "from pcgsepy.hullbuilder import HullBuilder" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [ 39 | "setup_matplotlib(larger_fonts=False)\n", 40 | "\n", 41 | "used_ll_blocks = [\n", 42 | " 'MyObjectBuilder_CubeBlock_LargeBlockArmorCornerInv',\n", 43 | " 'MyObjectBuilder_CubeBlock_LargeBlockArmorCorner',\n", 44 | " 'MyObjectBuilder_CubeBlock_LargeBlockArmorSlope',\n", 45 | " 'MyObjectBuilder_CubeBlock_LargeBlockArmorBlock',\n", 46 | " 'MyObjectBuilder_Gyro_LargeBlockGyro',\n", 47 | " 'MyObjectBuilder_Reactor_LargeBlockSmallGenerator',\n", 48 | " 'MyObjectBuilder_CargoContainer_LargeBlockSmallContainer',\n", 49 | " 'MyObjectBuilder_Cockpit_OpenCockpitLarge',\n", 50 | " 'MyObjectBuilder_Thrust_LargeBlockSmallThrust',\n", 51 | " 'MyObjectBuilder_InteriorLight_SmallLight',\n", 52 | " 'MyObjectBuilder_CubeBlock_Window1x1Slope',\n", 53 | " 'MyObjectBuilder_CubeBlock_Window1x1Flat',\n", 54 | " 'MyObjectBuilder_InteriorLight_LargeBlockLight_1corner'\n", 55 | "]\n", 56 | "\n", 57 | "lsystem = get_default_lsystem(used_ll_blocks=used_ll_blocks)" 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "metadata": {}, 63 | "source": [ 64 | "## Spaceship string\n", 65 | "\n", 66 | "Define here the high-level spaceship string." 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": null, 72 | "metadata": {}, 73 | "outputs": [], 74 | "source": [ 75 | "spaceship_string = 'cockpitcorridorsimple(1)corridorsimple(1)[RotYcwZcorridorsimple(2)]corridorgyros(1)[RotYcwXcorridorsimple(2)]corridorsimple(1)corridorreactors(2)corridorsimple(1)[RotYccwXcorridorsimple(1)]corridorcargo(1)thrusters'" 76 | ] 77 | }, 78 | { 79 | "cell_type": "markdown", 80 | "metadata": {}, 81 | "source": [ 82 | "## Spaceship creation" 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": null, 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [ 91 | "ml_string = lsystem.hl_solver.translator.transform(string=spaceship_string)\n", 92 | "ll_solution = lsystem.ll_solver.solve(string=ml_string,\n", 93 | " iterations=1,\n", 94 | " strings_per_iteration=1,\n", 95 | " check_sat=False)[0]\n", 96 | "base_position, orientation_forward, orientation_up = Vec.v3i(\n", 97 | " 0, 0, 0), Orientation.FORWARD.value, Orientation.UP.value\n", 98 | "structure = Structure(origin=base_position,\n", 99 | " orientation_forward=orientation_forward,\n", 100 | " orientation_up=orientation_up)\n", 101 | "structure = LLStructureMaker(\n", 102 | " atoms_alphabet=lsystem.ll_solver.atoms_alphabet,\n", 103 | " position=base_position).fill_structure(structure=structure,\n", 104 | " string=ll_solution.string)\n", 105 | "structure.sanify()\n", 106 | "\n", 107 | "hull_builder = HullBuilder(erosion_type='bin',\n", 108 | " apply_erosion=True,\n", 109 | " apply_smoothing=False)\n", 110 | "hull_builder.add_external_hull(structure=structure)\n", 111 | "\n", 112 | "structure.show('')" 113 | ] 114 | }, 115 | { 116 | "cell_type": "markdown", 117 | "metadata": {}, 118 | "source": [ 119 | "## In-game placement" 120 | ] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "execution_count": null, 125 | "metadata": {}, 126 | "outputs": [], 127 | "source": [ 128 | "base_position, orientation_forward, orientation_up = get_base_values()\n", 129 | "# place_structure(structure=structure,\n", 130 | "# position=base_position,\n", 131 | "# orientation_forward=orientation_forward,\n", 132 | "# orientation_up=orientation_up,\n", 133 | "# batchify=False)\n", 134 | "structure.update(\n", 135 | " origin=base_position,\n", 136 | " orientation_forward=orientation_forward,\n", 137 | " orientation_up=orientation_up,\n", 138 | ")\n", 139 | "toggle_gamemode(GameMode.PLACING)\n", 140 | "place_blocks(structure.get_all_blocks(), sequential=False)\n", 141 | "toggle_gamemode(GameMode.EVALUATING)" 142 | ] 143 | } 144 | ], 145 | "metadata": { 146 | "kernelspec": { 147 | "display_name": "Python 3.8.13 ('pcg')", 148 | "language": "python", 149 | "name": "python3" 150 | }, 151 | "language_info": { 152 | "codemirror_mode": { 153 | "name": "ipython", 154 | "version": 3 155 | }, 156 | "file_extension": ".py", 157 | "mimetype": "text/x-python", 158 | "name": "python", 159 | "nbconvert_exporter": "python", 160 | "pygments_lexer": "ipython3", 161 | "version": "3.8.13" 162 | }, 163 | "orig_nbformat": 4, 164 | "vscode": { 165 | "interpreter": { 166 | "hash": "baec60536c6749885c57d3beb549b4412d50c1c1ea218f0ac711a9872f2242c3" 167 | } 168 | } 169 | }, 170 | "nbformat": 4, 171 | "nbformat_minor": 2 172 | } 173 | -------------------------------------------------------------------------------- /l-system/.gitignore: -------------------------------------------------------------------------------- 1 | .ipynb_checkpoints/* 2 | *-checkpoint* 3 | *.json 4 | *.log 5 | *.npz 6 | results/* 7 | spaceships/* 8 | tileset/* -------------------------------------------------------------------------------- /l-system/README.md: -------------------------------------------------------------------------------- 1 | # L-System 2 | 3 | A notebook illustrating a simple L-System applied to Space Engineers using the [Space Engineers API](https://github.com/iv4xr-project/iv4xr-se-plugin) and the PCGSEPy library. 4 | 5 | Run the `l-system-demo.ipynb` notebook to view the demo. 6 | 7 | Modify the `configs.ini` file to change settings. The files `hlrules` and `llrules` contain the rules definitions, whereas `hl_atoms.json` defines the high-level structures dimensions. 8 | 9 | Running the `fi2pop-demo.ipynb` will reproduce the results presented in the paper 10 | ``` 11 | Roberto Gallotta, Kai Arulkumaran, and L. B. Soros. 2022. Evolving spaceships with a hybrid L-system constrained optimisation evolutionary algorithm. In Proceedings of the Genetic and Evolutionary Computation Conference Companion (GECCO '22). Association for Computing Machinery, New York, NY, USA, 711–714. https://doi.org/10.1145/3520304.3528775 12 | ``` 13 | -------------------------------------------------------------------------------- /l-system/configs.ini: -------------------------------------------------------------------------------- 1 | [LIBRARY] 2 | use_torch = False 3 | ; available loggers: webapp, mapelites, fi2pop, genops 4 | active_loggers = webapp 5 | [API] 6 | host = localhost 7 | port = 3333 8 | [L-SYSTEM] 9 | common_atoms = common_atoms.json 10 | hl_atoms = hl_atoms.json 11 | pl_range = 1, 3 12 | req_tiles = cockpit,corridor,thruster 13 | n_iterations = 5 14 | n_axioms_generated = 2 15 | [GENOPS] 16 | mutations_lower_bound = -2 17 | mutations_upper_bound = 2 18 | mutations_initial_p = 0.4 19 | mutations_decay = 0.005 20 | crossover_p = 0.4 21 | [FI2POP] 22 | population_size = 20 23 | n_initial_retries = 100 24 | n_generations = 50 25 | max_string_len = 500 26 | gen_patience = 5 27 | [FITNESS] 28 | use_bounding_box = False 29 | bounding_box = 100.0,200.0,150.0 30 | # major axis / medium axis 31 | mame_mean = 1.77 32 | mame_std = 0.75 33 | # major axis / minimum axis 34 | mami_mean = 2.71 35 | mami_std = 1.24 36 | # functional blocks / total blocks 37 | futo_mean = 0.32 38 | futo_std = 0.1 39 | # total blocks / volume 40 | tovo_mean = 0.3 41 | tovo_std = 0.18 42 | [MAPELITES] 43 | bin_n = 10,10 44 | max_x_size = 1000 45 | max_y_size = 1000 46 | max_z_size = 1000 47 | bin_population = 5 48 | max_age = 5 49 | n_dimensions_reduced = 10 50 | max_possible_dimensions = 500000 51 | epsilon_fitness = 1e-5 52 | alignment_interval = 3 53 | rescale_infeas_fitness = True 54 | bin_min_resolution = .25 55 | use_linear_estimator = False 56 | [EXPERIMENT] 57 | n_runs = 50 58 | exp_name = base-exp-name 59 | [USER-STUDY] 60 | emitters_list = mapelites_human,mapelites_random,mapelites_greedy,mapelites_contbandit 61 | n_generations_allowed = 6 62 | n_emitter_steps = 3 63 | # ..., x axis size, y axis size, z axis size 64 | context_idxs = 4,5,6 65 | beta_a = 0 66 | beta_b = 0 -------------------------------------------------------------------------------- /l-system/estimators/futo.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/l-system/estimators/futo.pkl -------------------------------------------------------------------------------- /l-system/estimators/mame.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/l-system/estimators/mame.pkl -------------------------------------------------------------------------------- /l-system/estimators/mami.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/l-system/estimators/mami.pkl -------------------------------------------------------------------------------- /l-system/estimators/tovo.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/l-system/estimators/tovo.pkl -------------------------------------------------------------------------------- /l-system/hlrules: -------------------------------------------------------------------------------- 1 | # -- CARGO SHIP HIGH LEVEL RULES 2 | # - head 3 | head 1 cockpit 4 | # - tail 5 | tail 1 thrusters 6 | # - body 7 | body 1 corridorsimple(X) 8 | # extension 9 | corridorsimple(x) 0.25 corridorsimple(Y)corridorsimple(X) 10 | # introduce special blocks 11 | corridorsimple(x) 0.05 corridorsimple(Y)corridorreactors(X) 12 | corridorsimple(x) 0.15 corridorsimple(Y)corridorcargo(X) 13 | corridorsimple(x) 0.05 corridorsimple(Y)corridorgyros(X) 14 | corridorsimple(x)] 0.15 corridorsimple(Y)thrusters(1)] 15 | corridorsimple(x)] 0.85 corridorsimple(x)] 16 | # rotate 17 | corridorsimple(x) 0.125 corridorsimple(Y)[RotYcwXcorridorsimple(X)] 18 | corridorsimple(x) 0.125 corridorsimple(Y)[RotYccwXcorridorsimple(X)] 19 | corridorsimple(x) 0.125 corridorsimple(Y)[RotYcwZcorridorsimple(X)] 20 | corridorsimple(x) 0.125 corridorsimple(Y)[RotYccwZcorridorsimple(X)] -------------------------------------------------------------------------------- /l-system/rules-extractor.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "78ff2dd3-6a8a-4472-8007-cc9b95bf1351", 6 | "metadata": {}, 7 | "source": [ 8 | "# Rules extractor\n", 9 | "\n", 10 | "Use this notebook to extract low-level rules from the game (ie: create low level tiles)." 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "id": "eed5969b-1667-4b96-b397-493617928546", 17 | "metadata": { 18 | "tags": [] 19 | }, 20 | "outputs": [], 21 | "source": [ 22 | "import numpy as np\n", 23 | "import os\n", 24 | "import xml.etree.ElementTree as ET\n", 25 | "\n", 26 | "from pcgsepy.common.vecs import Orientation, orientation_from_vec, Vec\n", 27 | "from pcgsepy.xml_conversion import convert_xml_to_structure\n", 28 | "from pcgsepy.xml_conversion import extract_rule" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "id": "404419b4-821e-4306-a3ac-43fbd0fafc43", 35 | "metadata": { 36 | "tags": [] 37 | }, 38 | "outputs": [], 39 | "source": [ 40 | "BLUEPRINTS_DIR = 'tileset'\n", 41 | "EXTRACT_ALL = False\n", 42 | "\n", 43 | "\n", 44 | "available_tiles = os.listdir(BLUEPRINTS_DIR)" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": null, 50 | "id": "5d78f751-c1a9-4a2f-9682-595433d5d17e", 51 | "metadata": { 52 | "tags": [] 53 | }, 54 | "outputs": [], 55 | "source": [ 56 | "if EXTRACT_ALL:\n", 57 | " for tile in available_tiles:\n", 58 | " rule, dims = extract_rule(bp_dir=os.path.join(BLUEPRINTS_DIR, tile))\n", 59 | " print(f'----\\n{tile} 1 {rule}')\n", 60 | " print({\n", 61 | " tile: {\n", 62 | " 'dimensions': [dims[0], dims[1], dims[2]],\n", 63 | " 'offset': 5\n", 64 | " }\n", 65 | " })\n", 66 | "else:\n", 67 | " print('Available tiles:')\n", 68 | " for i, tile in enumerate(available_tiles):\n", 69 | " print(f\" {i+1}. {tile}\")\n", 70 | " t = int(input('Choose which tile to process (number): ')) - 1\n", 71 | " assert t > -1 and t < len(available_tiles), f'Invalid tile index: {t}'\n", 72 | " rule_name = input(\"Enter name of tile (leave blank to use folder's): \")\n", 73 | " rule_name = rule_name if rule_name else available_tiles[t]\n", 74 | " blueprint_directory = os.path.join(BLUEPRINTS_DIR, available_tiles[t])\n", 75 | " rule, dims = extract_rule(bp_dir=blueprint_directory, title=rule_name)\n", 76 | " print(f'RULE: {rule_name}')\n", 77 | " print(rule)\n", 78 | " print(f'\\nTILE DIMENSIONS: {dims}')" 79 | ] 80 | } 81 | ], 82 | "metadata": { 83 | "kernelspec": { 84 | "display_name": "Python 3.8.13 ('pcg')", 85 | "language": "python", 86 | "name": "python3" 87 | }, 88 | "language_info": { 89 | "codemirror_mode": { 90 | "name": "ipython", 91 | "version": 3 92 | }, 93 | "file_extension": ".py", 94 | "mimetype": "text/x-python", 95 | "name": "python", 96 | "nbconvert_exporter": "python", 97 | "pygments_lexer": "ipython3", 98 | "version": "3.8.13" 99 | }, 100 | "vscode": { 101 | "interpreter": { 102 | "hash": "baec60536c6749885c57d3beb549b4412d50c1c1ea218f0ac711a9872f2242c3" 103 | } 104 | } 105 | }, 106 | "nbformat": 4, 107 | "nbformat_minor": 5 108 | } 109 | -------------------------------------------------------------------------------- /main_webapp_launcher.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import logging 3 | import os 4 | import sys 5 | 6 | from waitress import serve 7 | 8 | if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): 9 | os.chdir(sys._MEIPASS) 10 | curr_folder = os.path.dirname(sys.executable) 11 | else: 12 | curr_folder = sys.path[0] 13 | 14 | import argparse 15 | import webbrowser 16 | 17 | from pcgsepy.config import ACTIVE_LOGGERS, BIN_N 18 | from pcgsepy.evo.fitness import (Fitness, box_filling_fitness, 19 | func_blocks_fitness, mame_fitness, 20 | mami_fitness) 21 | from pcgsepy.evo.genops import expander 22 | from pcgsepy.guis.main_webapp.webapp import app, serve_layout, app_settings 23 | from pcgsepy.mapelites.behaviors import (BehaviorCharacterization, avg_ma, 24 | mame, mami, symmetry) 25 | from pcgsepy.setup_utils import get_default_lsystem, setup_matplotlib 26 | from pcgsepy.mapelites.buffer import Buffer, mean_merge 27 | from pcgsepy.nn.estimators import GaussianEstimator 28 | from pcgsepy.mapelites.map import MAPElites 29 | from pcgsepy.mapelites.emitters import ContextualBanditEmitter, RandomEmitter 30 | from sklearn.gaussian_process.kernels import DotProduct, WhiteKernel 31 | 32 | parser = argparse.ArgumentParser() 33 | parser.add_argument("--mapelites_file", help="Location of the MAP-Elites object", 34 | type=str, default=None) 35 | parser.add_argument("--dev_mode", help="Launch the webapp in developer mode", 36 | action='store_true') 37 | parser.add_argument("--debug", help="Launch the webapp in debug mode", 38 | action='store_true') 39 | parser.add_argument("--host", help="Specify host address", 40 | type=str, default='127.0.0.1') 41 | parser.add_argument("--port", help="Specify port", 42 | type=int, default=8050) 43 | 44 | args = parser.parse_args() 45 | 46 | logging.getLogger('werkzeug').setLevel(logging.ERROR) 47 | 48 | current_datetime = datetime.now().strftime("%Y%m%d%H%M%S") 49 | file_handler = logging.FileHandler(filename=os.path.join(curr_folder, f'log_{current_datetime}.log'), 50 | mode='w+') 51 | file_handler.addFilter(lambda record: record.levelno >= logging.DEBUG) 52 | sysout_handler = logging.StreamHandler(sys.stdout) 53 | sysout_handler.addFilter(lambda record: record.levelno >= (logging.DEBUG if args.debug else logging.INFO)) 54 | 55 | logging.basicConfig(level=logging.WARNING, 56 | format='[%(asctime)s] %(message)s', 57 | handlers=[ 58 | sysout_handler, 59 | file_handler 60 | ]) 61 | 62 | for logger_name in ACTIVE_LOGGERS: 63 | logging.getLogger(logger_name).setLevel(logging.DEBUG) 64 | 65 | setup_matplotlib(larger_fonts=False) 66 | 67 | used_ll_blocks = [ 68 | 'MyObjectBuilder_CubeBlock_LargeBlockArmorCornerInv', 69 | 'MyObjectBuilder_CubeBlock_LargeBlockArmorCorner', 70 | 'MyObjectBuilder_CubeBlock_LargeBlockArmorSlope', 71 | 'MyObjectBuilder_CubeBlock_LargeBlockArmorBlock', 72 | 'MyObjectBuilder_Gyro_LargeBlockGyro', 73 | 'MyObjectBuilder_Reactor_LargeBlockSmallGenerator', 74 | 'MyObjectBuilder_CargoContainer_LargeBlockSmallContainer', 75 | 'MyObjectBuilder_Cockpit_OpenCockpitLarge', 76 | 'MyObjectBuilder_Thrust_LargeBlockSmallThrust', 77 | 'MyObjectBuilder_InteriorLight_SmallLight', 78 | 'MyObjectBuilder_CubeBlock_Window1x1Slope', 79 | 'MyObjectBuilder_CubeBlock_Window1x1Flat', 80 | 'MyObjectBuilder_InteriorLight_LargeBlockLight_1corner' 81 | ] 82 | 83 | lsystem = get_default_lsystem(used_ll_blocks=used_ll_blocks) 84 | 85 | expander.initialize(rules=lsystem.hl_solver.parser.rules) 86 | 87 | feasible_fitnesses = [ 88 | Fitness(name='BoxFilling', 89 | f=box_filling_fitness, 90 | bounds=(0, 1)), 91 | Fitness(name='FuncionalBlocks', 92 | f=func_blocks_fitness, 93 | bounds=(0, 1)), 94 | Fitness(name='MajorMediumProportions', 95 | f=mame_fitness, 96 | bounds=(0, 1)), 97 | Fitness(name='MajorMinimumProportions', 98 | f=mami_fitness, 99 | bounds=(0, 1)) 100 | ] 101 | 102 | behavior_descriptors = [ 103 | BehaviorCharacterization(name='Major axis / Medium axis', 104 | func=mame, 105 | bounds=(0, 6)), 106 | BehaviorCharacterization(name='Major axis / Smallest axis', 107 | func=mami, 108 | bounds=(0, 12)), 109 | BehaviorCharacterization(name='Average Proportions', 110 | func=avg_ma, 111 | bounds=(0, 10)), 112 | BehaviorCharacterization(name='Symmetry', 113 | func=symmetry, 114 | bounds=(0, 1)) 115 | ] 116 | 117 | buffer = Buffer(merge_method=mean_merge) 118 | mapelites = MAPElites(lsystem=lsystem, 119 | feasible_fitnesses=feasible_fitnesses, 120 | estimator=GaussianEstimator(bound='upper', 121 | kernel=DotProduct() + WhiteKernel(), 122 | max_f=sum([f.bounds[1] for f in feasible_fitnesses])), 123 | buffer=buffer, 124 | behavior_descriptors=behavior_descriptors, 125 | n_bins=BIN_N, 126 | emitter=ContextualBanditEmitter(estimator='mlp', 127 | tau=0.5, 128 | sampling_decay=0.05)) 129 | mapelites.allow_aging = False 130 | 131 | mapelites.hull_builder.apply_smoothing = False 132 | 133 | app_settings.initialize(mapelites=mapelites, 134 | dev_mode=args.dev_mode) 135 | 136 | app.layout = serve_layout 137 | 138 | webapp_url = f'http://{args.host}:{args.port}/' 139 | logging.getLogger().info(f'Serving webapp on http://{args.host}:{args.port}/...') 140 | webbrowser.open_new(webapp_url) 141 | 142 | # close the splash screen if launched via application 143 | try: 144 | import pyi_splash 145 | if pyi_splash.is_alive(): 146 | pyi_splash.close() 147 | except ModuleNotFoundError as e: 148 | pass 149 | 150 | serve(app.server, 151 | threads=16, 152 | host=args.host, 153 | port=args.port) 154 | -------------------------------------------------------------------------------- /media/UI_comparator_preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/media/UI_comparator_preview.jpg -------------------------------------------------------------------------------- /media/UI_devmode_preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/media/UI_devmode_preview.jpg -------------------------------------------------------------------------------- /media/UI_usermode_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/media/UI_usermode_preview.png -------------------------------------------------------------------------------- /media/UI_userstudy_preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/media/UI_userstudy_preview.jpg -------------------------------------------------------------------------------- /media/pcgsepy_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/media/pcgsepy_banner.png -------------------------------------------------------------------------------- /media/tilesmaker_preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/media/tilesmaker_preview.jpg -------------------------------------------------------------------------------- /pcgsepy/.gitignore: -------------------------------------------------------------------------------- 1 | .ipynb_checkpoints/* 2 | __pycache__/* 3 | *.pyc -------------------------------------------------------------------------------- /pcgsepy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/pcgsepy/__init__.py -------------------------------------------------------------------------------- /pcgsepy/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/pcgsepy/common/__init__.py -------------------------------------------------------------------------------- /pcgsepy/common/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/pcgsepy/common/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /pcgsepy/common/__pycache__/api_call.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/pcgsepy/common/__pycache__/api_call.cpython-38.pyc -------------------------------------------------------------------------------- /pcgsepy/common/__pycache__/vecs.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/pcgsepy/common/__pycache__/vecs.cpython-38.pyc -------------------------------------------------------------------------------- /pcgsepy/common/jsonifier.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pickle 3 | from typing import IO, Any 4 | 5 | 6 | # Adapted from https://stackoverflow.com/questions/18478287/making-object-json-serializable-with-regular-encoder/18561055#18561055 7 | class PythonObjectEncoder(json.JSONEncoder): 8 | def default(self, obj): 9 | return {'_python_object': pickle.dumps(obj).decode('latin1')} 10 | 11 | 12 | def _as_python_object(dct): 13 | try: 14 | return pickle.loads(dct['_python_object'].encode('latin1')) 15 | except KeyError: 16 | return dct 17 | 18 | 19 | def json_dump(obj: Any, 20 | fp: IO[str]) -> None: 21 | """Dump a Python object to file as JSON. 22 | Non-serializable Python objects are first converted to `str` of `pickle`. 23 | 24 | Args: 25 | obj (Any): The Python object. 26 | fp (IO[str]): The file pointer. 27 | """ 28 | json.dump(obj=obj, 29 | fp=fp, 30 | cls=PythonObjectEncoder) 31 | 32 | 33 | def json_load(fp: IO[str]) -> Any: 34 | """Load a Python object from JSON file. 35 | Non-serializable Python objects are loaded from their `pickle`d `str` representation. 36 | 37 | Args: 38 | fp (IO[str]): The file pointer. 39 | 40 | Returns: 41 | Any: The Python object. 42 | """ 43 | return json.load(fp=fp, 44 | object_hook=_as_python_object) 45 | 46 | 47 | def json_dumps(obj: Any) -> str: 48 | """Dump a Python object to JSON `str`. 49 | Non-serializable Python objects are first converted to `str` of `pickle`. 50 | 51 | Args: 52 | obj (Any): The Python object. 53 | 54 | Returns: 55 | str: The JSON `str`. 56 | """ 57 | return json.dumps(obj=obj, 58 | cls=PythonObjectEncoder) 59 | 60 | 61 | def json_loads(s: str) -> Any: 62 | """Load a Python object from JSON `str`. 63 | Non-serializable Python objects are loaded from their `pickle`d `str` representation. 64 | 65 | Args: 66 | s (str): The JSON `str`. 67 | 68 | Returns: 69 | Any: The Python object. 70 | """ 71 | return json.loads(s=s, 72 | object_hook=_as_python_object) 73 | -------------------------------------------------------------------------------- /pcgsepy/common/regex_handler.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | import re 3 | from typing import Tuple 4 | from functools import total_ordering 5 | 6 | 7 | @total_ordering 8 | class MyMatch: 9 | def __init__(self, 10 | lhs: str, 11 | span: Tuple[int, int], 12 | lhs_string: str) -> None: 13 | self.lhs = lhs 14 | self.start = span[0] 15 | self.end = span[1] 16 | self.lhs_string = lhs_string 17 | 18 | def __eq__(self, __o: object) -> bool: 19 | return isinstance(__o, MyMatch) and self.start == __o.start and self.end == __o.end 20 | 21 | def __lt__(self, __o: object) -> bool: 22 | return isinstance(__o, MyMatch) and (self.start < __o.start or (self.start == __o.start and self.end > __o.end)) 23 | 24 | def __str__(self) -> str: 25 | return f'{self.lhs=}; self.span={self.start},{self.end}; {self.lhs_string=}' 26 | 27 | def __repr__(self) -> str: 28 | return str(self) 29 | 30 | 31 | char_to_re = { 32 | '(': '\(', 33 | ')': '\)', 34 | '[': '\[', 35 | ']': '\]', 36 | 'x': '\d', 37 | 'X': '\d', 38 | 'Y': '\d', 39 | } 40 | 41 | 42 | def extract_regex(lhs: str) -> str: 43 | """Extract the regex from the LHS rule. 44 | 45 | Args: 46 | lhs (str): The LHS rule (human-readable). 47 | 48 | Returns: 49 | str: The compiled regex. 50 | """ 51 | r = copy(lhs) 52 | for k, v in char_to_re.items(): 53 | r = r.replace(k, v) 54 | return re.compile(r) -------------------------------------------------------------------------------- /pcgsepy/common/str_utils.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | 3 | 4 | def get_atom_indexes(string: str, 5 | atom: str) -> List[Tuple[int, int]]: 6 | """Get the indexes of the positions of the given atom in the string. 7 | Args: 8 | string (str): The string. 9 | atom (str): The atom. 10 | Returns: 11 | List[Tuple[int, int]]: The list of pair indexes. 12 | """ 13 | indexes = [] 14 | for i, _ in enumerate(string): 15 | if string[i:].startswith(atom): 16 | cb = string.find(')', i + len(atom)) 17 | indexes.append((i, cb)) 18 | i = cb 19 | return indexes 20 | 21 | 22 | def get_matching_brackets(string: str) -> List[Tuple[int, int]]: 23 | """Get indexes of matching square brackets. 24 | 25 | Args: 26 | string (str): The string. 27 | 28 | Returns: 29 | List[Tuple[int, int]]: The list of pair indexes. 30 | """ 31 | brackets = [] 32 | for i, c in enumerate(string): 33 | if c == '[': 34 | # find first closing bracket 35 | idx_c = string.index(']', i) 36 | # update closing bracket position in case of nested brackets 37 | ni_o = string.find('[', i + 1) 38 | while ni_o != -1 and string.find('[', ni_o) < idx_c: 39 | idx_c = string.index(']', idx_c + 1) 40 | ni_o = string.find('[', ni_o + 1) 41 | # add to list of brackets 42 | brackets.append((i, idx_c)) 43 | return brackets 44 | -------------------------------------------------------------------------------- /pcgsepy/config.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import os 3 | import sys 4 | 5 | config = configparser.ConfigParser() 6 | 7 | if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): 8 | os.chdir(sys._MEIPASS) 9 | curr_dir = os.path.dirname(sys.executable) 10 | else: 11 | curr_dir = sys.path[0] 12 | 13 | # curr_dir = os.getcwd() 14 | config.read(os.path.join(curr_dir, 'configs.ini')) 15 | 16 | USE_TORCH = config['LIBRARY'].getboolean('use_torch') 17 | ACTIVE_LOGGERS = [x for x in config['LIBRARY'].get('active_loggers').split(',')] 18 | 19 | HOST = config['API'].get('host') 20 | PORT = config['API'].getint('port') 21 | 22 | # json file with common atoms and action+args 23 | COMMON_ATOMS = config['L-SYSTEM'].get('common_atoms') 24 | # json file with high level atoms and dimensions 25 | HL_ATOMS = config['L-SYSTEM'].get('hl_atoms') 26 | # ranges of parametric l-system 27 | pl_range = config['L-SYSTEM'].get('pl_range').strip().split(',') 28 | PL_LOW, PL_HIGH = int(pl_range[0]), int(pl_range[1]) 29 | # required tiles for constraint components_constraint 30 | REQ_TILES = config['L-SYSTEM'].get('req_tiles').split(',') 31 | # L-system variables 32 | # number of iterations (high level) 33 | N_ITERATIONS = config['L-SYSTEM'].getint('n_iterations') 34 | # number of axioms generated at each expansion step 35 | N_SPE = config['L-SYSTEM'].getint('n_axioms_generated') 36 | 37 | # initial mutation probability 38 | MUTATION_INITIAL_P = config['GENOPS'].getfloat('mutations_initial_p') 39 | # mutation decay 40 | MUTATION_DECAY = config['GENOPS'].getfloat('mutations_decay') 41 | # crossover probability 42 | CROSSOVER_P = config['GENOPS'].getfloat('crossover_p') 43 | 44 | # population size 45 | POP_SIZE = config['FI2POP'].getint('population_size') 46 | # number of initialization retries 47 | N_RETRIES = config['FI2POP'].getint('n_initial_retries') 48 | # number of generations 49 | N_GENS = config['FI2POP'].getint('n_generations') 50 | # maximum string length (-1 for unlimited lenght) 51 | MAX_STRING_LEN = config['FI2POP'].getint('max_string_len') 52 | # maximum patience when generating new pools 53 | GEN_PATIENCE = config['FI2POP'].getint('gen_patience') 54 | 55 | # use or don't use the bounding box fitness 56 | USE_BBOX = config['FITNESS'].get('use_bounding_box') 57 | if USE_BBOX: 58 | bbox = config['FITNESS'].get('bounding_box').split(',') 59 | # bounding box upper limits 60 | BBOX_X, BBOX_Y, BBOX_Z = float(bbox[0]), float(bbox[1]), float(bbox[2]) 61 | MAME_MEAN = config['FITNESS'].getfloat('mame_mean') 62 | MAME_STD = config['FITNESS'].getfloat('mame_std') 63 | MAMI_MEAN = config['FITNESS'].getfloat('mami_mean') 64 | MAMI_STD = config['FITNESS'].getfloat('mami_std') 65 | MAX_X_SIZE = config['MAPELITES'].getint('max_x_size') 66 | MAX_Y_SIZE = config['MAPELITES'].getint('max_y_size') 67 | MAX_Z_SIZE = config['MAPELITES'].getint('max_z_size') 68 | 69 | # number of solutions per bin 70 | BIN_POP_SIZE = config['MAPELITES'].getint('bin_population') 71 | BIN_N = config['MAPELITES'].get('bin_n').split(',') 72 | BIN_N = tuple([int(x) for x in BIN_N]) 73 | # maximum age of solutions 74 | CS_MAX_AGE = config['MAPELITES'].getint('max_age') 75 | # PCA dimensions 76 | N_DIM_RED = config['MAPELITES'].getint('n_dimensions_reduced') 77 | # maximum number of dimensions PCA can analyze 78 | MAX_DIMS_RED = config['MAPELITES'].getint('max_possible_dimensions') 79 | # minimum fitness assignable 80 | EPSILON_F = config['MAPELITES'].getfloat('epsilon_fitness') 81 | # interval for realigning infeasible fitnesses 82 | ALIGNMENT_INTERVAL = config['MAPELITES'].getint('alignment_interval') 83 | # rescale infeasible fitness with reporduction probability 84 | RESCALE_INFEAS_FITNESS = config['MAPELITES'].getboolean('rescale_infeas_fitness') 85 | # minimum subdivision percentage of a bin 86 | BIN_SMALLEST_PERC = config['MAPELITES'].getfloat('bin_min_resolution') 87 | # whether to use a linear estimator or a NN estimator in the emitter (if possible) 88 | USE_LINEAR_ESTIMATOR = config['MAPELITES'].getboolean('use_linear_estimator') 89 | 90 | N_EPOCHS = config['MAPELITES'].getint('n_epochs') 91 | X_RANGE = tuple([int(x) for x in config['MAPELITES'].get('x_range').split(',')]) 92 | Y_RANGE = tuple([int(x) for x in config['MAPELITES'].get('y_range').split(',')]) 93 | Z_RANGE = tuple([int(x) for x in config['MAPELITES'].get('z_range').split(',')]) 94 | 95 | # number of experiments to run 96 | N_RUNS = config['EXPERIMENT'].getint('n_runs') 97 | # name of the current experiment 98 | EXP_NAME = config['EXPERIMENT'].get('exp_name') 99 | 100 | # number of automated emitters steps 101 | N_EMITTER_STEPS = config['USER-STUDY'].getint('n_emitter_steps') 102 | 103 | CONTEXT_IDXS = config['USER-STUDY'].get('context_idxs').split(',') 104 | CONTEXT_IDXS = [int(x) for x in CONTEXT_IDXS] 105 | 106 | BETA_A = config['USER-STUDY'].getint('beta_a') 107 | BETA_B = config['USER-STUDY'].getint('beta_b') 108 | -------------------------------------------------------------------------------- /pcgsepy/evo/.gitignore: -------------------------------------------------------------------------------- 1 | .ipynb_checkpoints/* 2 | __pycache__/* 3 | *pyc -------------------------------------------------------------------------------- /pcgsepy/evo/fitness.py: -------------------------------------------------------------------------------- 1 | import math 2 | import pickle 3 | from typing import Any, Callable, Dict, Tuple 4 | 5 | import numpy as np 6 | from pcgsepy.config import BBOX_X, BBOX_Y, BBOX_Z 7 | from pcgsepy.lsystem.solution import CandidateSolution 8 | from scipy.stats import gaussian_kde 9 | 10 | # load pickled estimators 11 | with open('./estimators/futo.pkl', 'rb') as f: 12 | futo_es: gaussian_kde = pickle.load(f) 13 | with open('./estimators/tovo.pkl', 'rb') as f: 14 | tovo_es: gaussian_kde = pickle.load(f) 15 | with open('./estimators/mame.pkl', 'rb') as f: 16 | mame_es: gaussian_kde = pickle.load(f) 17 | with open('./estimators/mami.pkl', 'rb') as f: 18 | mami_es: gaussian_kde = pickle.load(f) 19 | # Compute max values for estimators to normalize fitnesses 20 | # values are chosen upon inspection. 21 | x = np.linspace(0, 0.5, int(0.5 / 0.005)) 22 | futo_max = float(np.max(futo_es.evaluate(x))) 23 | x = np.linspace(0, 1, int(1 / 0.005)) 24 | tovo_max = float(np.max(tovo_es.evaluate(x))) 25 | x = np.linspace(0, 6, int(6 / 0.005)) 26 | mame_max = float(np.max(mame_es.evaluate(x))) 27 | x = np.linspace(0, 10, int(10 / 0.005)) 28 | mami_max = float(np.max(mami_es.evaluate(x))) 29 | 30 | 31 | def bounding_box_fitness(cs: CandidateSolution) -> float: 32 | """Measure how close the structure fits in the bounding box. 33 | Penalizes in both ways. 34 | Normalized in [0,1]. 35 | 36 | Args: 37 | cs (CandidateSolution): The candidate solution. 38 | 39 | Returns: 40 | float: The fitness value. 41 | """ 42 | x, y, z = cs.content.as_array.shape 43 | f = np.clip((BBOX_X - abs(BBOX_X - x)) / BBOX_X, 0, 1) 44 | f += np.clip((BBOX_Y - abs(BBOX_Y - y)) / BBOX_Y, 0, 1) 45 | f += np.clip((BBOX_Z - abs(BBOX_Z - z)) / BBOX_Z, 0, 1) 46 | return f[0] / 3 47 | 48 | 49 | def box_filling_fitness(cs: CandidateSolution) -> float: 50 | """Measures how much of the total volume is filled with blocks. 51 | Normalized in [0,1]. 52 | 53 | Args: 54 | cs (CandidateSolution): The candidate solution. 55 | 56 | Returns: 57 | float: The fitness value. 58 | """ 59 | return tovo_es.evaluate(sum([b.volume for b in cs.content._blocks.values()]) / math.prod(cs.content.as_array.shape))[0] / tovo_max 60 | 61 | 62 | def func_blocks_fitness(cs: CandidateSolution) -> float: 63 | """Measures how much of the total blocks is functional blocks. 64 | Normalized in [0,1]. 65 | 66 | Args: 67 | cs (CandidateSolution): The candidate solution. 68 | 69 | Returns: 70 | float: The fitness value. 71 | """ 72 | fu, to = 0., 0. 73 | for b in cs.content._blocks.values(): 74 | fu += b.volume if not b.block_type.startswith('MyObjectBuilder_CubeBlock_') else 0 75 | to += b.volume 76 | return futo_es.evaluate(fu / to)[0] / futo_max 77 | 78 | 79 | def mame_fitness(cs: CandidateSolution) -> float: 80 | """Measures the proportions of the largest and medium axis. 81 | Normalized in [0,1]. 82 | 83 | Args: 84 | cs (CandidateSolution): The candidate solution. 85 | 86 | Returns: 87 | float: The fitness value. 88 | """ 89 | largest_axis, medium_axis, _ = reversed(sorted(list(cs.content.as_array.shape))) 90 | return mame_es.evaluate(largest_axis / medium_axis)[0] / mame_max 91 | 92 | 93 | def mami_fitness(cs: CandidateSolution) -> float: 94 | """Measures the proportions of the largest and smallest axis. 95 | Normalized in [0,1] 96 | 97 | Args: 98 | cs (CandidateSolution): The candidate solution. 99 | 100 | Returns: 101 | float: The fitness value. 102 | """ 103 | largest_axis, _, smallest_axis = reversed(sorted(list(cs.content.as_array.shape))) 104 | return mami_es.evaluate(largest_axis / smallest_axis)[0] / mami_max 105 | 106 | 107 | fitness_functions = { 108 | 'bounding_box_fitness': bounding_box_fitness, 109 | 'box_filling_fitness': box_filling_fitness, 110 | 'func_blocks_fitness': func_blocks_fitness, 111 | 'mame_fitness': mame_fitness, 112 | 'mami_fitness': mami_fitness 113 | } 114 | 115 | 116 | class Fitness: 117 | def __init__(self, 118 | name: str, 119 | f: Callable[[CandidateSolution, Dict[str, Any]], float], 120 | bounds: Tuple[float, float], 121 | weight: float = 1.0): 122 | self.name = name 123 | self.f = f 124 | self.bounds = bounds 125 | self.weight = weight 126 | 127 | def __repr__(self) -> str: 128 | return str(self.__dict__) 129 | 130 | def __str__(self) -> str: 131 | return f'Fitness {self.name} (in {self.bounds})' 132 | 133 | def __call__(self, 134 | cs: CandidateSolution) -> float: 135 | return self.f(cs=cs) 136 | 137 | def to_json(self) -> Dict[str, Any]: 138 | return { 139 | 'name': self.name, 140 | 'f': self.f.__name__, 141 | 'bounds': list(self.bounds), 142 | 'weight': self.weight 143 | } 144 | 145 | @staticmethod 146 | def from_json(my_args: Dict[str, Any]) -> 'Fitness': 147 | return Fitness(name=my_args['name'], 148 | f=fitness_functions[my_args['f']], 149 | bounds=tuple(my_args['bounds']), 150 | weight=my_args['weight']) 151 | -------------------------------------------------------------------------------- /pcgsepy/guis/assets/algo_info.md: -------------------------------------------------------------------------------- 1 | This application uses an [evolutionary algorithm](https://en.wikipedia.org/wiki/Evolutionary_algorithm) to generate spaceships. Evolutionary algorithms are a family of optimisation algorithms that use operations such as "mutation" and "crossover" on individuals within a population in order to construct new individuals to satisfy a given goal. In this case, the goal is to generate interesting spaceships that can be successfully piloted in-game. 2 | 3 | Each spaceship is defined by a string which describes the spaceship's tiles and rotations. This string is generated by an [L-system](https://wikipedia.org/wiki/L-system) and modified by the [FI-2Pop genetic algorithm](https://www.sciencedirect.com/science/article/abs/pii/S0377221707005668). The fitness of a solution (i.e.: how *good* it is) is based on four different measures we extracted from the most voted spaceships on the Steam Workshop. 4 | 5 | The spaceships are subdivided in groups according to their characteristics. The grid you see on the left is the *behavioral grid* of [MAP-Elites](https://arxiv.org/abs/1504.04909). The different configurations you will interact with during the user study rely on different *emitters*, which determine which group of spaceship to use during the automated steps. 6 | 7 | If you want to know more about this system, why not check out our previous publications? 8 | Our first paper introduces the L-system and FI-2Pop 9 | > Gallotta, R., Arulkumaran, K., & Soros, L. B. (2022). Evolving Spaceships with a Hybrid L-system Constrained Optimisation Evolutionary Algorithm. In Genetic and Evolutionary Computation Conference Companion. https://dl.acm.org/doi/abs/10.1145/3520304.3528775 10 | 11 | and our second paper explains how we improved the FI-2Pop algorithm to produce valid spaceships more reliably, as well as introducing the MAP-Elites' emitters in our domain 12 | > Gallotta, R., Arulkumaran, K., & Soros, L. B. (2022). Surrogate Infeasible Fitness Acquirement FI-2Pop for Procedural Content Generation. In IEEE Conference on Games. https://ieeexplore.ieee.org/document/9893592 13 | 14 | Finally, in our third paper we introduced the preference-learning emitters (PLE) framework, where different types of emitters were used to learn user preferences 15 | > Gallotta, R., Arulkumaran, K., & Soros, L. B. (2022). Preference-Learning Emitters for Mixed-Initiative Quality-Diversity Algorithms. arXiv preprint arXiv:2210.13839. https://arxiv.org/abs/2210.13839) -------------------------------------------------------------------------------- /pcgsepy/guis/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/pcgsepy/guis/assets/favicon.ico -------------------------------------------------------------------------------- /pcgsepy/guis/assets/help.md: -------------------------------------------------------------------------------- 1 | How to use the program: 2 | 3 | ##### Load data 4 | Load the `.json` files you obtained from the previous step of the experiment here to preview the different spaceships. 5 | 6 | ##### Rank the spaceships 7 | Rank the different spaceships according to your preferences using the menus below the spaceship preview. Remember to rank them with different scores! 8 | 9 | ##### Saving scores 10 | Once you're happy with the scores, click the "Save" button to download the results! You will need to upload this file in the final part of the questionnaire. -------------------------------------------------------------------------------- /pcgsepy/guis/assets/privacy_policy.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/pcgsepy/guis/assets/privacy_policy.pdf -------------------------------------------------------------------------------- /pcgsepy/guis/assets/quickstart.md: -------------------------------------------------------------------------------- 1 | This application generates spaceships for Space Engineers using an evolutionary algorithm. New spaceships can continually be "evolved" from an initial population of spaceships. 2 | 3 | You can select a spaceship from the population on the left; all colored cells can be selected. The spaceship you selected will change its symbol to "☑". You can click the "**Evolve from selected spaceship**" button to generate new spaceships. After new spaceships are generated, the symbol "▣" will mark the cells in the grid with a new spaceship. 4 | 5 | When you select a spaceship, its main properties are displayed on the right, and an interactive preview of the spaceship is displayed in the middle of the application window. You can also download a spaceship by clicking the corresponding button next to the spaceship properties. You can also change the base color of the spaceship by picking it from the widget next to the spaceship properties. 6 | 7 | You can check system messages in the "**Log**" window on the right. 8 | 9 | 10 | You can also follow this video tutorial: 11 | 12 | [![Tutorial Video](https://img.youtube.com/vi/bVASWQj6DHc/0.jpg)](https://youtu.be/bVASWQj6DHc "Space Engineers AI Spaceship Generator] User-study Quick start") -------------------------------------------------------------------------------- /pcgsepy/guis/assets/quickstart_usermode.md: -------------------------------------------------------------------------------- 1 | This application generates spaceships for Space Engineers using an evolutionary algorithm. New spaceships can continually be "evolved" from an initial population of spaceships. 2 | 3 | You can select a spaceship from the population on the left; all colored cells can be selected. The spaceship you selected will change its symbol to "☑". You can click the "**Evolve from selected spaceship**" button to generate new spaceships. After new spaceships are generated, the symbol "▣" will mark the cells in the grid with a new spaceship. 4 | 5 | When you select a spaceship, its main properties are displayed on the right, and an interactive preview of the spaceship is displayed in the middle of the application window. You can also download a spaceship by clicking the corresponding button next to the spaceship properties. You can also change the base color of the spaceship by picking it from the widget next to the spaceship properties. 6 | 7 | You can check system messages in the "**Log**" window on the right. 8 | 9 | You can reinitialize the population by clicking the corresponding button (beware: it may take a while for the reinitialization to complete). 10 | 11 | You can also toggle between safe and unsafe mode. In safe mode, we ensure thrusters are placed on all six sides of the spaceships, at the cost of less diversity. Switching between modes requires the population to be reinitialized. 12 | 13 | The "Evolution iterations" slider will allow you to change how many steps the automated evolution takes. While this will increase the chances of obtaining more spaceships, it also comes at the cost of additional time per iteration, so be mindful when setting it! 14 | 15 | You can also follow this video tutorial: 16 | 17 | [![Tutorial Video](https://img.youtube.com/vi/bVASWQj6DHc/0.jpg)](https://youtu.be/bVASWQj6DHc "Space Engineers AI Spaceship Generator] User-study Quick start") -------------------------------------------------------------------------------- /pcgsepy/guis/assets/style.css: -------------------------------------------------------------------------------- 1 | .title { 2 | padding: 20px; 3 | text-align: center; 4 | } 5 | .page-description { 6 | padding: 10px; 7 | text-align: left; 8 | } 9 | .header { 10 | background-image: linear-gradient(#464d55, #222222); 11 | color: rgb(238, 238, 238); 12 | } 13 | .footer { 14 | background-image: linear-gradient(#222222, #464d55); 15 | color: #EEEEEE; 16 | } 17 | .section-title { 18 | text-align: center; 19 | } 20 | .content-string-area { 21 | height: 200px; 22 | } 23 | .log-area { 24 | height: 200px; 25 | } 26 | .rules-area { 27 | height: 600px; 28 | } 29 | .button-fullsize { 30 | width: 100%; 31 | overflow: auto; 32 | } 33 | .spacer { 34 | margin-top: 1%; 35 | } 36 | .container { 37 | display: grid; 38 | } 39 | .content, .overlay { 40 | position: relative; 41 | grid-area: 1 / 1; 42 | } 43 | .help { 44 | cursor: help 45 | } 46 | .upload { 47 | text-align: center; 48 | display: inline-flex; 49 | flex-wrap: nowrap; 50 | flex-direction: column-reverse; 51 | justify-content: center; 52 | align-items: center; 53 | border: deepskyblue; 54 | border-width: thick; 55 | border-style: dotted; 56 | border-radius: 10%; 57 | } -------------------------------------------------------------------------------- /pcgsepy/guis/assets/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/pcgsepy/guis/assets/thumb.png -------------------------------------------------------------------------------- /pcgsepy/guis/assets/webapp_help_dev.md: -------------------------------------------------------------------------------- 1 | How to use the program: 2 | ##### Plot settings 3 | Here it is possible to change the data that is being visualized in the MAP-Elites plot (left) and content plot (right). 4 | 5 | It is possible to visualize behavior characteristics for both the feasible and infeasible population. It is possible to plot either the Fitness, the Age, and the Coverage metric for all solutions. Additionally, it is possible to plot metrics for only the elite or the bin population. 6 | 7 | By selecting a bin in the MAP-Elites plot, the elite content will be displayed in the content plot (right) and the content properties (spaceship size, number of blocks used, and string representation) will be shown next to it. Additionally, the selected spaceship can be downloaded by clicking the "DOWNLOAD CONTENT" button at any time. You can also change the base color of the spaceship by picking it from the widget next to the spaceship properties. 8 | 9 | ##### Experiment settings & control 10 | Here there are both information on the experiment and buttons that allow the user to interact with the evolution procedure. 11 | 12 | In particular: 13 | - **Experiment settings** shows the bins that are valid for evolution, the current generation, and the bins selected by the user. There are also different controls that the user can modify: 14 | - **Choose feature descriptors (X, Y)**: here you can select which behaviour characteristics to use in MAP-Elites. 15 | - **Toggle L-system modules**: here you can select which parts of the spaceship are allowed to mutate. 16 | - **Control fitness weights**: here you can choose how much impact each metric has on the overall fitness. Slider values go from 0.0 to 1.0, and the MAP-Elites preview is updated accordingly. 17 | - **Select emitter**: here you can select which emitter to use during the experiment. Note that changing the emitter will create a new one, so all emitter data collected thus far will be lost! 18 | 19 | - **Experiment controls** is comprised of different buttons: 20 | - **APPLY STEP**: Executes a step of FI-2Pop with the selected bin(s) populations. If no bin is selected, or if the selected bin(s) is invalid, an error is thrown and no step is executed. 21 | - **INITIALIZE/RESET**: Either initializes the entire population of solutions (if empty) or resets it. 22 | - **CLEAR SELECTION**: Clears the selected bin(s). 23 | - **TOGGLE BIN SELECTION**: Toggles evolution on a single bin or multiple bins. If toggled to false and more than one bin were selected, only the last bin will remain selected. 24 | - **SUBDIVIDE SELECTED BIN(S)**: Subdivides the selected bin(s) in half, reassigning the solutions to the correct bin. 25 | - **DOWNLOAD MAP-ELITES**: Downloads the MAP-Elites object. This is only possible after a certain number of generations has elapsed. 26 | 27 | ##### High-level rules 28 | Here it is possible to inspect and update the high-level rules used by the L-system. When updating a rule, a check is always performed to ensure the expansion probability of the left-hand side rule sums up to 1. 29 | 30 | ##### Log 31 | All log messages are relayed here. As some operations may take some time to complete, progress messages are also reported. -------------------------------------------------------------------------------- /pcgsepy/guis/assets/webapp_info.md: -------------------------------------------------------------------------------- 1 | How to use the program: 2 | 3 | ##### Plots 4 | There are two plots that you can interact with: 5 | 6 | * Spaceship Population: here you can select spaceships based on their characteristics; only colored cells be selected. The spaceship you selected will change its symbol to "☑". After new spaceships are generated, the symbol "▣" will mark the cells in the grid with a new spaceship. The color of the spaceship is determined by its *Fitness*, which is a measure of how "good" the spaceship is. You can zoom using the scrollwheel and pan by keeping the left mouse button pressed and dragging the plot around. 7 | * Spaceship Preview: here you can explore a preview of the spaceship you selected. Each sphere in the plot represents a game block and you can tell which block it is simply by hovering over it with the mouse cursor. The color wheel on the right allows you to change the base color of the spaceship. 8 | 9 | ##### Properties & Download 10 | Once you select a spaceship, its properties are displayed in the table on the right. You can also download the currently selected spaceship as a `.zip` file by clicking the **Download** button. The compressed folder, located in your default download directory, contains the files needed to load the spaceship in Space Engineers as a blueprint, as well as a program-specific file used in the "Spaceship Comparator" application. Simply place the unzipped folder in `..\AppData\Roaming\SpaceEngineers\Blueprints\local` and load Space Engineers. In a scenario world, press `Ctrl+F10` to bring up the **Blueprints** window and you will see the spaceship listed among the local blueprints. 11 | 12 | ##### Generating new spaceships 13 | To generate new spaceships from the currently selected one, simply press the **Evolve from spaceship** button. A progress bar will appear on the left during the generation and will disappear once the process is completed. The new spaceships are automatically added to the "Spaceship Population" at the end of the generation. 14 | 15 | ##### Log 16 | All log messages are relayed here. As some operations may take some time to complete, progress messages are also reported. -------------------------------------------------------------------------------- /pcgsepy/guis/main_webapp/modals_msgs.py: -------------------------------------------------------------------------------- 1 | no_selection_error = "You must choose a spaceship from the grid on the left!" 2 | 3 | spaceship_population_help = """ 4 | The population is the collection of all spaceships generated by the program. The spaceships are subdivided according to the ratios of their dimensions (axis). 5 | 6 | Each cell in the grid corresponds to a small collection of spaceships with similar shape properties. By clicking a cell, the best spaceship in the cell is displayed in the "Spaceship Preview". 7 | 8 | Each spaceship has a "fitness" value assigned, which measures how _good_ the spaceship is according to different metrics. The higher the fitness, the better the spaceship is. 9 | """ 10 | 11 | spaceship_preview_help = """ 12 | You can click and drag the preview to rotate the spaceship, and use the mouse scrollwheel to zoom in or out. 13 | 14 | Hovering over the spaceship blocks preview will show the corresponding block type. 15 | """ 16 | 17 | download_help = """ 18 | ### Colors 19 | You can select a different color for the armor blocks in your spaceship! Simply pick a color from the palette below and press the "Apply" button to update the existing spaceships color. 20 | 21 | ### Download 22 | You can download the currently selected spaceship as a `.zip` file by clicking the **Download** button. 23 | The compressed folder, located in your default download directory, contains the files needed to load the spaceship in Space Engineers as a blueprint (`bp.sbc` and `thumb.png`), as well as a program-specific file used in the "Spaceship Comparator" application. 24 | 25 | Simply place the unzipped folder in `..\AppData\Roaming\SpaceEngineers\Blueprints\local` and load Space Engineers. 26 | In a scenario world, press `Ctrl+F10` to bring up the **Blueprints** window and you will see the spaceship listed among the local blueprints. 27 | """ 28 | 29 | 30 | toggle_safe_rules_off_msg = """ 31 | Do you really want to turn off safe mode? This will reset the current progress of evolution, because the population of spaceships will need to be re-initialized. 32 | 33 | By toggling off this feature, we will not check that thrusters are placed on all six sides of the spaceship, making it difficult to maneuver without manually editing the ship in Space Engineers. 34 | 35 | On the other hand, the evolution will be able to propose a more diverse population of spaceships, allowing you to explore a richer set of hull shapes. 36 | """ 37 | 38 | toggle_safe_rules_on_msg = """ 39 | Do you want to turn on safe mode again? This will reset the current progress of evolution, because the population of spaceships will need to be re-initialized. 40 | """ -------------------------------------------------------------------------------- /pcgsepy/guis/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from enum import Enum 3 | import logging 4 | import uuid 5 | from typing import Any, Dict, List, Optional, Tuple 6 | 7 | import numpy as np 8 | from pcgsepy.config import BIN_POP_SIZE, CS_MAX_AGE, N_EMITTER_STEPS 9 | from pcgsepy.mapelites.behaviors import (BehaviorCharacterization, avg_ma, 10 | mame, mami, symmetry) 11 | 12 | from pcgsepy.mapelites.map import MAPElites 13 | 14 | 15 | class Semaphore: 16 | def __init__(self, 17 | locked: bool = False) -> None: 18 | """Create a `Semamphore` object. 19 | 20 | Args: 21 | locked (bool, optional): Initial locked value. Defaults to False. 22 | """ 23 | self._is_locked = locked 24 | self._running = '' 25 | 26 | @property 27 | def is_locked(self) -> bool: 28 | """Check if the semaphore is currently locked. 29 | 30 | Returns: 31 | bool: The locked value. 32 | """ 33 | return self._is_locked 34 | 35 | def lock(self, 36 | name: Optional[str] = '') -> None: 37 | """Lock the semaphore. 38 | 39 | Args: 40 | name (Optional[str], optional): The locking process name. Defaults to ''. 41 | """ 42 | self._is_locked = True 43 | self._running = name 44 | 45 | def unlock(self) -> None: 46 | """Unlock the semaphore""" 47 | self._is_locked = False 48 | self._running = '' 49 | 50 | 51 | class DashLoggerHandler(logging.StreamHandler): 52 | def __init__(self): 53 | """Create a new logging handler. 54 | """ 55 | logging.StreamHandler.__init__(self) 56 | self.queue = [] 57 | 58 | def emit(self, 59 | record: Any) -> None: 60 | """Process the incoming record. 61 | 62 | Args: 63 | record (Any): The new logging record. 64 | """ 65 | t = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 66 | msg = self.format(record) 67 | self.queue.append(f'[{t}]\t{msg}') 68 | 69 | 70 | class AppMode(Enum): 71 | """Enumerator for the application mode.""" 72 | USER = 1 73 | DEV = 2 74 | 75 | 76 | class AppSettings: 77 | def __init__(self) -> None: 78 | """Generate a new `AppSettings` object.""" 79 | self.current_mapelites: Optional[MAPElites] = None 80 | self.gen_counter: int = 0 81 | self.hm_callback_props: Dict[str, Any] = {} 82 | self.behavior_descriptors: List[BehaviorCharacterization] = [ 83 | BehaviorCharacterization(name='Major axis / Medium axis', 84 | func=mame, 85 | bounds=(0, 10)), 86 | BehaviorCharacterization(name='Major axis / Smallest axis', 87 | func=mami, 88 | bounds=(0, 20)), 89 | BehaviorCharacterization(name='Average Proportions', 90 | func=avg_ma, 91 | bounds=(0, 20)), 92 | BehaviorCharacterization(name='Symmetry', 93 | func=symmetry, 94 | bounds=(0, 1)) 95 | ] 96 | self.rngseed = uuid.uuid4().int 97 | self.selected_bins: List[Tuple[int, int]] = [] 98 | self.step_progress: int = -1 99 | self.use_custom_colors: bool = True 100 | self.app_mode: AppMode = None 101 | self.emitter_steps: int = N_EMITTER_STEPS 102 | self.symmetry: Optional[str] = None 103 | self.safe_mode: bool = True 104 | self.voxelised: bool = False 105 | 106 | def initialize(self, 107 | mapelites: MAPElites, 108 | dev_mode: bool = False): 109 | """Initialize the object. 110 | 111 | Args: 112 | mapelites (MAPElites): The MAP-Elites object. 113 | dev_mode (bool, optional): Whether to set the application to developer mode. Defaults to False. 114 | """ 115 | self.current_mapelites = mapelites 116 | self.app_mode = AppMode.DEV if dev_mode else AppMode.USER 117 | self.hm_callback_props['pop'] = { 118 | 'Feasible': 'feasible', 119 | 'Infeasible': 'infeasible' 120 | } 121 | self.hm_callback_props['metric'] = { 122 | 'Fitness': { 123 | 'name': 'fitness', 124 | 'zmax': { 125 | 'feasible': sum([x.weight * x.bounds[1] for x in self.current_mapelites.feasible_fitnesses]) + self.current_mapelites.nsc, 126 | 'infeasible': 1. 127 | }, 128 | 'colorscale': 'Inferno' 129 | }, 130 | 'Age': { 131 | 'name': 'age', 132 | 'zmax': { 133 | 'feasible': CS_MAX_AGE, 134 | 'infeasible': CS_MAX_AGE 135 | }, 136 | 'colorscale': 'Aggrnyl' 137 | }, 138 | 'Coverage': { 139 | 'name': 'size', 140 | 'zmax': { 141 | 'feasible': BIN_POP_SIZE, 142 | 'infeasible': BIN_POP_SIZE 143 | }, 144 | 'colorscale': 'Hot' 145 | } 146 | } 147 | self.hm_callback_props['method'] = { 148 | 'Population': True, 149 | 'Elite': False 150 | } -------------------------------------------------------------------------------- /pcgsepy/guis/voxel.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | # modified code based off https://github.com/olive004/Plotly-voxel-renderer 5 | # TODO: improve readibility 6 | class VoxelData(): 7 | 8 | def __init__(self, data): 9 | self.data = data 10 | self.intensities = [] 11 | self.triangles = np.zeros((np.size(np.shape(self.data)), 1)) 12 | self.xyz = self.get_coords() 13 | self.x_length = np.size(data, 0) 14 | self.y_length = np.size(data, 1) 15 | self.z_length = np.size(data, 2) 16 | self.vert_count = 0 17 | self.vertices = self.make_edge_verts() 18 | self.triangles = np.delete(self.triangles, 0, 1) 19 | 20 | def get_coords(self): 21 | indices = np.nonzero(self.data) 22 | indices = np.stack((indices[0], indices[1], indices[2])) 23 | return indices 24 | 25 | def has_voxel(self, neighbor_coord): 26 | return self.data[neighbor_coord[0], neighbor_coord[1], neighbor_coord[2]] 27 | 28 | def get_neighbor(self, voxel_coords, direction): 29 | x = voxel_coords[0] 30 | y = voxel_coords[1] 31 | z = voxel_coords[2] 32 | offset_to_check = CubeData.offsets[direction] 33 | neighbor_coord = [x + offset_to_check[0], y + 34 | offset_to_check[1], z+offset_to_check[2]] 35 | 36 | # return 0 if neighbor out of bounds or nonexistent 37 | if (any(np.less(neighbor_coord, 0)) | (neighbor_coord[0] >= self.x_length) | (neighbor_coord[1] >= self.y_length) | (neighbor_coord[2] >= self.z_length)): 38 | return 0 39 | else: 40 | return self.has_voxel(neighbor_coord) 41 | 42 | def make_face(self, voxel, direction): 43 | voxel_coords = self.xyz[:, voxel] 44 | explicit_dir = CubeData.direction[direction] 45 | vert_order = CubeData.face_triangles[explicit_dir] 46 | 47 | next_i = [self.vert_count, self.vert_count] 48 | next_j = [self.vert_count+1, self.vert_count+2] 49 | next_k = [self.vert_count+2, self.vert_count+3] 50 | 51 | next_tri = np.vstack((next_i, next_j, next_k)) 52 | self.triangles = np.hstack((self.triangles, next_tri)) 53 | 54 | face_verts = np.zeros((len(voxel_coords), len(vert_order))) 55 | for i in range(len(vert_order)): 56 | face_verts[:, i] = voxel_coords + \ 57 | CubeData.cube_verts[vert_order[i]] 58 | 59 | self.vert_count = self.vert_count+4 60 | 61 | return face_verts 62 | 63 | def make_cube_verts(self, voxel): 64 | voxel_coords = self.xyz[:, voxel] 65 | cube = np.zeros((len(voxel_coords), 1)) 66 | 67 | # only make a new face if there's no neighbor in that direction 68 | dirs_no_neighbor = [] 69 | for direction in range(len(CubeData.direction)): 70 | if np.any(self.get_neighbor(voxel_coords, direction)): 71 | continue 72 | else: 73 | dirs_no_neighbor = np.append(dirs_no_neighbor, direction) 74 | face = self.make_face(voxel, direction) 75 | cube = np.append(cube, face, axis=1) 76 | 77 | m, n, p = voxel_coords 78 | self.intensities.extend([self.data[m, n, p]] * 2) 79 | 80 | # remove cube initialization 81 | cube = np.delete(cube, 0, 1) 82 | 83 | return cube 84 | 85 | def make_edge_verts(self): 86 | # make only outer vertices 87 | edge_verts = np.zeros((np.size(self.xyz, 0), 1)) 88 | num_voxels = np.size(self.xyz, 1) 89 | for voxel in range(num_voxels): 90 | # passing voxel num rather than 91 | cube = self.make_cube_verts(voxel) 92 | edge_verts = np.append(edge_verts, cube, axis=1) 93 | edge_verts = np.delete(edge_verts, 0, 1) 94 | return edge_verts 95 | 96 | 97 | class CubeData: 98 | # all data and knowledge from https://github.com/boardtobits/procedural-mesh-tutorial/blob/master/CubeMeshData.cs 99 | # for creating faces correctly by direction 100 | face_triangles = { 101 | 'North': [0, 1, 2, 3], # +y 102 | 'East': [5, 0, 3, 6], # +x 103 | 'South': [4, 5, 6, 7], # -y 104 | 'West': [1, 4, 7, 2], # -x 105 | 'Up': [5, 4, 1, 0], # +z 106 | 'Down': [3, 2, 7, 6] # -z 107 | } 108 | 109 | cube_verts = [ 110 | [1, 1, 1], 111 | [0, 1, 1], 112 | [0, 1, 0], 113 | [1, 1, 0], 114 | [0, 0, 1], 115 | [1, 0, 1], 116 | [1, 0, 0], 117 | [0, 0, 0], 118 | ] 119 | 120 | direction = [ 121 | 'North', 122 | 'East', 123 | 'South', 124 | 'West', 125 | 'Up', 126 | 'Down' 127 | ] 128 | 129 | opposing_directions = [ 130 | ['North', 'South'], 131 | ['East', 'West'], 132 | ['Up', 'Down'] 133 | ] 134 | 135 | # xyz direction corresponding to 'Direction' 136 | offsets = [ 137 | [0, 1, 0], 138 | [1, 0, 0], 139 | [0, -1, 0], 140 | [-1, 0, 0], 141 | [0, 0, 1], 142 | [0, 0, -1], 143 | ] 144 | -------------------------------------------------------------------------------- /pcgsepy/lsystem/actions.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | import numpy as np 3 | 4 | 5 | class AtomAction(str, Enum): 6 | """ 7 | Enumerator for actions defined for an axiom's atom. 8 | """ 9 | MOVE = 'move' 10 | PLACE = 'place' 11 | POP = 'pop' 12 | PUSH = 'push' 13 | ROTATE = 'rotate' 14 | 15 | 16 | class Rotations(str, Enum): 17 | """ 18 | Enumerator for all possible rotations. 19 | Format: 'Axis cw/ccw OtherAxis'. 20 | """ 21 | XcwY = 'XcwY' 22 | XcwZ = 'XcwZ' 23 | YcwX = 'YcwX' 24 | YcwZ = 'YcwZ' 25 | ZcwX = 'ZcwX' 26 | ZcwY = 'ZcwY' 27 | XccwY = 'XccwY' 28 | XccwZ = 'XccwZ' 29 | YccwX = 'YccwX' 30 | YccwZ = 'YccwZ' 31 | ZccwX = 'ZccwX' 32 | ZccwY = 'ZccwY' 33 | 34 | 35 | rotations_from_str = { 36 | 'XcwY': Rotations.XcwY, 37 | 'XcwZ': Rotations.XcwZ, 38 | 'YcwX': Rotations.YcwX, 39 | 'YcwZ': Rotations.YcwZ, 40 | 'ZcwX': Rotations.ZcwX, 41 | 'ZcwY': Rotations.ZcwY, 42 | 'XccwY': Rotations.XccwY, 43 | 'XccwZ': Rotations.XccwZ, 44 | 'YccwX': Rotations.YccwX, 45 | 'YccwZ': Rotations.YccwZ, 46 | 'ZccwX': Rotations.ZccwX, 47 | 'ZccwY': Rotations.ZccwY 48 | } 49 | 50 | 51 | # Rotation matrices for each rotation as NumPy matrices. 52 | rotation_matrices = { 53 | Rotations.XcwY: np.asarray([[0, 1, 0], [-1, 0, 0], [0, 0, 1]]), 54 | Rotations.XccwY: np.asarray([[0, -1, 0], [1, 0, 0], [0, 0, 1]]), 55 | Rotations.XcwZ: np.asarray([[0, 0, 1], [0, 1, 0], [-1, 0, 0]]), 56 | Rotations.XccwZ: np.asarray([[0, 0, -1], [0, 1, 0], [1, 0, 0]]), 57 | Rotations.YcwX: np.asarray([[0, -1, 0], [1, 0, 0], [0, 0, 1]]), 58 | Rotations.YccwX: np.asarray([[0, 1, 0], [-1, 0, 0], [0, 0, 1]]), 59 | Rotations.YcwZ: np.asarray([[1, 0, 0], [0, 0, 1], [0, -1, 0]]), 60 | Rotations.YccwZ: np.asarray([[1, 0, 0], [0, 0, -1], [0, 1, 0]]), 61 | Rotations.ZcwX: np.asarray([[0, 0, 1], [0, 1, 0], [-1, 0, 0]]), 62 | Rotations.ZccwX: np.asarray([[0, 0, -1], [0, 1, 0], [1, 0, 0]]), 63 | Rotations.ZcwY: np.asarray([[1, 0, 0], [0, 0, 1], [0, -1, 0]]), 64 | Rotations.ZccwY: np.asarray([[1, 0, 0], [0, 0, -1], [0, 1, 0]]) 65 | } 66 | -------------------------------------------------------------------------------- /pcgsepy/lsystem/constraints.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum, auto 2 | from typing import Any, Callable, Dict 3 | 4 | from pcgsepy.lsystem.constraints_funcs import * 5 | from pcgsepy.lsystem.solution import CandidateSolution 6 | 7 | 8 | constraint_funcs = { 9 | 'components_constraint': components_constraint, 10 | 'intersection_constraint': intersection_constraint, 11 | 'symmetry_constraint': symmetry_constraint, 12 | 'axis_constraint': axis_constraint, 13 | } 14 | 15 | 16 | class ConstraintLevel(IntEnum): 17 | SOFT_CONSTRAINT = auto() 18 | HARD_CONSTRAINT = auto() 19 | 20 | 21 | class ConstraintTime(IntEnum): 22 | DURING = auto() 23 | END = auto() 24 | 25 | 26 | class ConstraintHandler: 27 | 28 | def __init__(self, 29 | name: str, 30 | level: ConstraintLevel, 31 | when: ConstraintTime, 32 | f: Callable[[CandidateSolution, Dict[str, Any]], bool], 33 | extra_args: Dict[str, Any], 34 | needs_ll: bool = False): 35 | self.name = name 36 | self.level = level 37 | self.when = when 38 | self.needs_ll = needs_ll 39 | self.constraint = f 40 | self.extra_args = extra_args 41 | 42 | def __repr__(self) -> str: 43 | return str(self.__dict__) 44 | 45 | def __str__(self) -> str: 46 | return f'Constraint {self.name} ({self.level.name}) at {self.when.name}' 47 | 48 | def __eq__(self, other): 49 | return str(self) == str(other) 50 | 51 | def __hash__(self): 52 | return hash((self.name, self.level.value, self.when.value, 53 | str(self.extra_args))) 54 | 55 | def to_json(self) -> Dict[str, Any]: 56 | return { 57 | 'name': self.name, 58 | 'level': self.level.value, 59 | 'when': self.when.value, 60 | 'needs_ll': self.needs_ll, 61 | 'constraint': self.constraint.__name__, 62 | 'extra_args': self.extra_args 63 | } 64 | 65 | @staticmethod 66 | def from_json(my_args: Dict[str, Any]) -> 'ConstraintHandler': 67 | return ConstraintHandler(name=my_args['name'], 68 | level=ConstraintLevel(my_args['level']), 69 | when=ConstraintTime(my_args['when']), 70 | f=constraint_funcs[my_args['constraint']], 71 | extra_args=my_args['extra_args'], 72 | needs_ll=my_args['needs_ll']) 73 | -------------------------------------------------------------------------------- /pcgsepy/lsystem/constraints_funcs.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | import numpy as np 4 | from pcgsepy.config import MAME_MEAN, MAME_STD, MAMI_MEAN, MAMI_STD 5 | from pcgsepy.lsystem.solution import CandidateSolution 6 | 7 | 8 | def components_constraint(cs: CandidateSolution, 9 | extra_args: Dict[str, Any]) -> bool: 10 | req_tiles = extra_args['req_tiles'] 11 | components_ok = True 12 | for c in req_tiles: 13 | components_ok &= c in cs.string 14 | return components_ok 15 | 16 | 17 | def intersection_constraint(cs: CandidateSolution, 18 | extra_args: Dict[str, Any]) -> bool: 19 | return cs.content.has_intersections 20 | 21 | 22 | def symmetry_constraint(cs: CandidateSolution, 23 | extra_args: Dict[str, Any]) -> bool: 24 | structure = cs.content.as_array 25 | is_symmetric = False 26 | for dim in range(3): 27 | is_symmetric |= np.array_equal(structure, np.flip(structure, axis=dim)) 28 | return is_symmetric 29 | 30 | 31 | def axis_constraint(cs: CandidateSolution, 32 | extra_args: Dict[str, Any]) -> bool: 33 | volume = cs.content.as_array.shape 34 | largest_axis, medium_axis, smallest_axis = reversed(sorted(list(volume))) 35 | mame = largest_axis / medium_axis 36 | mami = largest_axis / smallest_axis 37 | sat = True 38 | sat &= MAME_MEAN - MAME_STD <= mame <= MAME_MEAN + MAME_STD 39 | sat &= MAMI_MEAN - MAMI_STD <= mami <= MAMI_MEAN + MAMI_STD 40 | return sat 41 | -------------------------------------------------------------------------------- /pcgsepy/lsystem/rules.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List 2 | 3 | import numpy as np 4 | 5 | 6 | class StochasticRules: 7 | def __init__(self): 8 | """Create a ruleset""" 9 | self._rules = {} 10 | self.lhs_alphabet = set() 11 | 12 | def add_rule(self, 13 | lhs: str, 14 | rhs: str, 15 | p: float) -> None: 16 | """Add a rule. 17 | 18 | Args: 19 | lhs (str): The LHS of the rule. 20 | rhs (str): The RHS of the rule. 21 | p (float): The probability of applying RHS given LHS. 22 | """ 23 | if lhs in self._rules.keys(): 24 | self._rules[lhs][0].append(rhs) 25 | self._rules[lhs][1].append(p) 26 | else: 27 | self._rules[lhs] = ([rhs], [p]) 28 | lhs = lhs.replace('(x)', '').replace(']', '') 29 | self.lhs_alphabet.add(lhs) 30 | 31 | def rem_rule(self, 32 | lhs: str) -> None: 33 | """Remove a rule. 34 | 35 | Args: 36 | lhs (str): The LHS to remove. 37 | """ 38 | self._rules.pop(lhs) 39 | lhs = lhs.replace('(x)', '').replace(']', '') 40 | self.lhs_alphabet.pop(lhs) 41 | 42 | def get_lhs(self) -> List[str]: 43 | """Get all the LHS of the rule set. 44 | 45 | Returns: 46 | List[str]: The list of LHSs. 47 | """ 48 | return list(self._rules.keys()) 49 | 50 | def get_rhs(self, 51 | lhs: str) -> str: 52 | """Get the RHS of the given LHS according to the selection probability. 53 | 54 | Args: 55 | lhs (str): The LHS. 56 | 57 | Returns: 58 | str: The RHS. 59 | """ 60 | rhs, p = self._rules[lhs] 61 | return np.random.choice(rhs, p=p) 62 | 63 | def validate(self): 64 | """Ensure all probabilities for each LHS sum up to 1.""" 65 | for lhs in self._rules.keys(): 66 | p = sum(self._rules[lhs][1]) 67 | assert np.isclose(p, 1., atol=0.01), f'Probability must sum to 1: found {p} for `{lhs}`.' 68 | 69 | def __str__(self) -> str: 70 | return '\n'.join(['\n'.join([f'{k} {p} {o}' for (o, p) in zip(os, ps)]) for (k, (os, ps)) in self._rules.items()]) 71 | 72 | def to_json(self) -> Dict[str, Any]: 73 | return { 74 | 'rules': self._rules, 75 | 'lhs_alphabet': list(self.lhs_alphabet) 76 | } 77 | 78 | @staticmethod 79 | def from_json(my_args: Dict[str, Any]) -> 'StochasticRules': 80 | sr = StochasticRules() 81 | sr._rules = my_args['rules'] 82 | sr.lhs_alphabet = set(my_args['lhs_alphabet']) 83 | return sr 84 | 85 | 86 | class RuleMaker: 87 | def __init__(self, 88 | ruleset: str): 89 | """Create a ruleset maker. 90 | 91 | Args: 92 | ruleset (str): The set of rules. 93 | """ 94 | with open(ruleset, 'r') as f: 95 | self.ruleset = f.readlines() 96 | 97 | def get_rules(self) -> StochasticRules: 98 | """Create a ruleset. 99 | 100 | Returns: 101 | StochasticRules: The ruleset. 102 | """ 103 | rules = StochasticRules() 104 | for rule in self.ruleset: 105 | if rule.startswith('#'): # comment in configuration file 106 | pass 107 | else: 108 | lhs, p, rhs = rule.strip().split(' ') 109 | rules.add_rule(lhs=lhs, 110 | rhs=rhs, 111 | p=float(p)) 112 | rules.validate() 113 | return rules 114 | -------------------------------------------------------------------------------- /pcgsepy/lsystem/solution.py: -------------------------------------------------------------------------------- 1 | from functools import cached_property 2 | from typing import Any, Dict, List, Optional, Tuple 3 | 4 | import numpy as np 5 | 6 | from pcgsepy.common.vecs import Vec 7 | 8 | from ..structure import Structure 9 | 10 | 11 | class CandidateSolution: 12 | __slots__ = ['string', '_content', 'age', 'b_descs', 'c_fitness', 'fitness', 'hls_mod', 13 | 'is_feasible', 'll_string', 'n_feas_offspring', 'n_offspring', 'ncv', 14 | 'parents', 'representation', 'base_color', 'n_blocks', 'content_size'] 15 | 16 | def __init__(self, 17 | string: str, 18 | content: Optional[Structure] = None): 19 | self.string: str = string 20 | self._content: Structure = content 21 | 22 | self.age: int = 0 23 | self.b_descs: Tuple[float, float] = (0., 0.) 24 | self.c_fitness: float = 0. 25 | self.fitness: List[float] = [] 26 | self.hls_mod: Dict[str, Any] = {} # keys: 'string', 'mutable' 27 | self.is_feasible: bool = True 28 | self.ll_string: str = '' 29 | self.n_feas_offspring: int = 0 30 | self.n_offspring: int = 0 31 | self.ncv: int = 0 # number of constraints violated 32 | self.parents: List[CandidateSolution] = [] 33 | self.representation: List[float] = [] 34 | self.base_color = Vec.v3f(x=0.45, y=0.45, z=0.45) # default block color is #737373 35 | self.n_blocks = 0 36 | self.content_size = (0, 0, 0) 37 | 38 | def __str__(self) -> str: 39 | return f'{self.string}; fitness: {self.c_fitness}; is_feasible: {self.is_feasible}' 40 | 41 | def __repr__(self) -> str: 42 | return str(self) 43 | 44 | def __eq__(self, 45 | other: 'CandidateSolution') -> bool: 46 | if isinstance(other, CandidateSolution): 47 | return self.string == other.string 48 | return False 49 | 50 | def __hash__(self): 51 | return hash(self.string) 52 | 53 | def set_content(self, 54 | content: Structure): 55 | """Set the content of the solution. 56 | 57 | Args: 58 | content (Structure): The content. 59 | 60 | Raises: 61 | Exception: Raised if the solution already has a content set. 62 | """ 63 | if self._content: 64 | raise Exception('Structure already exists for this CandidateSolution.') 65 | else: 66 | self._content = content 67 | self.n_blocks = len(content._blocks) 68 | 69 | @property 70 | def content(self) -> Structure: 71 | if self._content: 72 | return self._content 73 | else: 74 | raise NotImplementedError('Structure has not been set yet.') 75 | 76 | @property 77 | def size(self) -> Tuple[int, int, int]: 78 | return self._content._max_dims 79 | 80 | @property 81 | def unique_blocks(self) -> Dict[str, int]: 82 | unique_blocks_dict = { 83 | 'Gyroscopes': ['MyObjectBuilder_Gyro_LargeBlockGyro'], 84 | 'Reactors': ['MyObjectBuilder_Reactor_LargeBlockSmallGenerator'], 85 | 'Containers': ['MyObjectBuilder_CargoContainer_LargeBlockSmallContainer'], 86 | 'Cockpits': ['MyObjectBuilder_Cockpit_OpenCockpitLarge'], 87 | 'Thrusters': ['MyObjectBuilder_Thrust_LargeBlockSmallThrust'], 88 | 'Lights': ['MyObjectBuilder_InteriorLight_SmallLight', 'MyObjectBuilder_InteriorLight_LargeBlockLight_1corner'] 89 | } 90 | counts = {} 91 | for k, vs in unique_blocks_dict.items(): 92 | counts[k] = 0 93 | for v in vs: 94 | counts[k] = counts[k] + self._content.unique_blocks_count(block_type=v) 95 | return counts 96 | 97 | def to_json(self) -> Dict[str, Any]: 98 | return { 99 | 'string': self.string, 100 | 'age': self.age, 101 | 'b_descs': self.b_descs, 102 | 'c_fitness': self.c_fitness, 103 | 'fitness': self.fitness, 104 | 'hls_mod': self.hls_mod, 105 | 'is_feasible': self.is_feasible, 106 | 'll_string': self.ll_string, 107 | 'n_feas_offspring': self.n_feas_offspring, 108 | 'n_offspring': self.n_offspring, 109 | 'ncv': self.ncv, 110 | 'parents': [p.to_json() for p in self.parents], 111 | 'representation': self.representation 112 | } 113 | 114 | @staticmethod 115 | def from_json(my_args: Dict[str, Any]) -> 'CandidateSolution': 116 | cs = CandidateSolution(string=my_args['string'], 117 | content=None) 118 | cs.age = my_args['age'] 119 | cs.b_descs = my_args['b_descs'] 120 | cs.c_fitness = my_args['c_fitness'] 121 | cs.fitness = my_args['fitness'] 122 | cs.hls_mod = my_args['hls_mod'] 123 | cs.is_feasible = my_args['is_feasible'] 124 | cs.ll_string = my_args['ll_string'] 125 | cs.n_feas_offspring = my_args['n_feas_offspring'] 126 | cs.n_offspring = my_args['n_offspring'] 127 | cs.ncv = my_args['ncv'] 128 | cs.parents = [CandidateSolution.from_json(args=p) for p in my_args['parents']] 129 | cs.representation = my_args['representation'] 130 | return cs 131 | 132 | 133 | def string_merging(ls: List[str]) -> str: 134 | """Merge a list of strings. 135 | 136 | Args: 137 | ls (List[str]): The list of strings. 138 | 139 | Returns: 140 | str: The merged string. 141 | """ 142 | # any additional control on alignment etc. should be done here. 143 | return ''.join(s for s in ls) 144 | 145 | 146 | def merge_solutions(lcs: List[CandidateSolution], 147 | modules_names: List[str], 148 | modules_active: List[bool]) -> CandidateSolution: 149 | """ 150 | Merge solutions in a single solution, keeping track of modules' solutions. 151 | 152 | Args: 153 | lcs (List[CandidateSolution]): The list of solutions to merge, ordered. 154 | modules_names (List[str]): The name of the L-system modules. 155 | modules_active: (List[bool]): The default mutability of the L-system modules. 156 | 157 | Returns: 158 | CandidateSolution: The merged solution 159 | """ 160 | assert len(lcs) == len(modules_names), f'Each solution should be produced by a module! Passed {len(lcs)} solutions and {len(modules_names)} modules.' 161 | m_cs = CandidateSolution(string=string_merging(ls=[cs.string for cs in lcs])) 162 | for i, (cs, default_m) in enumerate(zip(lcs, modules_active)): 163 | m_cs.hls_mod[modules_names[i]] = {'string': cs.string, 164 | 'mutable': default_m} 165 | return m_cs 166 | -------------------------------------------------------------------------------- /pcgsepy/lsystem/solver.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any, Dict, List, Optional, Union 3 | 4 | import numpy as np 5 | 6 | from pcgsepy.lsystem.rules import StochasticRules 7 | 8 | from .constraints import ConstraintHandler, ConstraintTime, ConstraintLevel 9 | from .parser import HLParser, HLtoMLTranslator, LParser, LLParser 10 | from .solution import CandidateSolution 11 | 12 | 13 | class LSolver: 14 | 15 | def __init__(self, 16 | parser: LParser, 17 | atoms_alphabet: Dict[str, Any], 18 | extra_args: Dict[str, Any]): 19 | self.parser = parser 20 | self.atoms_alphabet = atoms_alphabet 21 | self.constraints = [] 22 | self.inner_loops_during = 5 23 | self.inner_loops_end = 5 24 | self.translator = None 25 | if isinstance(self.parser, HLParser): 26 | self.translator = HLtoMLTranslator( 27 | alphabet=self.atoms_alphabet, 28 | tiles_dims=extra_args['tiles_dimensions'], 29 | tiles_block_offset=extra_args['tiles_block_offset']) 30 | self.ll_rules = extra_args['ll_rules'] 31 | 32 | def _forward_expansion(self, 33 | cs: CandidateSolution, 34 | n: int, 35 | dc_check: bool = False) -> Optional[CandidateSolution]: 36 | logging.getLogger('solver').debug(f'[{__name__}._forward_expansion] Expanding {cs.string=}.') 37 | for i in range(n): 38 | cs.string = self.parser.expand(string=cs.string) 39 | if dc_check and len([c for c in self.constraints if c.when == ConstraintTime.DURING]) > 0: 40 | if not self._check_constraints(cs=cs, 41 | when=ConstraintTime.DURING)[ConstraintLevel.HARD_CONSTRAINT][0]: 42 | cs = None # do not continue expansion if it breaks hard constraints during expansion 43 | return cs 44 | 45 | def _check_constraints(self, 46 | cs: CandidateSolution, 47 | when: ConstraintTime, 48 | keep_track: bool = False) -> Dict[ConstraintLevel, List[Union[bool, int]]]: 49 | logging.getLogger('solver').debug(f'[{__name__}._check_constraints] Checking constraints on {cs.string=}.') 50 | sat = { 51 | ConstraintLevel.SOFT_CONSTRAINT: [True, 0], 52 | ConstraintLevel.HARD_CONSTRAINT: [True, 0], 53 | } 54 | for lev in sat.keys(): 55 | for c in self.constraints: 56 | if c.when == when and c.level == lev: 57 | s = c.constraint(cs=cs, 58 | extra_args=c.extra_args) 59 | logging.getLogger('solver').debug(f'[{__name__}._forward_expansion] \t{c}:\t{s}.') 60 | sat[lev][0] &= s 61 | if keep_track: 62 | sat[lev][1] += ( 63 | 1 if lev == ConstraintLevel.HARD_CONSTRAINT else 64 | 0.5) if not s else 0 65 | return sat 66 | 67 | def solve(self, 68 | string: str, 69 | iterations: int, 70 | strings_per_iteration: int = 1, 71 | check_sat: bool = True) -> List[CandidateSolution]: 72 | all_solutions = [CandidateSolution(string=string)] 73 | # forward expansion + DURING constraints check 74 | for i in range(iterations): 75 | logging.getLogger('solver').debug(f'[{__name__}.solve] Expansion {i+1}/{iterations}; current number of strings: {len(all_solutions)}') 76 | new_all_solutions = [] 77 | for cs in all_solutions: 78 | for _ in range(strings_per_iteration): 79 | new_cs = CandidateSolution(string=cs.string[:]) 80 | new_cs = self._forward_expansion(cs=new_cs, 81 | n=1, 82 | dc_check=check_sat and i > 0) 83 | if new_cs is not None: 84 | new_all_solutions.append(new_cs) 85 | all_solutions = new_all_solutions 86 | all_solutions = list(set(all_solutions)) # remove duplicates 87 | 88 | # END constraints check + possible backtracking 89 | if check_sat and len([c for c in self.constraints if c.when == ConstraintTime.END]) > 0: 90 | to_keep = np.zeros(shape=len(all_solutions), dtype=np.bool8) 91 | for i, cs in enumerate(all_solutions): 92 | logging.getLogger('solver').debug(f'[{__name__}.solve] Finalizing string {cs.string}') 93 | to_keep[i] = self._check_constraints(cs=cs, 94 | when=ConstraintTime.END)[ConstraintLevel.HARD_CONSTRAINT][0] 95 | # remaining strings are SAT 96 | all_solutions = [cs for i, cs in enumerate(all_solutions) if to_keep[i]] 97 | 98 | return all_solutions 99 | 100 | def set_constraints(self, 101 | cs: List[ConstraintHandler]) -> None: 102 | self.constraints = cs 103 | 104 | def to_json(self) -> Dict[str, Any]: 105 | j = { 106 | 'has_hl_parser': isinstance(self.parser, HLParser), 107 | 'rules': self.parser.rules.to_json(), 108 | 'atoms_alphabet': self.atoms_alphabet 109 | } 110 | if isinstance(self.parser, HLParser): 111 | j['extra_args'] = { 112 | 'tiles_dimensions': self.translator.td, 113 | 'tiles_block_offset': self.translator.tbo, 114 | 'll_rules': self.ll_rules 115 | } 116 | return j 117 | 118 | @staticmethod 119 | def from_json(my_args: Dict[str, Any]) -> 'LSolver': 120 | parser = HLParser if my_args['has_hl_parser'] else LLParser 121 | return LSolver(parser=parser(rules=StochasticRules.from_json(my_args['rules'])), 122 | atoms_alphabet=my_args['atoms_alphabet'], 123 | extra_args=my_args.get('extra_args', {})) -------------------------------------------------------------------------------- /pcgsepy/lsystem/structure_maker.py: -------------------------------------------------------------------------------- 1 | from ..structure import Structure, Block 2 | from ..common.vecs import Vec, orientation_from_str, orientation_from_vec 3 | from .actions import rotation_matrices, AtomAction 4 | 5 | from abc import ABC, abstractmethod 6 | from typing import Any, Dict 7 | import re 8 | 9 | 10 | class StructureMaker(ABC): 11 | 12 | def __init__(self, atoms_alphabet, position: Vec): 13 | self.pattern = re.compile(r'(\[|\])|(Rot[XYZ]c{1,2}w[XYZ])|((\w+|\W+)(\(.{1,3}\)))') 14 | self.atoms_alphabet = atoms_alphabet 15 | self._calls = { 16 | AtomAction.PLACE: self._place, 17 | AtomAction.MOVE: self._move, 18 | AtomAction.ROTATE: self._rotate, 19 | AtomAction.PUSH: self._push, 20 | AtomAction.POP: self._pop 21 | } 22 | self.position = position 23 | self.rotations = [] 24 | self.position_history = [] 25 | 26 | def _apply_rotation(self, arr: Vec) -> Vec: 27 | arr = arr.as_array() 28 | for rot in reversed(self.rotations): 29 | arr = rot.dot(arr) 30 | return Vec.from_np(arr) 31 | 32 | def _rotate(self, action_args: Any) -> None: 33 | self.rotations.append(rotation_matrices[action_args['action_args']]) 34 | 35 | def _move(self, action_args: Any) -> None: 36 | dpos = action_args['action_args'].value 37 | n = int(action_args['parameters'][0]) 38 | if self.rotations: 39 | dpos = self._apply_rotation(arr=dpos) 40 | for _ in range(n): 41 | self.position = self.position.sum(dpos) 42 | 43 | def _push(self, action_args: Any) -> None: 44 | self.position_history.append(self.position) 45 | 46 | def _pop(self, action_args: Any) -> None: 47 | self.position = self.position_history.pop(-1) 48 | if self.rotations: 49 | self.rotations.pop(-1) 50 | 51 | @abstractmethod 52 | def _place(self, action_args: Any) -> None: 53 | pass 54 | 55 | @abstractmethod 56 | def fill_structure(self, 57 | structure: Structure, 58 | string: str, 59 | additional_args: Dict[str, Any] = {}) -> None: 60 | pass 61 | 62 | 63 | class LLStructureMaker(StructureMaker): 64 | 65 | def _place(self, action_args: Any) -> None: 66 | orientation_forward, orientation_up = action_args['parameters'][0], action_args['parameters'][1] 67 | orientation_forward = orientation_from_str[orientation_forward] 68 | orientation_up = orientation_from_str[orientation_up] 69 | if self.rotations: 70 | orientation_forward = orientation_from_vec(self._apply_rotation(arr=orientation_forward.value)) 71 | orientation_up = orientation_from_vec(self._apply_rotation(arr=orientation_up.value)) 72 | block = Block(block_type=action_args['action_args'][0], 73 | orientation_forward=orientation_forward, 74 | orientation_up=orientation_up) 75 | self.structure.add_block(block=block, 76 | grid_position=self.position.as_tuple()) 77 | 78 | def fill_structure(self, 79 | structure: Structure, 80 | string: str, 81 | additional_args: Dict[str, Any] = {}) -> Structure: 82 | self.additional_args = additional_args 83 | self.structure = structure 84 | for g1, g2, _, g4, g5 in [match.groups() for match in self.pattern.finditer(string=string)]: 85 | if g1 is not None: 86 | atom, params = (g1, '') 87 | elif g2 is not None: 88 | atom, params = (g2, '') 89 | else: 90 | atom, params = (g4, g5) 91 | params = params.replace('(','').replace(')','').split(',') 92 | action, args = self.atoms_alphabet[atom]['action'], self.atoms_alphabet[atom]['args'] 93 | self._calls[action]({ 94 | 'action_args': args, 95 | 'parameters': params, 96 | 'string': atom 97 | }) 98 | self.structure.sanify() 99 | 100 | return self.structure 101 | -------------------------------------------------------------------------------- /pcgsepy/mapelites/bandit.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Optional 2 | 3 | import numpy as np 4 | 5 | 6 | class Bandit: 7 | def __init__(self, 8 | action: str): 9 | """Bandit class. 10 | 11 | Args: 12 | action (str): The action that is applied by the bandit. 13 | """ 14 | self.action = action 15 | self.tot_rewards = 0. 16 | self.tot_actions = 0 17 | 18 | def __str__(self) -> str: 19 | return f'{self.action}_{str(self.avg_rewards)}' 20 | 21 | @property 22 | def avg_rewards(self) -> float: 23 | """Get the average reward of the bandit. 24 | 25 | Returns: 26 | float: The average reward. 27 | """ 28 | return 0 if self.tot_actions == 0 else self.tot_rewards / self.tot_actions 29 | 30 | def to_json(self) -> Dict[str, Any]: 31 | return { 32 | 'action': self.action, 33 | 'tot_rewards': self.tot_rewards, 34 | 'tot_actions': self.tot_actions 35 | } 36 | 37 | @staticmethod 38 | def from_json(my_args: Dict[str, Any]) -> 'Bandit': 39 | b = Bandit(action=my_args['action']) 40 | b.tot_rewards = my_args['tot_rewards'] 41 | b.tot_actions = my_args['tot_actions'] 42 | return b 43 | 44 | 45 | class EpsilonGreedyAgent: 46 | def __init__(self, 47 | bandits: List[Bandit], 48 | epsilon: Optional[float]) -> None: 49 | """Simulate an epsilon-greedy multiarmed bandit agent. 50 | 51 | Args: 52 | bandits (List[Bandit]): The bandits. 53 | epsilon (Optional[float]): A fixed epsilon value. If not set, a decayed epsilon will be used. 54 | """ 55 | assert len(bandits) > 0, 'Can\'t initialize an agent without bandits!' 56 | self.bandits = bandits 57 | self.epsilon = epsilon 58 | self.tot_actions = 0 59 | 60 | def __str__(self) -> str: 61 | return f'{len(self.bandits)}-armed bandit ε-greedy agent' 62 | 63 | def _get_random_bandit(self) -> Bandit: 64 | """Get a random bandit from the available bandits. 65 | 66 | Returns: 67 | Bandit: A randomly picked bandit. 68 | """ 69 | return np.random.choice(self.bandits) 70 | 71 | def _get_best_bandit(self) -> Bandit: 72 | """Get the best (highest reward) bandit. 73 | 74 | Returns: 75 | Bandit: The bandit with the highest reward. 76 | """ 77 | return self.bandits[np.argmax([x.avg_rewards for x in self.bandits])] 78 | 79 | def choose_bandit(self) -> Bandit: 80 | """Pick a bandit. Applies epsilon-greedy policy when picking. 81 | 82 | Returns: 83 | Bandit: The selected bandit. 84 | """ 85 | p = np.random.uniform(low=0, high=1, size=1) < (self.epsilon or 1 / (1 + self.tot_actions)) 86 | return self._get_random_bandit() if p else self._get_best_bandit() 87 | 88 | def reward_bandit(self, 89 | bandit: Bandit, 90 | reward: float) -> None: 91 | self.tot_actions += 1 92 | bandit.tot_actions += 1 93 | bandit.tot_rewards += reward 94 | 95 | def to_json(self) -> Dict[str, Any]: 96 | return { 97 | 'bandits': [b.to_json() for b in self.bandits], 98 | 'epsilon': self.epsilon, 99 | 'tot_actions': self.tot_actions 100 | } 101 | 102 | @staticmethod 103 | def from_json(my_args: Dict[str, Any]) -> 'EpsilonGreedyAgent': 104 | ega = EpsilonGreedyAgent(bandits=[Bandit.from_json(b) for b in my_args['bandits']], 105 | epsilon=my_args.get('epsilon', None)) 106 | ega.tot_actions = my_args['tot_actions'] 107 | return ega 108 | -------------------------------------------------------------------------------- /pcgsepy/mapelites/behaviors.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Tuple 2 | import numpy as np 3 | 4 | from pcgsepy.lsystem.solution import CandidateSolution 5 | 6 | 7 | class BehaviorCharacterization: 8 | def __init__(self, 9 | name: str, 10 | func: callable, 11 | bounds: Tuple[float, float]): 12 | """Create a behavior characterization object. 13 | 14 | Args: 15 | name (str): The name. 16 | func (callable): The function to compute. 17 | bounds (Tuple[float, float]): The upper and lower bounds. 18 | """ 19 | self.name = name 20 | self.bounds = bounds 21 | self.f = func 22 | 23 | def __call__(self, 24 | cs: CandidateSolution) -> float: 25 | return self.f(cs) 26 | 27 | def to_json(self) -> Dict[str, Any]: 28 | return { 29 | 'name': self.name, 30 | 'bounds': list(self.bounds), 31 | 'f': self.f.__name__ 32 | } 33 | 34 | @staticmethod 35 | def from_json(my_args: Dict[str, Any]) -> 'BehaviorCharacterization': 36 | return BehaviorCharacterization(name=my_args['name'], 37 | func=behavior_funcs[my_args['f']], 38 | bounds=tuple(my_args['bounds'])) 39 | 40 | 41 | def mame(cs: CandidateSolution) -> float: 42 | """Major axis over Medium axis. 43 | 44 | Args: 45 | cs (CandidateSolution): The solution. 46 | 47 | Returns: 48 | float: The value of this behavior characterization. 49 | """ 50 | largest_axis, medium_axis, _ = reversed(sorted(list(cs.content.as_grid_array.shape))) 51 | return largest_axis / medium_axis 52 | 53 | 54 | def mami(cs: CandidateSolution) -> float: 55 | """Major axis over Minimum axis. 56 | 57 | Args: 58 | cs (CandidateSolution): The solution. 59 | 60 | Returns: 61 | float: The value of this behavior characterization. 62 | """ 63 | largest_axis, _, smallest_axis = reversed(sorted(list(cs.content.as_grid_array.shape))) 64 | return largest_axis / smallest_axis 65 | 66 | 67 | def avg_ma(cs: CandidateSolution) -> float: 68 | """The average axis proportions. 69 | 70 | Args: 71 | cs (CandidateSolution): The solution. 72 | 73 | Returns: 74 | float: The value of this behavior characterization. 75 | """ 76 | largest_axis, medium_axis, smallest_axis = reversed(sorted(list(cs.content.as_grid_array.shape))) 77 | return ((largest_axis / medium_axis) + (largest_axis / smallest_axis)) / 2 78 | 79 | 80 | def symmetry(cs: CandidateSolution): 81 | """Symmetry of the solution, expressed in `[0,1]`. 82 | 83 | Args: 84 | cs (CandidateSolution): The solution. 85 | 86 | Returns: 87 | _type_: The value of this behavior characterization. 88 | """ 89 | structure = cs.content 90 | pivot_blocktype = 'MyObjectBuilder_Cockpit_OpenCockpitLarge' 91 | midpoint = [x for x in structure._blocks.values() if x.block_type == pivot_blocktype][0].position.scale(1 / structure.grid_size).to_veci() 92 | arr = structure.as_grid_array 93 | 94 | # along x 95 | x_shape = max(midpoint.x, arr.shape[0] - midpoint.x) 96 | upper = np.zeros((x_shape, arr.shape[1], arr.shape[2])) 97 | lower = np.zeros((x_shape, arr.shape[1], arr.shape[2])) 98 | upper[np.nonzero(np.flip(arr[midpoint.x:, :, :], 1))] = np.flip(arr[midpoint.x:, :, :], 1)[np.nonzero(np.flip(arr[midpoint.x:, :, :], 1))] 99 | lower[np.nonzero(arr[:midpoint.x - 1, :, :])] = arr[np.nonzero(arr[:midpoint.x - 1, :, :])] 100 | err_x = abs(np.sum(upper - lower)) 101 | 102 | # along z 103 | z_shape = max(midpoint.z, arr.shape[2] - midpoint.z) 104 | upper = np.zeros((arr.shape[0], arr.shape[1], z_shape)) 105 | lower = np.zeros((arr.shape[0], arr.shape[1], z_shape)) 106 | tmp = np.flip(arr[:, :, midpoint.z:], 2) 107 | upper[np.nonzero(np.flip(arr[:, :, midpoint.z:], 2))] = np.flip(arr[:, :, midpoint.z:], 2)[np.nonzero(np.flip(arr[:, :, midpoint.z:], 2))] 108 | lower[np.nonzero(arr[:, :, :midpoint.z - 1])] = arr[np.nonzero(arr[:, :, :midpoint.z - 1])] 109 | err_z = abs(np.sum(upper - lower)) 110 | 111 | return 1 - (min(err_x, err_z) / np.sum(arr)) 112 | 113 | 114 | behavior_funcs = { 115 | 'mame': mame, 116 | 'mami': mami, 117 | 'avg_ma': avg_ma, 118 | 'symmetry': symmetry 119 | } 120 | -------------------------------------------------------------------------------- /pcgsepy/mapelites/buffer.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, Dict, Tuple 2 | 3 | import numpy as np 4 | import numpy.typing as npt 5 | 6 | 7 | class EmptyBufferException(Exception): 8 | pass 9 | 10 | 11 | def mean_merge(x1: Any, 12 | x2: Any) -> Any: 13 | """Return the average of two values. 14 | 15 | Args: 16 | x1 (Any): The first value. 17 | x2 (Any): The second value. 18 | 19 | Returns: 20 | Any: The average of the two values. 21 | """ 22 | return (x1 + x2) / 2 23 | 24 | 25 | def max_merge(x1: Any, 26 | x2: Any) -> Any: 27 | """Return the maximum of two values. 28 | 29 | Args: 30 | x1 (Any): The first value. 31 | x2 (Any): The second value. 32 | 33 | Returns: 34 | Any: The maximum of the two values. 35 | """ 36 | return max(x1, x2) 37 | 38 | 39 | def min_merge(x1: Any, 40 | x2: Any) -> Any: 41 | """Return the minimum of two values. 42 | 43 | Args: 44 | x1 (Any): The first value. 45 | x2 (Any): The second value. 46 | 47 | Returns: 48 | Any: The minimum of the two values. 49 | """ 50 | return min(x1, x2) 51 | 52 | 53 | merge_methods = { 54 | 'mean_merge': mean_merge, 55 | 'max_merge': max_merge, 56 | 'min_merge': min_merge 57 | } 58 | 59 | 60 | class Buffer: 61 | def __init__(self, 62 | merge_method: Callable[[Any, Any], Any] = mean_merge) -> None: 63 | """Create an empty buffer. 64 | 65 | Args: 66 | merge_method (Callable[[Any, Any], Any], optional): The merging method. Defaults to `mean_merge`. 67 | """ 68 | self._xs, self._ys = [], [] 69 | self._merge = merge_method 70 | 71 | def _contains(self, 72 | x: Any) -> int: 73 | """Check whether the element is present in the buffer. If it is, the index of the element is returned, otherwise `-1` is returned. 74 | 75 | Args: 76 | x (Any): The element to check. 77 | 78 | Returns: 79 | int: The index of the element if it is present, otherwise `-1`. 80 | """ 81 | for i, _x in enumerate(self._xs): 82 | if np.array_equal(x, _x): 83 | return i 84 | return -1 85 | 86 | def insert(self, 87 | x: Any, 88 | y: npt.NDArray[np.float32]) -> None: 89 | """Add a datapoint to the buffer. 90 | 91 | Args: 92 | x (Any): The input data. 93 | y (Any): The label data. 94 | """ 95 | i = self._contains(x) 96 | if i > -1: 97 | y0 = self._ys[i] 98 | self._ys[i] = self._merge(y0, y) 99 | else: 100 | self._xs.append(np.asarray(x)) 101 | self._ys.append(y) 102 | 103 | def get(self) -> Tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]: 104 | """Get the array representation of the buffer. 105 | 106 | Raises: 107 | EmptyBufferException: Raised if the buffer is empty. 108 | 109 | Returns: 110 | Tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]: The input data and label data as NumPy arrays. 111 | """ 112 | if len(self._xs) > 0: 113 | xs = np.empty((len(self._xs), len(self._xs[0]))) 114 | for i, X in enumerate(self._xs): 115 | xs[i, :] = X 116 | ys = np.asarray(self._ys) 117 | return xs, ys 118 | else: 119 | raise EmptyBufferException('Buffer is empty!') 120 | 121 | def clear(self) -> None: 122 | """Clear the buffer.""" 123 | self._xs, self._ys = [], [] 124 | 125 | def to_json(self) -> Dict[str, Any]: 126 | return { 127 | 'xs': [x.tolist() for x in self._xs], 128 | 'ys': [y.tolist() for y in self._ys], 129 | 'merge_method': self._merge.__name__ 130 | } 131 | 132 | @staticmethod 133 | def from_json(my_args: Dict[str, Any]) -> 'Buffer': 134 | b = Buffer(merge_method=merge_methods[my_args['merge_method']]) 135 | b._xs = [np.asarray(x) for x in my_args['xs']] 136 | b._ys = [np.asarray(y) for y in my_args['ys']] 137 | return b 138 | -------------------------------------------------------------------------------- /pcgsepy/setup_utils.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | import os 4 | import sys 5 | from typing import Any, Dict, List 6 | 7 | import matplotlib 8 | from matplotlib import pyplot as plt 9 | 10 | from pcgsepy.common.vecs import Vec, orientation_from_str 11 | from pcgsepy.config import COMMON_ATOMS, HL_ATOMS 12 | from pcgsepy.lsystem.actions import AtomAction, rotations_from_str 13 | from pcgsepy.lsystem.constraints import (ConstraintHandler, ConstraintLevel, 14 | ConstraintTime) 15 | from pcgsepy.lsystem.constraints_funcs import (components_constraint, 16 | intersection_constraint, 17 | symmetry_constraint) 18 | from pcgsepy.lsystem.lsystem import LSystem 19 | from pcgsepy.lsystem.parser import HLParser, LLParser 20 | from pcgsepy.lsystem.rules import RuleMaker 21 | from pcgsepy.lsystem.solver import LSolver 22 | 23 | 24 | def setup_matplotlib(type3_fix: bool = True, 25 | larger_fonts: bool = True): 26 | """Setup Matplotlib. 27 | 28 | Args: 29 | type3_fix (bool, optional): Ensures no Type3 font is used. Defaults to `True`. 30 | larger_fonts (bool, optional): Enlarge font sizes. Defaults to `True`. 31 | """ 32 | if type3_fix: 33 | matplotlib.rcParams['pdf.fonttype'] = 42 34 | matplotlib.rcParams['ps.fonttype'] = 42 35 | if larger_fonts: 36 | SMALL_SIZE = 20 37 | MEDIUM_SIZE = 22 38 | BIGGER_SIZE = 26 39 | 40 | plt.rc('font', size=SMALL_SIZE) # controls default text sizes 41 | plt.rc('axes', titlesize=BIGGER_SIZE) # fontsize of the axes title 42 | plt.rc('axes', labelsize=MEDIUM_SIZE) # fontsize of the x and y labels 43 | plt.rc('xtick', labelsize=SMALL_SIZE) # fontsize of the tick labels 44 | plt.rc('ytick', labelsize=SMALL_SIZE) # fontsize of the tick labels 45 | plt.rc('legend', fontsize=MEDIUM_SIZE) # legend fontsize 46 | plt.rc('figure', titlesize=BIGGER_SIZE) # fontsize of the figure title 47 | 48 | 49 | def get_default_lsystem(used_ll_blocks: List[str]) -> LSystem: 50 | """Get the default L-system. 51 | 52 | Args: 53 | used_ll_blocks (List[str]): List of game blocks used. 54 | 55 | Returns: 56 | LSystem: The default L-system. 57 | """ 58 | if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): 59 | os.chdir(sys._MEIPASS) 60 | curr_dir = os.path.dirname(sys.executable) 61 | else: 62 | curr_dir = sys.path[0] 63 | 64 | # load the common atoms 65 | with open(os.path.join(curr_dir, COMMON_ATOMS), "r") as f: 66 | common_alphabet: Dict[str, Any] = json.load(f) 67 | # add actions 68 | action_to_args = { 69 | AtomAction.MOVE: orientation_from_str, 70 | AtomAction.ROTATE: rotations_from_str, 71 | AtomAction.POP: {}, 72 | AtomAction.PUSH: {}, 73 | } 74 | common_alphabet.update({k: {'action': AtomAction(common_alphabet[k]["action"]), 75 | 'args': action_to_args[AtomAction(common_alphabet[k]["action"])].get(str(common_alphabet[k]["args"]), common_alphabet[k]["args"])} for k in common_alphabet}) 76 | # load high-level atoms 77 | with open(os.path.join(curr_dir, HL_ATOMS), "r") as f: 78 | hl_atoms = json.load(f) 79 | # get the tile properties 80 | tiles_dimensions = {tile: Vec.from_tuple(hl_atoms[tile]["dimensions"]) for tile in hl_atoms} 81 | tiles_block_offset = {tile: hl_atoms[tile]["offset"] for tile in hl_atoms} 82 | # create the high-level alphabet 83 | hl_alphabet = {k: v for k, v in common_alphabet.items()} 84 | hl_alphabet.update({k: {"action": AtomAction.PLACE, "args": []} 85 | for k in hl_atoms}) 86 | # create the low-level alphabet 87 | ll_alphabet = {k: v for k, v in common_alphabet.items()} 88 | ll_alphabet.update({k: {"action": AtomAction.PLACE, "args": [k]} 89 | for k in used_ll_blocks}) 90 | # create the rulesets 91 | hl_rules = RuleMaker(ruleset=os.path.join(curr_dir, 'hlrules_sm')).get_rules() 92 | ll_rules = RuleMaker(ruleset=os.path.join(curr_dir, 'llrules')).get_rules() 93 | # create the parsers 94 | hl_parser = HLParser(rules=hl_rules) 95 | ll_parser = LLParser(rules=ll_rules) 96 | # create the solver 97 | hl_solver = LSolver(parser=hl_parser, 98 | atoms_alphabet=hl_alphabet, 99 | extra_args={ 100 | 'tiles_dimensions': tiles_dimensions, 101 | 'tiles_block_offset': tiles_block_offset, 102 | 'll_rules': ll_rules 103 | }) 104 | ll_solver = LSolver(parser=ll_parser, 105 | atoms_alphabet=dict(hl_alphabet, **ll_alphabet), 106 | extra_args={}) 107 | # create the constraints 108 | rcc1 = ConstraintHandler( 109 | name="required_components", 110 | level=ConstraintLevel.HARD_CONSTRAINT, 111 | when=ConstraintTime.END, 112 | f=components_constraint, 113 | extra_args={ 114 | 'alphabet': hl_alphabet 115 | } 116 | ) 117 | rcc1.extra_args["req_tiles"] = ['cockpit'] 118 | rcc2 = ConstraintHandler( 119 | name="required_components", 120 | level=ConstraintLevel.HARD_CONSTRAINT, 121 | when=ConstraintTime.END, 122 | f=components_constraint, 123 | extra_args={ 124 | 'alphabet': hl_alphabet 125 | } 126 | ) 127 | rcc2.extra_args["req_tiles"] = [ 128 | 'corridorcargo', 'corridorgyros', 'corridorreactors'] 129 | rcc3 = ConstraintHandler( 130 | name="required_components", 131 | level=ConstraintLevel.HARD_CONSTRAINT, 132 | when=ConstraintTime.END, 133 | f=components_constraint, 134 | extra_args={ 135 | 'alphabet': hl_alphabet 136 | } 137 | ) 138 | rcc3.extra_args["req_tiles"] = ['thrusters'] 139 | nic = ConstraintHandler( 140 | name="no_intersections", 141 | level=ConstraintLevel.HARD_CONSTRAINT, 142 | when=ConstraintTime.DURING, 143 | f=intersection_constraint, 144 | extra_args={ 145 | 'alphabet': dict(hl_alphabet, **ll_alphabet) 146 | }, 147 | needs_ll=True 148 | ) 149 | nic.extra_args["tiles_dimensions"] = tiles_dimensions 150 | sc = ConstraintHandler( 151 | name="symmetry", 152 | level=ConstraintLevel.SOFT_CONSTRAINT, 153 | when=ConstraintTime.END, 154 | f=symmetry_constraint, 155 | extra_args={ 156 | 'alphabet': dict(hl_alphabet, **ll_alphabet) 157 | } 158 | ) 159 | # create the L-system 160 | lsystem = LSystem(hl_solver=hl_solver, 161 | ll_solver=ll_solver, 162 | names=['HeadModule', 'BodyModule', 'TailModule'] 163 | ) 164 | # add the high-level constraints 165 | lsystem.add_hl_constraints(cs=[ 166 | [nic, rcc1], 167 | [nic, rcc2], 168 | [nic, rcc3] 169 | ]) 170 | # add the low-level constraints 171 | lsystem.add_ll_constraints(cs=[ 172 | [sc], 173 | [sc], 174 | [sc] 175 | ]) 176 | 177 | return lsystem 178 | -------------------------------------------------------------------------------- /pcgsepy/stats/plots.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Optional 2 | import pandas as pd 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | from numpy.typing import NDArray 6 | 7 | 8 | def plot_rankings(samples: List[NDArray], 9 | labels: List[str], 10 | names: List[str], 11 | title: str, 12 | filename: Optional[str]): 13 | 14 | def __count_score(arr: NDArray, 15 | v: int) -> int: 16 | return np.sum([1 if x == v else 0 for x in arr]) 17 | 18 | data = {} 19 | for i, label in enumerate(labels): 20 | data[label] = [] 21 | for sample in samples: 22 | data[label].append(__count_score(arr=sample, 23 | v=i + 1)) 24 | df = pd.DataFrame(data=data, 25 | index=names) 26 | ax = df.plot.barh() 27 | plt.legend(bbox_to_anchor=(1,1), loc="upper left") 28 | plt.title(title) 29 | if filename: 30 | plt.savefig(filename) 31 | plt.show() 32 | 33 | 34 | def plot_scores(samples: List[NDArray], 35 | names: List[str], 36 | score_to_value: Dict[int, float], 37 | title: str, 38 | filename: Optional[str]): 39 | all_values = [] 40 | 41 | for sample in samples: 42 | all_values.append([]) 43 | for e in sample: 44 | all_values[-1].append(score_to_value[e]) 45 | 46 | plt.bar(names, height=[np.sum(x) for x in all_values]) 47 | plt.title(title) 48 | # plt.xticks(rotation = 45) 49 | if filename: 50 | plt.savefig(filename) 51 | plt.show() 52 | -------------------------------------------------------------------------------- /pcgsepy/stats/tests.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from typing import List, Tuple 3 | 4 | from numpy.typing import NDArray 5 | from scipy.stats import f_oneway, kruskal, shapiro 6 | from statsmodels.stats.multitest import multipletests 7 | 8 | THRESHOLD_PVALUE = 0.05 9 | 10 | 11 | def shapiro_wilk(samples: List[NDArray]) -> List[Tuple[float, float]]: 12 | stats = [] 13 | for sample in samples: 14 | shapiro_test = shapiro(sample) 15 | stats.append((shapiro_test.statistic, shapiro_test.pvalue)) 16 | return stats 17 | 18 | 19 | def anova(samples: List[NDArray]) -> List[Tuple[float, float]]: 20 | stats = [] 21 | stats.append(f_oneway(*samples)) 22 | if stats[0][1] < THRESHOLD_PVALUE: 23 | for comb in itertools.combinations(iterable=samples, 24 | r=2): 25 | stats.append(f_oneway(*comb)) 26 | return stats 27 | 28 | 29 | def kruskal_wallis(samples: List[NDArray]) -> List[Tuple[float, float]]: 30 | stats = [] 31 | kruskal_test = kruskal(*samples) 32 | stats.append((kruskal_test.statistic, kruskal_test.pvalue)) 33 | if stats[0][1] < THRESHOLD_PVALUE: 34 | for comb in itertools.combinations(iterable=samples, 35 | r=2): 36 | kruskal_test = kruskal(*comb) 37 | stats.append((kruskal_test.statistic, kruskal_test.pvalue)) 38 | return stats 39 | 40 | 41 | def holm_posthoc_correction(pvals: List[float]) -> Tuple[List[bool], List[float], float, float]: 42 | return multipletests(pvals=pvals, 43 | alpha=THRESHOLD_PVALUE, 44 | method='holm') 45 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bs4 2 | dash 3 | dash-bootstrap-components 4 | dash-core-components 5 | dash-html-components 6 | kaleido 7 | joblib 8 | matplotlib 9 | numpy 10 | pandas 11 | pyinstaller 12 | scipy 13 | simplejson 14 | sklearn 15 | statsmodels 16 | steamctl 17 | # decomment to use PyTorch 18 | # torch 19 | tqdm 20 | waitress -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | import configparser 4 | import os 5 | 6 | config = configparser.ConfigParser() 7 | curr_dir = os.getcwd() 8 | config.read(os.path.join(curr_dir, 'configs.ini')) 9 | 10 | USE_TORCH = config['LIBRARY'].getboolean('use_torch') 11 | 12 | base_dependencies = [ 13 | "bs4 >= 0.0.1", 14 | "dash >= 2.4.0", 15 | "dash-bootstrap-components >= 1.2.1", 16 | "dash-core-components >= 2.0.0", 17 | "dash-html-components >= 2.0.0", 18 | "kaleido == 0.0.1", 19 | "joblib == 1.2.0", 20 | "matplotlib >= 3.5.1", 21 | "numpy >= 1.22.3", 22 | "pandas >= 1.4.2", 23 | "plotly == 5.5.0", 24 | # "plotly-orca == 1.3.1", 25 | "pyinstaller >= 5.2", 26 | "psutil == 5.9.0", 27 | "scipy >= 1.8.0", 28 | "simplejson >= 3.17.6", 29 | "sklearn >= 0.0", 30 | "statsmodels >= 0.13.2", 31 | "steamctl >= 0.9.1", 32 | "tqdm >= 4.64.0", 33 | "typing_extensions >= 4.3.0", 34 | "waitress >= 2.1.2" 35 | ] 36 | 37 | if USE_TORCH: 38 | base_dependencies.append("torch >= 1.11.0") 39 | 40 | setup( 41 | name='PCGSEPy', 42 | version='0.0.1', 43 | author='Roberto Gallotta', 44 | author_email='roberto_gallotta@araya.org', 45 | packages=['pcgsepy'], 46 | scripts=[], 47 | url='https://github.com/arayabrain/space-engineers-ai-spaceship-generator', 48 | license='LICENSE.md', 49 | description='PCG Python package for Space Engineers', 50 | long_description='This package provides methods and classes to run a Procedural Content Generation task in the videogame Space Engineers.', 51 | install_requires=base_dependencies 52 | ) -------------------------------------------------------------------------------- /steam-workshop-downloader/.gitignore: -------------------------------------------------------------------------------- 1 | .ipynb_checkpoints/* 2 | content/* 3 | downloads/* 4 | private 5 | *.sbc -------------------------------------------------------------------------------- /steam-workshop-downloader/README.md: -------------------------------------------------------------------------------- 1 | # Steam Workshop Downloader 2 | 3 | ## steam-ws-downloader 4 | 5 | An utility to locally download Steam Workshop files. 6 | 7 | Uses the [steamctl](https://github.com/ValvePython/steamctl) to interact with the workshop. 8 | 9 | API key and domain have been registered and are to be placed in the `private` file. 10 | 11 | ## spaceships-analyzer 12 | A tool to compute metrics of interest for a set of spaceships downloaded. -------------------------------------------------------------------------------- /steam-workshop-downloader/common_atoms.json: -------------------------------------------------------------------------------- 1 | { 2 | "+": {"action": "move", "args": "R"}, 3 | "-": {"action": "move", "args": "L"}, 4 | "!": {"action": "move", "args": "U"}, 5 | "?": {"action": "move", "args": "D"}, 6 | ">": {"action": "move", "args": "F"}, 7 | "<": {"action": "move", "args": "B"}, 8 | "RotXcwY": {"action": "rotate", "args": "XcwY"}, 9 | "RotXcwZ": {"action": "rotate", "args": "XcwZ"}, 10 | "RotYcwX": {"action": "rotate", "args": "YcwX"}, 11 | "RotYcwZ": {"action": "rotate", "args": "YcwZ"}, 12 | "RotZcwX": {"action": "rotate", "args": "ZcwX"}, 13 | "RotZcwY": {"action": "rotate", "args": "ZcwY"}, 14 | "RotXccwY": {"action": "rotate", "args": "XccwY"}, 15 | "RotXccwZ": {"action": "rotate", "args": "XccwZ"}, 16 | "RotYccwX": {"action": "rotate", "args": "YccwX"}, 17 | "RotYccwZ": {"action": "rotate", "args": "YccwZ"}, 18 | "RotZccwX": {"action": "rotate", "args": "ZccwX"}, 19 | "RotZccwY": {"action": "rotate", "args": "ZccwY"}, 20 | "[": {"action": "push", "args": []}, 21 | "]": {"action": "pop", "args": []} 22 | } -------------------------------------------------------------------------------- /steam-workshop-downloader/configs.ini: -------------------------------------------------------------------------------- 1 | [API] 2 | host = localhost 3 | port = 3333 4 | [L-SYSTEM] 5 | common_atoms = common_atoms.json 6 | hl_atoms = hl_atoms.json 7 | pl_range = 1, 3 8 | req_tiles = cockpit,corridor,thruster 9 | n_iterations = 5 10 | n_axioms_generated = 2 11 | [GENOPS] 12 | mutations_lower_bound = -2 13 | mutations_upper_bound = 2 14 | mutations_initial_p = 0.4 15 | mutations_decay = 0.005 16 | crossover_p = 0.4 17 | [FI2POP] 18 | population_size = 20 19 | n_initial_retries = 100 20 | n_generations = 50 21 | [FITNESS] 22 | use_bounding_box = False 23 | bounding_box = 100.0,200.0,150.0 24 | # major axis / medium axis 25 | mame_mean = 1.77 26 | mame_std = 0.75 27 | # major axis / minimum axis 28 | mami_mean = 2.71 29 | mami_std = 1.24 30 | # functional blocks / total blocks 31 | futo_mean = 0.32 32 | futo_std = 0.1 33 | # total blocks / volume 34 | tovo_mean = 0.3 35 | tovo_std = 0.18 36 | [MAPELITES] 37 | max_x_size = 1000 38 | max_y_size = 1000 39 | max_z_size = 1000 40 | bin_population = 5 41 | max_age = 5 42 | n_dimensions_reduced = 10 43 | max_possible_dimensions = 500000 44 | epsilon_fitness = 1e-5 45 | alignment_interval = 3 46 | rescale_infeas_fitness = True 47 | [EXPERIMENT] 48 | n_runs = 50 49 | exp_name = base-exp-name 50 | [USER-STUDY] 51 | n_generations_allowed = 10 52 | n_emitter_steps = 5 53 | # ..., x axis size, y axis size, z axis size 54 | context_idxs = 4,5,6 55 | beta_a = 9 56 | beta_b = 9 -------------------------------------------------------------------------------- /steam-workshop-downloader/hl_atoms.json: -------------------------------------------------------------------------------- 1 | { 2 | "cockpit": { 3 | "dimensions": [25, 15, 25], 4 | "offset": 5 5 | }, 6 | "thrusters": { 7 | "dimensions": [25, 15, 35], 8 | "offset": 5 9 | }, 10 | "corridorsimple": { 11 | "dimensions": [25, 15, 25], 12 | "offset": 5 13 | }, 14 | "corridorreactors": { 15 | "dimensions": [25, 40, 45], 16 | "offset": 5 17 | }, 18 | "corridorcargo": { 19 | "dimensions": [25, 25, 35], 20 | "offset": 5 21 | }, 22 | "corridorgyros": { 23 | "dimensions": [25, 35, 45], 24 | "offset": 5 25 | }, 26 | "altthrusters": { 27 | "dimensions": [25, 15, 35], 28 | "offset": 5 29 | } 30 | } -------------------------------------------------------------------------------- /steam-workshop-downloader/hlrules: -------------------------------------------------------------------------------- 1 | # -- CARGO SHIP HIGH LEVEL RULES 2 | # - head 3 | head 1 cockpit 4 | # - tail 5 | tail 1 thrusters 6 | # - body 7 | body 1 corridorsimple(X) 8 | # extension 9 | corridorsimple(x) 0.25 corridorsimple(Y)corridorsimple(X) 10 | # introduce special blocks 11 | corridorsimple(x) 0.05 corridorsimple(Y)corridorreactors(X) 12 | corridorsimple(x) 0.15 corridorsimple(Y)corridorcargo(X) 13 | corridorsimple(x) 0.05 corridorsimple(Y)corridorgyros(X) 14 | corridorsimple(x)] 0.15 corridorsimple(Y)thrusters(1)] 15 | corridorsimple(x)] 0.85 corridorsimple(x)] 16 | # rotate 17 | corridorsimple(x) 0.125 corridorsimple(Y)[RotYcwXcorridorsimple(X)] 18 | corridorsimple(x) 0.125 corridorsimple(Y)[RotYccwXcorridorsimple(X)] 19 | corridorsimple(x) 0.125 corridorsimple(Y)[RotYcwZcorridorsimple(X)] 20 | corridorsimple(x) 0.125 corridorsimple(Y)[RotYccwZcorridorsimple(X)] -------------------------------------------------------------------------------- /tiles_maker.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import sys 4 | 5 | from pcgsepy.xml_conversion import extract_rule 6 | 7 | parser = argparse.ArgumentParser() 8 | parser.add_argument("--all", help="Extract all available tiles in the tileset folder.", 9 | action='store_true') 10 | 11 | args = parser.parse_args() 12 | 13 | if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): 14 | os.chdir(sys._MEIPASS) 15 | curr_folder = os.path.dirname(sys.executable) 16 | else: 17 | curr_folder = sys.path[0] 18 | 19 | BLUEPRINTS_DIR = os.path.join(curr_folder, 'tileset') 20 | if not os.path.exists(BLUEPRINTS_DIR): 21 | os.makedirs(BLUEPRINTS_DIR) 22 | available_tiles = os.listdir(BLUEPRINTS_DIR) 23 | 24 | # close the splash screen if launched via application 25 | try: 26 | import pyi_splash 27 | if pyi_splash.is_alive(): 28 | pyi_splash.close() 29 | except ModuleNotFoundError as e: 30 | pass 31 | 32 | if available_tiles: 33 | if args.all: 34 | for tile in available_tiles: 35 | rule, dims, offset = extract_rule(bp_dir=os.path.join(BLUEPRINTS_DIR, tile)) 36 | print(f'----\n{tile} % {rule}') 37 | print(f'"{tile}": ' + '{' + f'"dimensions": {dims}, "offset": {offset}' + '}') 38 | else: 39 | print('Available tiles:') 40 | for i, tile in enumerate(available_tiles): 41 | print(f" {i+1}. {tile}") 42 | t = int(input('Choose which tile to process (number): ')) - 1 43 | assert t > -1 and t < len(available_tiles), f'Invalid tile index: {t}' 44 | rule_name = input("Enter name of tile (leave blank to use folder's): ") 45 | rule_name = rule_name if rule_name else available_tiles[t] 46 | blueprint_directory = os.path.join(BLUEPRINTS_DIR, available_tiles[t]) 47 | rule, dims, offset = extract_rule(bp_dir=blueprint_directory, title=rule_name) 48 | print(f'\n\nTILE: {rule_name}') 49 | print('\nAdd to the low-level rules the following (replace the % with the desired probability):') 50 | print(f'{rule_name} % {rule}') 51 | print('\nAdd the following tile entry to the high-level atoms (if not present already):') 52 | print(f'"{rule_name}": ' + '{' + f'"dimensions": {dims}, "offset": {offset}' + '}') 53 | print(f'\nNow you can use {rule_name} in the high-level rules!') 54 | else: 55 | print('No tiles found!') -------------------------------------------------------------------------------- /tileset/CorridorWithCargo/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/tileset/CorridorWithCargo/thumb.png -------------------------------------------------------------------------------- /tileset/Thrusters/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodAI/space-engineers-ai-spaceship-generator/8a338616b06c707a5ffda0162f9da7e363d52496/tileset/Thrusters/thumb.png --------------------------------------------------------------------------------