├── .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 | [](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 |
26 |
39 |
41 |
43 |
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 |
26 |
43 |
45 |
47 |
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 |
26 |
43 |
45 |
47 |
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 |
26 |
39 |
41 |
43 |
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 |
26 |
39 |
41 |
43 |
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 |
40 |
42 |
44 |
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 |
26 |
55 |
57 |
59 |
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 |
26 |
35 |
37 |
39 |
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 |
26 |
39 |
41 |
43 |
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 | [](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 | [](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
--------------------------------------------------------------------------------