├── .coveragerc
├── .github
└── workflows
│ └── build_on_setup.yml
├── .gitignore
├── .versionrc
├── CHANGELOG.md
├── LICENSE
├── MapAnalyzer
├── Debugger.py
├── MapData.py
├── Pather.py
├── Polygon.py
├── Region.py
├── __init__.py
├── cext
│ ├── __init__.py
│ ├── mapanalyzerext.so
│ ├── src
│ │ └── ma_ext.c
│ └── wrapper.py
├── constants.py
├── constructs.py
├── decorators.py
├── destructibles.py
├── exceptions.py
├── pickle_gameinfo
│ ├── 2000AtmospheresAIE.xz
│ ├── AbyssalReefLE.xz
│ ├── AutomatonLE.xz
│ ├── BerlingradAIE.xz
│ ├── BlackburnAIE.xz
│ ├── CuriousMindsAIE.xz
│ ├── DeathAuraLE.xz
│ ├── EphemeronLE.xz
│ ├── EternalEmpireLE.xz
│ ├── EverDreamLE.xz
│ ├── GlitteringAshesAIE.xz
│ ├── GoldenWallLE.xz
│ ├── HardwireAIE.xz
│ ├── IceandChromeLE.xz
│ ├── JagannathaAIE.xz
│ ├── LightshadeAIE.xz
│ ├── NightshadeLE.xz
│ ├── OxideAIE.xz
│ ├── PillarsofGoldLE.xz
│ ├── RomanticideAIE.xz
│ ├── SimulacrumLE.xz
│ ├── SubmarineLE.xz
│ ├── Triton.xz
│ ├── WorldofSleepersLE.xz
│ └── ZenLE.xz
├── settings.py
└── utils.py
├── README.md
├── conftest.py
├── dummybot.py
├── ladder_build.sh
├── ladder_build
└── Dockerfile
├── monkeytest.py
├── package-lock.json
├── package.json
├── pf_perf.py
├── pytest.ini
├── requirements.dev.txt
├── requirements.txt
├── run.py
├── setup.py
├── tests
├── __init__.py
├── mocksetup.py
├── monkeytest.py
├── pathing_grid.txt
├── test_c_extension.py
├── test_docs.py
├── test_pathihng.py
└── test_suite.py
└── vb.py
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | omit =
3 | MapAnalyzer/sc2pathlibp/*
4 |
5 |
--------------------------------------------------------------------------------
/.github/workflows/build_on_setup.yml:
--------------------------------------------------------------------------------
1 | name: BuildOnSetup
2 |
3 | on:
4 | push:
5 | branches: [ master, develop ]
6 | pull_request:
7 | branches: [ master, develop ]
8 |
9 |
10 | jobs:
11 | build:
12 | runs-on: ${{ matrix.os }}
13 | strategy:
14 | matrix:
15 | os: [ubuntu-latest, windows-latest, macos-latest] # windows-latest need to figure out how to run on win64
16 | python-version: [3.7, 3.8, 3.9]
17 |
18 | steps:
19 | - uses: actions/checkout@v2
20 | - name: Set up ${{ matrix.python-version }}
21 | uses: actions/setup-python@v2
22 | with:
23 | python-version: ${{ matrix.python-version }}
24 | - name: Install dependencies
25 | run: |
26 | python -m pip install --upgrade pip
27 | pip install .[dev]
28 | - name: Test with pytest + Coverage
29 | run: |
30 | pytest --html=html/${{ matrix.os }}-test-results-${{ matrix.python-version }}.html
31 | - name: Upload pytest test results
32 | uses: actions/upload-artifact@v2
33 | with:
34 | name: pytest-results-${{ matrix.os }}-${{ matrix.python-version }}
35 | path: html/${{ matrix.os }}-test-results-${{ matrix.python-version }}.html
36 | # Use always() to always run this step to publish test results when there are test failures
37 | if: ${{ always() }}
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### Example user template template
3 | ### Example user template
4 | notes.md
5 | *.png
6 | # IntelliJ project files
7 | .idea
8 | *.iml
9 | out
10 | gen
11 | ### Python template
12 | # Byte-compiled / optimized / DLL files
13 | __pycache__/
14 | *.py[cod]
15 | *$py.class
16 |
17 | # C extensions
18 | *.so
19 |
20 | # Distribution / packaging
21 | node_modules/
22 | .Python
23 | build/
24 | develop-eggs/
25 | dist/
26 | downloads/
27 | eggs/
28 | .eggs/
29 | lib/
30 | lib64/
31 | parts/
32 | sdist/
33 | var/
34 | wheels/
35 | pip-wheel-metadata/
36 | share/python-wheels/
37 | *.egg-info/
38 | .installed.cfg
39 | *.egg
40 | MANIFEST
41 |
42 | # PyInstaller
43 | # Usually these files are written by a python script from a template
44 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
45 | *.manifest
46 | *.spec
47 |
48 | # Installer logs
49 | pip-log.txt
50 | pip-delete-this-directory.txt
51 |
52 | # Unit test / coverage reports
53 | htmlcov/
54 | .tox/
55 | .nox/
56 | .coverage
57 | .coverage.*
58 | .cache
59 | nosetests.xml
60 | coverage.xml
61 | *.cover
62 | .hypothesis/
63 | .pytest_cache/
64 | .benchmarks/
65 |
66 | # Translations
67 | *.mo
68 | *.pot
69 |
70 | # Django stuff:
71 | *.log
72 | local_settings.py
73 | db.sqlite3
74 | *.sqlite3
75 |
76 |
77 | # Flask stuff:
78 | instance/
79 | .webassets-cache
80 |
81 | # Scrapy stuff:
82 | .scrapy
83 |
84 | # Sphinx documentation
85 | docs/_build/
86 | *.html
87 | # PyBuilder
88 | target/
89 |
90 | # Jupyter Notebook
91 | .ipynb_checkpoints
92 | *.ipynb
93 |
94 | # IPython
95 | profile_default/
96 | ipython_config.py
97 |
98 | # pyenv
99 | .python-version
100 |
101 | # pipenv
102 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
103 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
104 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
105 | # install all needed dependencies.
106 | #Pipfile.lock
107 |
108 | # celery beat schedule file
109 | celerybeat-schedule
110 |
111 | # SageMath parsed files
112 | *.sage.py
113 |
114 | # Environments
115 | .env
116 | .venv
117 | env/
118 | venv/
119 | ENV/
120 | env.bak/
121 | venv.bak/
122 |
123 | # Spyder project settings
124 | .spyderproject
125 | .spyproject
126 |
127 | # Rope project settings
128 | .ropeproject
129 |
130 | # mkdocs documentation
131 | /site
132 |
133 | # mypy
134 | .mypy_cache/
135 | .dmypy.json
136 | dmypy.json
137 |
138 | # Pyre type checker
139 | .pyre/
140 |
141 | /tmap.py
142 |
--------------------------------------------------------------------------------
/.versionrc:
--------------------------------------------------------------------------------
1 | {
2 | "types": [
3 | {"type":"feat","section":"Features"},
4 | {"type":"fix","section":"Bug Fixes"},
5 | {"type":"perf","section":"Performance Improvements"},
6 | {"type":"refactor","section":"Refactoring"},
7 | {"type":"test","section":"Tests"},
8 | {"type":"build","section":"Build System"},
9 | {"type":"docs","section":"Documentation"}
10 | ]
11 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ### [0.0.87](https://github.com/spudde123/SC2MapAnalysis/compare/v0.0.86...v0.0.87) (2021-05-04)
6 |
7 | * Include function add_cost_to_multiple_grids to avoid calculating the same circle for every different grid the user
8 | wants to add the same cost to
9 | ([6baedc0](https://github.com/eladyaniv01/SC2MapAnalysis/commit/6baedc09ace996d918f78f79dbe0079d2e87bcb5),
10 | thanks to rasper!)
11 |
12 | ### [0.0.86](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.85...v0.0.86) (2021-04-03)
13 |
14 | ### Bug Fixes
15 |
16 | * Fixing bug when going in and out of the same nydus entrance when
17 | pathfinding ([abf444f1](https://github.com/eladyaniv01/SC2MapAnalysis/commit/abf444f1d5a14ae53052df53a3087ca07c524c0b))
18 |
19 | ### [0.0.85](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.84...v0.0.85) (2021-03-27)
20 |
21 | * Removed pyastar dependency and the deprecated pathfind_pyastar function
22 | completely ([c310ac4c](https://github.com/eladyaniv01/SC2MapAnalysis/commit/c310ac4cfca7e85d499da33822060c567993e145))
23 | * Added support for using nyduses with
24 | pathfinding ([636af5ea](https://github.com/eladyaniv01/SC2MapAnalysis/commit/636af5ea8bd233d27e0a5a68699c4390de250614))
25 |
26 | ### [0.0.84](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.83...v0.0.84) (2021-02-03)
27 |
28 | ### Refactoring
29 |
30 | * **test:** renamed test pathing function to be more descriptive of what it does ([778333d](https://github.com/eladyaniv01/SC2MapAnalysis/commit/778333d671a722eee1d4aac06e55c26ae89168b0))
31 |
32 | ### Tests
33 |
34 | * assert that illegal weights are tolerated in
35 | Pather ([4c846aa](https://github.com/eladyaniv01/SC2MapAnalysis/commit/4c846aa98144330b21273ec66fdc122fdc1a496d))
36 |
37 | ### Bug Fixes
38 |
39 | * fix add_cost crashing when a very specific input is
40 | present ([bc5052ec](https://github.com/eladyaniv01/SC2MapAnalysis/commit/bc5052ecbb6000f72e872ba7e18027400e90e9f7))
41 |
42 |
43 | ### [0.0.83](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.82...v0.0.83) (2021-02-02)
44 |
45 | ### Bug Fixes
46 |
47 | * Fix circle drawing ([be317f54](https://github.com/eladyaniv01/SC2MapAnalysis/pull/145/commits/be317f542703b78d988627960d3ca2360bcbbc0d))
48 | * Fix climber grid on DeathAura ([1f1def166](https://github.com/eladyaniv01/SC2MapAnalysis/pull/145/commits/1f1def166c378a5eed8c9a4f7eb8876ec8e66cd8))
49 |
50 |
51 | ### [0.0.82](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.81...v0.0.82) (2021-01-11)
52 |
53 | ### Bug Fixes
54 |
55 | * Fixed climber grid issue on SubmarineLE ([#140](https://github.com/eladyaniv01/SC2MapAnalysis/pull/140))
56 |
57 |
58 | ### [0.0.81](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.80...v0.0.81) (2021-01-06)
59 |
60 | ### Features
61 |
62 | * Adjust finding eligible pathing points ([090eed9](https://github.com/eladyaniv01/SC2MapAnalysis/pull/138/commits/090eed98b691711d8aa100b9ac992033a48433a0))
63 | * Break a choke check loop if choke is removed ([9413288](https://github.com/eladyaniv01/SC2MapAnalysis/pull/138/commits/941328872671158059f64e778d373c80bb610e5f))
64 | * Turn path smoothing around, so it works front to back ([9413288](https://github.com/eladyaniv01/SC2MapAnalysis/pull/138/commits/941328872671158059f64e778d373c80bb610e5f))
65 |
66 | ### Bug Fixes
67 |
68 | * Fix path smoothing calculations ([0a69a87](https://github.com/eladyaniv01/SC2MapAnalysis/pull/138/commits/0a69a8731050b303ca285b3558ff206f47ad13b1))
69 | * Fix variable having wrong type ([3a663d4](https://github.com/eladyaniv01/SC2MapAnalysis/pull/138/commits/3a663d4c9d7b0137674ffc77cff0a94ce20396f9))
70 | * Fix Crash on cleaning overlapping chokes ([6aebbc4](https://github.com/eladyaniv01/SC2MapAnalysis/pull/138/commits/6aebbc4d9feafc6162dd0f7dfa1c24b88dfbc53b))
71 |
72 |
73 | ### [0.0.80](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.79...v0.0.80) (2021-01-03)
74 |
75 | ### Features
76 |
77 | * Added support for array output in lowest_cost_points_array ([06d96e4](https://github.com/eladyaniv01/SC2MapAnalysis/pull/134/commits/06d96e4339745df4279846431e940ba5e58ec38a))
78 | * Pathfinding engine now takes into account unit size (footprint) ([ec3abaf5](https://github.com/eladyaniv01/SC2MapAnalysis/pull/134/commits/ec3abaf5783dd8c716ef82536b26b0155cce40c8))
79 | * New draw_circle function to replace skdraw.disk ([072ee6c](https://github.com/eladyaniv01/SC2MapAnalysis/pull/134/commits/072ee6c05ccfb3a437324bab98af244bc839d202))
80 | * Add function to find eligible points nearby in pather ([5a2e5c7](https://github.com/eladyaniv01/SC2MapAnalysis/pull/134/commits/5a2e5c72650554c20301afc6815128e275520327))
81 | * Add destructable type to grid calculations ([7495b9b](https://github.com/eladyaniv01/SC2MapAnalysis/pull/134/commits/7495b9b110d202dd0ba3b0fcf5d25e91e259fd73))
82 |
83 | #### [Updating pathing grid, pathfinding with different sized units, pathing grid fixes #130](https://github.com/eladyaniv01/SC2MapAnalysis/pull/130)
84 | * Pathfinding now allows to query with unit sizes x <= 1 and 1 < x <= 2
85 | * Pathfinding starting and end points are adjusted if they are inside some nonpathable thing such as a building or rocks. The new start or end point will be the closest pathable point, prioritizing points on the same terrain height level first before looking at other points
86 | * Pathing grids update during the game and mostly without much work
87 | * Pathing grid is also now much more accurate. The attempt is to represent each unit with precisely the right size instead of circles that are roughly correct
88 | * Creating new pathing grids when requested is much faster because we don't need to loop over the destructables
89 |
90 |
91 |
92 | ### Bug Fixes
93 |
94 | * air grid generation ([8acbed77](https://github.com/eladyaniv01/SC2MapAnalysis/pull/134/commits/8acbed77b947736902b8f8bc1d562bed5e9e8303))
95 | * air vs ground grid ([39ca942d](https://github.com/eladyaniv01/SC2MapAnalysis/pull/134/commits/39ca942d873a9f161ff756ea4390d9bfdca9a85b))
96 | * deprecated time.clock() on python version > 3.7 ([e83832d4](https://github.com/eladyaniv01/SC2MapAnalysis/pull/134/commits/e83832d43adb89fbb62ef42f6d438a85f0be3ed4))
97 | * All documentation example now work ([e6073df5](https://github.com/eladyaniv01/SC2MapAnalysis/pull/134/commits/e6073df5e9b04607cc7992d9c32182baa1c8ae4e))
98 |
99 |
100 | ### Tests
101 | * Test destructable types ([cafbead](https://github.com/eladyaniv01/SC2MapAnalysis/pull/134/commits/cafbead33a096df43e2800a470fc7995a5258e88))
102 | * fix air vs ground test ([a2f4e27](https://github.com/eladyaniv01/SC2MapAnalysis/pull/134/commits/a2f4e2702946d4432f0c25cf5c3b844fcb9d5619))
103 | * added --doctest-modules flag to pytest.ini. all doc tests use a Goldenwall MapData mock ([5364a6c3](https://github.com/eladyaniv01/SC2MapAnalysis/pull/134/commits/5364a6c31eeabfa1a6ac5f5d7081f0c9004d7f63))
104 |
105 |
106 | ### Issues Closed:
107 |
108 | * [#116 Update Readme and Docs branch](https://github.com/eladyaniv01/SC2MapAnalysis/issues/116)
109 | * [#109 Ramp objects should be updated during the game if they have destructables on them](https://github.com/eladyaniv01/SC2MapAnalysis/issues/109)
110 | * [#91 pathfind can return pathing outside of playable area.](https://github.com/eladyaniv01/SC2MapAnalysis/issues/91)
111 | * [#82 add instructions for cpp build tools for windows users](https://github.com/eladyaniv01/SC2MapAnalysis/issues/82) -- not needed
112 |
113 |
114 | ### [0.0.79](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.78...v0.0.79) (2020-12-30)
115 |
116 |
117 | ### Features
118 |
119 | * add a ladder compatible version of the c extension in the repo ([500a900](https://github.com/eladyaniv01/SC2MapAnalysis/commit/500a900d4d5ae22ab5d1391a23dd45ca129e43f7))
120 |
121 | ### Bug Fixes
122 |
123 | * CLI version parse error ([c133b7d](https://github.com/eladyaniv01/SC2MapAnalysis/commit/c133b7d9141a1fc6ca59dd91ceaceacd1a547aae))
124 |
125 | ### [0.0.78](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.76...v0.0.78) (2020-12-27)
126 |
127 | ### A* pathfinding is now implemented within the library, in C(default) [#111](https://github.com/eladyaniv01/SC2MapAnalysis/pull/111)
128 |
129 | * Implementing pathfinding and map analysis in C, allowing to remove pyastar and sc2pathlib as dependencies
130 | * Implementing smoothed pathfinding
131 | * Fixing choke objects in the mapanalyzer, so they have sides etc that better reflect reality
132 | * Fixing ramp objects that come from burnysc2 if the info is broken due to
133 | destructables ([a8ad54a](https://github.com/spudde123/SC2MapAnalysis/commit/a8ad54aeb17a5e0dab919b18cfd72d888e7c3624))
134 | * overlord spots +
135 | drawing ([bfac2bb](https://github.com/spudde123/SC2MapAnalysis/commit/bfac2bb742f35ea33a5286670b0d4b4b271c11dc))
136 |
137 | ### Refactoring
138 | * moving raw chokes into their own class(
139 | RawChoke) ([bf9aebb](https://github.com/spudde123/SC2MapAnalysis/commit/bf9aebbb160367f74c8dc34ed76c11ef66c2b6f5))
140 |
141 | ### Tests
142 | * test that for each choke a main line actually
143 | exists ([dcef428](https://github.com/spudde123/SC2MapAnalysis/commit/dcef4280722dd758a5fd4c0cb04c42fe833fe4bc))
144 | * testing the choke sides are included in choke
145 | points ([308cf29](https://github.com/eladyaniv01/SC2MapAnalysis/commit/308cf29a36115c29fc5fa7b96d86b9a77cfaf337))
146 |
147 | ### ⚠ BREAKING CHANGES
148 | * remove
149 | sc2pathlib ([787c6b8](https://github.com/spudde123/SC2MapAnalysis/commit/787c6b8844c24f58d0ea75a1d295b44821b2cc4b))
150 | * pyastar is now deprecated, yet still supported (and can be accessed via `pathfind_pyastar`)
151 | * RawChoke objects from C ext instead of PathlibChokes from former sc2pathlib
152 |
153 | ### Features
154 |
155 | * Improve find_lowest_cost_efficiency ([#114](https://github.com/eladyaniv01/SC2MapAnalysis/pull/114))
156 |
157 | ### Bug Fixes
158 |
159 | * int cast
160 | bug ([b055bf4](https://github.com/eladyaniv01/SC2MapAnalysis/commit/b055bf4fb5fcd82e6d8e45025451f6ced8edc2b3))
161 | * scout now walks the
162 | path ([2f04b03](https://github.com/eladyaniv01/SC2MapAnalysis/commit/2f04b035baae8eacceaa7d5d127fe7acf7315d11))
163 | * temp fix for climber grid
164 | test ([1f8f611](https://github.com/eladyaniv01/SC2MapAnalysis/commit/1f8f611ccb843ec903a8cf777a6b0879659d3b0d))
165 | * Fix crash on python3.9 ([#112](https://github.com/eladyaniv01/SC2MapAnalysis/pull/112))
166 |
167 | ### Issues Closed:
168 |
169 | * [#104 float based position & radius should be better for functions like add_cost](https://github.com/eladyaniv01/SC2MapAnalysis/issues/104)
170 | * [#97 Bug: Geysers too small on pathing grid (EternalEmpire 3oclock base)](https://github.com/eladyaniv01/SC2MapAnalysis/issues/97)
171 | * [#94 Bug: choke.left and choke.right don't take account the orientation of the choke](https://github.com/eladyaniv01/SC2MapAnalysis/issues/94)
172 |
173 |
174 | ### [0.0.77](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.76...v0.0.77) (2020-12-13)
175 |
176 | ### Features
177 |
178 | * Debugger now plots choke side a and side b with text indicators (sA, sB) ([9dd8c07](https://github.com/eladyaniv01/SC2MapAnalysis/commit/9dd8c072ebab8a66b99c42f792d7cde4c87a1fce))
179 | * Polygon now has top,bottom,right,left properties ([8f5b0c9](https://github.com/eladyaniv01/SC2MapAnalysis/commit/8f5b0c9cd48a2f23dd1666f14becd41aae815df7))
180 | * Round position in add_cost to reduce inaccuracy ([#100](https://github.com/eladyaniv01/SC2MapAnalysis/pull/100))
181 |
182 | ### Refactoring
183 |
184 | * left right of choke are now side_a and side_b, sides are computed accuratly
185 | now ([125f881](https://github.com/eladyaniv01/SC2MapAnalysis/commit/125f8812266c0bc80931c8d80f822276be5753ef))
186 |
187 | ### Tests
188 |
189 | * testing the choke sides are included in choke
190 | points ([e3b0b26](https://github.com/eladyaniv01/SC2MapAnalysis/commit/e3b0b26d556d4cde3c7724715e6a33c76a9e5c5e))
191 |
192 | ### Issues Closed:
193 |
194 | * [#94 Bug: choke.left and choke.right don't take account the orientation of the choke](https://github.com/eladyaniv01/SC2MapAnalysis/issues/94)
195 |
196 | ### [0.0.76](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.75...v0.0.76) (2020-11-24)
197 |
198 | ### Features
199 |
200 | * add `include_destructables` to `get_climber_grid` in
201 | mapdata ([0e8aaf9](https://github.com/eladyaniv01/SC2MapAnalysis/commit/0e8aaf926013eb04c40c1a2c12324ad929a4496d))
202 |
203 | ### [0.0.75](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.74...v0.0.75) (2020-11-07)
204 |
205 | ### Features
206 |
207 | * ChokeArea now has left/right properties ([be13d59](https://github.com/eladyaniv01/SC2MapAnalysis/commit/be13d598a7fc8ab1d2ab5e7932d36698b9635cb6))
208 | * MDRamp offset attribute for walloff, corner_walloff, middle_walloff_depot properties ([5fb232c](https://github.com/eladyaniv01/SC2MapAnalysis/commit/5fb232c9f21e4971a0508ad887561824780c50c8))
209 | * plot_map now also draws the Left/Right bounds of each choke ([da66c0e](https://github.com/eladyaniv01/SC2MapAnalysis/commit/da66c0e7c0dfe90214463e374b1b23fbc513c02e))
210 | * WIP wall off points for each and every choke ([e99bb63](https://github.com/eladyaniv01/SC2MapAnalysis/commit/e99bb63acba3ca57e9a51b16cb22b6c0bab7f395))
211 |
212 |
213 | ### Bug Fixes
214 |
215 | * fix install order in setup ([cdbf0f7](https://github.com/eladyaniv01/SC2MapAnalysis/commit/cdbf0f7de2d0d736ac07153467ff65a29dfe898d))
216 | * overlapping chokes from sc2pathlib are merged (still WIP) ([38cdaa5](https://github.com/eladyaniv01/SC2MapAnalysis/commit/38cdaa506af8b4360ae57dcb713e79c7e3f07e70))
217 | * typo, import logger from loguru ([64d0960](https://github.com/eladyaniv01/SC2MapAnalysis/commit/64d096048ad447dad77f3227f53a3cfbc5b7ccb9))
218 |
219 |
220 | ### Refactoring
221 |
222 | * add_cost is now a static method ([8217749](https://github.com/eladyaniv01/SC2MapAnalysis/commit/8217749cff9e3f5abd8465227590a2c91ff338f5))
223 | * overall static methods to be static ([66b4b0e](https://github.com/eladyaniv01/SC2MapAnalysis/commit/66b4b0e010f08ce04b18c441eeab3d0b56197af4))
224 | * remove log method from MapData ([0d98393](https://github.com/eladyaniv01/SC2MapAnalysis/commit/0d9839343974126eef35e85cf2cb493b2a463edf))
225 |
226 | ### [0.0.74](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.73...v0.0.74) (2020-10-17)
227 |
228 |
229 | ### Features
230 |
231 | * added example bot ref to README.MD ([7ceedf2](https://github.com/eladyaniv01/SC2MapAnalysis/commit/7ceedf2d16f86b34ecb7ad2b32e594987c2947f1))
232 | * dummybot.py is an example bot with a few handy use cases showing ([35f1cc9](https://github.com/eladyaniv01/SC2MapAnalysis/commit/35f1cc9295e1afac3d59ee68bb8f767f0d350c91))
233 |
234 | ### Issues Closed:
235 |
236 | * [#74 create basic example bot usage ](https://github.com/eladyaniv01/SC2MapAnalysis/issues/74)
237 |
238 |
239 | ### [0.0.73](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.72...v0.0.73) (2020-10-01)
240 |
241 |
242 | ### Bug Fixes
243 |
244 | * fix clean_air_grid being wrongly constructed of integers instead of np.float32 ([278241c](https://github.com/eladyaniv01/SC2MapAnalysis/commit/278241c7378709ead938b6ddb84eda4f6d6c0e10))
245 |
246 |
247 | ### Documentation
248 |
249 | * Added instruction on how to query cost in a specific point on the grid ([e0c8e19](https://github.com/eladyaniv01/SC2MapAnalysis/commit/e0c8e19f0602576eadc4b19d84035405ad61f761))
250 |
251 |
252 | ### Tests
253 |
254 | * now testing clean_air_grid for dtype bugfix by checking path lengths ([b47bae4](https://github.com/eladyaniv01/SC2MapAnalysis/commit/b47bae4f412cdd1e6785a0bb68c4eed8789f62f2))
255 |
256 | ### [0.0.72](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.71...v0.0.72) (2020-09-29)
257 |
258 |
259 | ### Bug Fixes
260 |
261 | * fix double transpose bug that happens when requesting low cost points more than once per frame ([ffd6b84](https://github.com/eladyaniv01/SC2MapAnalysis/commit/ffd6b843520a1b9eb3180d1a8429485495b61461))
262 |
263 |
264 | ### Tests
265 |
266 | * find low cost points now also tests that the distance between the point and the origin makes sense( the array is not transposed) ([76a0e19](https://github.com/eladyaniv01/SC2MapAnalysis/commit/76a0e191fcd838c95a627df2b5ec531bd5b325d0))
267 |
268 | ### [0.0.71](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.70...v0.0.71) (2020-09-28)
269 |
270 |
271 | ### Bug Fixes
272 |
273 | * fixed transposed grid bug when searching for low cost points , added support for grids constructed from outside sources ([c127901](https://github.com/eladyaniv01/SC2MapAnalysis/commit/c1279014e36adbdeb578b7d5717a1833eb5bdefc))
274 | * log compatability with bots ([8960716](https://github.com/eladyaniv01/SC2MapAnalysis/commit/896071677394c08993c10b4ea61025cb75620ad9))
275 |
276 |
277 | ### Tests
278 |
279 | * find low cost points is now tested on all grid types ([6ec010d](https://github.com/eladyaniv01/SC2MapAnalysis/commit/6ec010d172d609a7d867a6c51b29320cf310b062))
280 |
281 | ### [0.0.70](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.69...v0.0.70) (2020-09-22)
282 |
283 | ### Bug Fixes
284 |
285 | * MapData now accepts a `corner_distance` variable that will determine the corner distance calculation ([294df181](https://github.com/eladyaniv01/SC2MapAnalysis/commit/294df181c272b218eef0c167f24d1db7650a7202))
286 |
287 |
288 | ### Build System
289 |
290 | * add wheel to requirements and setup.py ([eb663a9](https://github.com/eladyaniv01/SC2MapAnalysis/commit/eb663a95c23c1e180ea385a783af306f47178c6e))
291 |
292 | ### Issues Closed:
293 |
294 | * [#88 BUG: all possible corners are not found ](https://github.com/eladyaniv01/SC2MapAnalysis/issues/88)
295 |
296 | ### [0.0.69](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.68...v0.0.69) (2020-09-19)
297 |
298 |
299 | ### Bug Fixes
300 |
301 | * fix pathing grid set up incorrectly when there are no vision blockers ([d49a084](https://github.com/eladyaniv01/SC2MapAnalysis/commit/d49a084543a90efc5d515b8af43072a1c1e39adb))
302 |
303 | ### [0.0.68](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.67...v0.0.68) (2020-09-19)
304 |
305 |
306 | ### Features
307 |
308 | * Buildable points now respect flying buildings ([b90123f](https://github.com/eladyaniv01/SC2MapAnalysis/commit/b90123f84651488af8a3eea794b95cf94df0b094))
309 | * pathfind method will now return the path without the start point in it ([06481d8](https://github.com/eladyaniv01/SC2MapAnalysis/commit/06481d8dfec5856bc966c4ec67f5d76c73dc460b))
310 |
311 | ### Bug Fixes
312 |
313 | * Excluded lowered supply depots from non pathables #85 ([#85](https://github.com/eladyaniv01/SC2MapAnalysis/pull/85))
314 |
315 | ### Issues Closed:
316 |
317 | * [#84 Bug: Lowered supply depots added to non-pathable ground grid](https://github.com/eladyaniv01/SC2MapAnalysis/issues/84)
318 |
319 | ### [0.0.67](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.66...v0.0.67) (2020-09-17)
320 |
321 |
322 | ### Features
323 |
324 | * add cost now accepts `initial_default_weights` argument ( known use case is for air_vs_ground grid ) ([7fe542f](https://github.com/eladyaniv01/SC2MapAnalysis/commit/7fe542f0a15daf90b0136453c0c6aa8f68242dd4))
325 |
326 | ### Issues Closed:
327 |
328 | * [#81 Air vs ground grid cost values in non ground pathable areas](https://github.com/eladyaniv01/SC2MapAnalysis/issues/81)
329 |
330 | ### [0.0.66](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.64...v0.0.66) (2020-09-10)
331 |
332 | ### Features
333 |
334 | * map_data.find_lowest_cost_points within a radius ([ab5073c](https://github.com/eladyaniv01/SC2MapAnalysis/pull/80/commits/ab5073cb57d0f5411d0f35a81c5c5b7a8609f602))
335 | * map_data.draw_influence_in_game ([23d0004](https://github.com/eladyaniv01/SC2MapAnalysis/pull/79/commits/23d00040fe9c35c106152cd8d09a83c90a4e8795))
336 |
337 |
338 | ### Issues Closed:
339 |
340 | * [#78 Feature request: find lowest influence / cost within a radius](https://github.com/eladyaniv01/SC2MapAnalysis/issues/78)
341 |
342 | ### [0.0.64](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.63...v0.0.64) (2020-09-06)
343 |
344 | ### Bug Fixes
345 |
346 | * (81, 29) on EverDreamLE is considered in map bounds even though it is not ([63b91f5](https://github.com/eladyaniv01/SC2MapAnalysis/commit/63b91f5d80688697617175a0155c68f3bd2b2668))
347 | * air_vs_ground grid now accounts ramp points as pathable ([99ad04d](https://github.com/eladyaniv01/SC2MapAnalysis/commit/99ad04da4b57b360bf82ebcb930b1a9213f21d3d))
348 |
349 |
350 | ### Performance Improvements
351 |
352 | * removed duplicate calculations from polygon _set_points ([948edeb](https://github.com/eladyaniv01/SC2MapAnalysis/commit/948edeb362bc1f4c15abe38a9864f17db70d6039))
353 |
354 |
355 | ### Tests
356 |
357 | * air_vs_ground grid now tests that ramps are computed correctly ([6a37be1](https://github.com/eladyaniv01/SC2MapAnalysis/commit/6a37be15769774a2f25c43d8b08a523378d7875a))
358 |
359 | ### Issues Closed:
360 |
361 | * [#77 ramp points which are pathable, are not computed correctly in the air vs ground grid](https://github.com/eladyaniv01/SC2MapAnalysis/issues/77)
362 | * [#76 Bug (81, 29) is not in map bounds even though it passes the in_bounds check on EverDreamLE](https://github.com/eladyaniv01/SC2MapAnalysis/issues/76)
363 |
364 |
365 | ### [0.0.63](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.62...v0.0.63) (2020-09-02)
366 |
367 |
368 | ### Bug Fixes
369 |
370 | * debugger will now not try to inherit sc2 logger when in arcade mode ([03fefd7](https://github.com/eladyaniv01/SC2MapAnalysis/commit/03fefd71869d56fff31a32b24743f34d80538c2e))
371 | * mapdata will now compile the map in arcade mode, with limited information available ([92d9a5f](https://github.com/eladyaniv01/SC2MapAnalysis/commit/92d9a5fb8c438e3bfea0a879ae812b4113c82ec3))
372 |
373 | ### [0.0.62](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.61...v0.0.62) (2020-09-02)
374 |
375 |
376 | ### Features
377 |
378 | * Buildable points are now mapped to Point2, logger now inherits sc2.main.logger ([7d8dc88](https://github.com/eladyaniv01/SC2MapAnalysis/commit/7d8dc8801eaafd435a53be2b914d07326675634f))
379 | * MapData now accepts arcade boolean argument on init ([d380f2e](https://github.com/eladyaniv01/SC2MapAnalysis/commit/d380f2e22f96e897f2a6cf1c323f2f412433f2d1))
380 |
381 |
382 | ### Performance Improvements
383 |
384 | * Pather changed the unpacking method of vision blockers into the grid ([7f4b9c3](https://github.com/eladyaniv01/SC2MapAnalysis/commit/7f4b9c3293b888305c1259f295ce15abfc363277))
385 |
386 | ### [0.0.61](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.60...v0.0.61) (2020-08-30)
387 |
388 |
389 | ### Performance Improvements
390 |
391 | * no need to convert path(numpy) to list anymore [https://github.com/BurnySc2/python-sc2/commit/cac70d738a24fcac749d371e8e0bed5e83b26b9e]
392 |
393 | ### Features
394 |
395 | * added check that returns only points that are in bounds ([86891c9](https://github.com/eladyaniv01/SC2MapAnalysis/commit/86891c9fb8560ffa8006bc8e9ff6e88d1952f668))
396 |
397 | * pather now includes geysers as non pathable ([94d592c](https://github.com/eladyaniv01/SC2MapAnalysis/commit/94d592c5e76b2cca79786ce963269521ceacd9a8))
398 |
399 | ### Issues Closed:
400 |
401 | * [#70 pather ignores geysers](https://github.com/eladyaniv01/SC2MapAnalysis/issues/70)
402 | * [#73 clean_air_grid size is bigger than playable area size, so pather with try to path to areas where units can't go](https://github.com/eladyaniv01/SC2MapAnalysis/issues/73)
403 |
404 |
405 | ### [0.0.60](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.59...v0.0.60) (2020-08-30)
406 |
407 |
408 | ### Features
409 |
410 | * visionblockers are now counted as pathable in the path grid ([8120fe9](https://github.com/eladyaniv01/SC2MapAnalysis/commit/8120fe954faab20ef88f3efd7522c1d521cab530))
411 |
412 | ### Issues Closed:
413 |
414 | * [#72 VisionBlockerArea is considered unpathable by the pathing grid ](https://github.com/eladyaniv01/SC2MapAnalysis/issues/72)
415 |
416 | ### [0.0.59](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.58...v0.0.59) (2020-08-27)
417 |
418 |
419 | ### Refactoring
420 |
421 | * removed in_region_i ([f8c7103](https://github.com/eladyaniv01/SC2MapAnalysis/commit/f8c710323456bb9755fd093783ea5ee9b2a7c058))
422 |
423 |
424 | ### Tests
425 |
426 | * region tests now deault to using `where_all` instead of `where` ([1ec9fc1](https://github.com/eladyaniv01/SC2MapAnalysis/commit/1ec9fc1a7c0c675544c25bf5ad948c0d3f46ea62))
427 | * removed `is_inside_indices` ([429dd52](https://github.com/eladyaniv01/SC2MapAnalysis/commit/429dd5251312c035482b5ef861dc6bb63894c66e))
428 |
429 | ### [0.0.58](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.57...v0.0.58) (2020-08-26)
430 |
431 |
432 | ### Bug Fixes
433 |
434 | * geysers are now accounted for iin the path grid, lowered mineral radius, changed the radius constants to more descriptive name (radius_factor) ([94d592c](https://github.com/eladyaniv01/SC2MapAnalysis/commit/94d592c5e76b2cca79786ce963269521ceacd9a8))
435 |
436 | ### [0.0.57](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.56...v0.0.57) (2020-08-26)
437 |
438 |
439 | ### ⚠ BREAKING CHANGES
440 |
441 | * Polygon.buildable_points -> Polygon.buildables
442 |
443 | ### Features
444 |
445 | * debugger now has .scatter method, moved readme examples to docs ([91be892](https://github.com/eladyaniv01/SC2MapAnalysis/commit/91be8925bcb22815c0b2edb806191a138467c6fd))
446 |
447 |
448 | ### Documentation
449 |
450 | * slight style changes ([dbc321c](https://github.com/eladyaniv01/SC2MapAnalysis/commit/dbc321c6eb6e8c8d35e3bca8cfd8eb973dd0581a))
451 |
452 |
453 | ### Refactoring
454 |
455 | * BuildablePoints to Buildables ([ce3b7ac](https://github.com/eladyaniv01/SC2MapAnalysis/commit/ce3b7acb2d3cacdf7b744d76c2f9e02aabe71413))
456 | * Pather add_influence to add_cost ([8cc6c47](https://github.com/eladyaniv01/SC2MapAnalysis/commit/8cc6c4747f1d0aa0228037cffa3d2989ee9ece83))
457 |
458 | ### [0.0.56](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.55...v0.0.56) (2020-08-22)
459 |
460 |
461 | ### Bug Fixes
462 |
463 | * fixed the calling order in polygon._set_points() ([2e60287](https://github.com/eladyaniv01/SC2MapAnalysis/commit/2e6028708efd7756925f0748f5aa8a4b14c80d14))
464 |
465 |
466 | ### Documentation
467 |
468 | * pretty up with autoapi ([4835350](https://github.com/eladyaniv01/SC2MapAnalysis/commit/48353502ced12f937d36500702a9b4bc07e4c9e5))
469 |
470 |
471 | ### Refactoring
472 |
473 | * version attribute is now in MapAnalyzer.__init__.py , updated versionbump methods in vb, and adjusted setup ([4409c7d](https://github.com/eladyaniv01/SC2MapAnalysis/commit/4409c7d2aea65b7ec5599916e0d466d3d82a4062))
474 |
475 | ### [0.0.55](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.54...v0.0.55) (2020-08-20)
476 |
477 |
478 | ### Bug Fixes
479 |
480 | * all point properties are now converted to int instead of np.int + tests for tuple vs Point2 ([120b27f](https://github.com/eladyaniv01/SC2MapAnalysis/commit/120b27fae6835dd7f4d3b00b0252d9b93068ac6c))
481 |
482 |
483 | ### Refactoring
484 |
485 | * Polygon now calls ._set_points() instead of doing it in __init__ ([02229e2](https://github.com/eladyaniv01/SC2MapAnalysis/commit/02229e29b98570aafd28fa76e3edb0a293b45a26))
486 |
487 |
488 | ### Documentation
489 |
490 | * build changlog for new version ([b5b357b](https://github.com/eladyaniv01/SC2MapAnalysis/commit/b5b357bf3549b7ff8b4d6752b1bf154a724f5fd3))
491 | * clean up + fix vb makedocs ([3e68375](https://github.com/eladyaniv01/SC2MapAnalysis/commit/3e6837557536b9258cbd4a766c4bfafbb308fbe6))
492 | * clean up readme a bit ([d7250fa](https://github.com/eladyaniv01/SC2MapAnalysis/commit/d7250fa312bf3d10b19db6c596cedd90e56586d7))
493 | * documented polygon, region, constructs ([131bbd3](https://github.com/eladyaniv01/SC2MapAnalysis/commit/131bbd3793d2c981f25740cc98b8f540a6479859))
494 |
495 | ### [0.0.54](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.48...v0.0.54) (2020-08-19)
496 |
497 |
498 | ### ⚠ BREAKING CHANGES
499 |
500 | ### Refactoring
501 |
502 | * *add_influence* to *add_cost*, r to radius, p to position, arr to grid ([4bfbfee](https://github.com/eladyaniv01/SC2MapAnalysis/commit/4bfbfee6edbddaa1f45a0d1bc06b5edc61bcb643))
503 | * compile_map is now a private method (_compile_map) ([8b27883](https://github.com/eladyaniv01/SC2MapAnalysis/commit/8b2788367c92f4eb5a4201ae96c165c75687f830))
504 |
505 | ### Bug Fixes
506 |
507 | * all points returned from mapdata object will now be of type Point2, and populated with standard integers
508 |
509 | ### Documentation
510 |
511 | * documentation is in draft stage and can be found at https://eladyaniv01.github.io/SC2MapAnalysis
512 |
513 |
514 | ### [0.0.53](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.48...v0.0.53) (2020-08-16)
515 |
516 | ### Features
517 |
518 | * MapData.region_connectivity_all_paths will now return all options for pathing from Region to Region, while respecting a not_through list of regions to be excluded. ([9758950](https://github.com/eladyaniv01/SC2MapAnalysis/commit/975895046b8bda12af90e2c413a106ce013443fe))
519 |
520 | * Base (#65) -> dev -> master (#66) ([60e2d2d](https://github.com/eladyaniv01/SC2MapAnalysis/commit/60e2d2de8cb89d6649a09e8e66e379483d0a7a0f)), closes [#65](https://github.com/eladyaniv01/SC2MapAnalysis/issues/65) [#66](https://github.com/eladyaniv01/SC2MapAnalysis/issues/66) [#64](https://github.com/eladyaniv01/SC2MapAnalysis/issues/64)
521 |
522 | ### Issues Closed:
523 | * [#51 Feature request: Pathfinding through regions](https://github.com/eladyaniv01/SC2MapAnalysis/issues/51)
524 |
525 | ### [0.0.52](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.51...v0.0.52) (2020-08-15)
526 |
527 | ### ⚠ BREAKING CHANGES
528 |
529 | * Region is now a Child of Polygon (Refactor)
530 |
531 | ### Bug Fixes
532 |
533 | * regions and ramps now set each other correctly
534 |
535 | * mapdata test for plotting ([b987fb6](https://github.com/eladyaniv01/SC2MapAnalysis/commit/b987fb6c29863cf57b30abfa5dad3b152456bcab))
536 |
537 | * Base (#65) ([209d6d1](https://github.com/eladyaniv01/SC2MapAnalysis/commit/209d6d1c065893f98ce6bbfaeb34ab38b74e41a9)), closes [#65](https://github.com/eladyaniv01/SC2MapAnalysis/issues/65) [#64](https://github.com/eladyaniv01/SC2MapAnalysis/issues/64)
538 |
539 |
540 | ### [0.0.51](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.50...v0.0.51) (2020-08-14)
541 |
542 | ### Bug Fixes
543 |
544 | * __bool__ compatibility with burnysc2 ([9618799](https://github.com/eladyaniv01/SC2MapAnalysis/commit/9618799a50f2fffcb78aa1f802e3903598c9a8ce))
545 |
546 | ### [0.0.50](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.49...v0.0.50) (2020-08-13)
547 |
548 |
549 | ### Features
550 |
551 | * Polygon/ Region now has the property 'buildable_points' ([25952f7](https://github.com/eladyaniv01/SC2MapAnalysis/commit/25952f75ed05a762124ae97e8425c946dd4cf058))
552 |
553 |
554 | ### Bug Fixes
555 |
556 | * [[#61]](https://github.com/eladyaniv01/SC2MapAnalysis/issues/61) pathfind will not crash the bot when start or goal are not passed properly, also a logging error will print out ([08466b5](https://github.com/eladyaniv01/SC2MapAnalysis/commit/08466b5e3650a694bf5b0d633b894fdb6bfbd7b8))
557 | * fix point_to_numpy_array method ([4d56755](https://github.com/eladyaniv01/SC2MapAnalysis/commit/4d567559e3eceede00615c637458cdb8a0870d36))
558 | * points_to_numpy_array now filters out outofbounds ([aedf9d2](https://github.com/eladyaniv01/SC2MapAnalysis/commit/aedf9d2ba45f585a279d7a014a7e990cbb9359a9))
559 |
560 |
561 | ### [0.0.49](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.48...v0.0.49) (2020-08-12)
562 |
563 |
564 | ### Bug Fixes
565 |
566 | * [#58](https://github.com/eladyaniv01/SC2MapAnalysis/issues/58) climber_grid is now aware of nonpathables (such as minerals, destructibles etc) ([98dcbec](https://github.com/eladyaniv01/SC2MapAnalysis/commit/98dcbec074e032047d4eacbb5f97e1961f06d395))
567 | * [#59](https://github.com/eladyaniv01/SC2MapAnalysis/issues/59) clean air grid will now accept 'default_weight' argument (MapData, Pather) ([355ab7d](https://github.com/eladyaniv01/SC2MapAnalysis/commit/355ab7d8f0522a905d77007e979d3e57734bc4e7))
568 | * climber_grid tests ([5216d0c](https://github.com/eladyaniv01/SC2MapAnalysis/commit/5216d0c8e796a3284c8e531e2ce4dcf3075582f4))
569 | * import error on region-polygon ([b8ea912](https://github.com/eladyaniv01/SC2MapAnalysis/commit/b8ea9126a6dea792d9d62538688c6d9d15c395d8))
570 | * mapdata test for plotting ([26a7c15](https://github.com/eladyaniv01/SC2MapAnalysis/commit/26a7c154a4a973995cea67267097aae0f9d58681))
571 | * versionbump cli now commits setup.py before calling standard-version ([50eb667](https://github.com/eladyaniv01/SC2MapAnalysis/commit/50eb667c48949a0847742ed5aec8957f07cd8ff9))
572 |
573 |
574 | ### Documentation
575 |
576 | * added cli reminder to commit setup.py on versionbump ([28a65a3](https://github.com/eladyaniv01/SC2MapAnalysis/commit/28a65a303a14ae08bf4b00253cb2ace13b8b5cff))
577 |
578 | ### [0.0.48](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.47...v0.0.48) (2020-08-11)
579 |
580 |
581 | ### Bug Fixes
582 |
583 | * path through destructables. destructables rocks radius factor from 0.8 to 1 ([2fd1a32](https://github.com/eladyaniv01/SC2MapAnalysis/commit/2fd1a326668f31317131a7586a549f472e986625))
584 |
585 | ### [0.0.47](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.46...v0.0.47) (2020-08-11)
586 |
587 | ### Features
588 |
589 | * feat: get_air_vs_ground_grid, get_clean_air_grid
590 |
591 | ### BugFixes
592 |
593 | * fix: max_region_size up to 8500 to fix goldenwall
594 | * added air_pathing deprecation warning for requesting through pyastar
595 |
596 | ### Issues Closed:
597 | * [#53 Feature request: get dead airspace from map analyzer](https://github.com/eladyaniv01/SC2MapAnalysis/issues/53)
598 |
599 | ### [0.0.46](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.45...v0.0.46) (2020-08-10)
600 |
601 | ### Documentation
602 |
603 | * moved changelog from readme.md to changelog.md ([0927cba](https://github.com/eladyaniv01/SC2MapAnalysis/commit/0927cbab8bbd2136de3527c314ab2cbd8f304cf5))
604 |
605 | ### [0.0.45](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.44...v0.0.45) (2020-08-10)
606 |
607 | ### Features
608 |
609 | Climber grid (#52)
610 | * feat: get_climber_grid a pathing grid for climbing units such as colossus and reapers
611 |
612 | ### [0.0.43](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.42...v0.0.43) (2020-08-10)
613 |
614 | ### BugFixes:
615 |
616 | * Pather will no longer try to path through mineral walls ( like in goldenwall)
617 | * Fix circular import bug on Debugger
618 | * Fixed malformed point orientation in rare cases
619 |
620 | ### Tests
621 |
622 | * Now testing both installation types (setup.py and requirements.txt)
623 | * Every test will now use the MapAnalyzer.util functions when it can
624 | * Removed redundant test_map_data from TestSanity, and put it in its right place, test_mapdata
625 |
626 | ### Issues Closed:
627 | * [#46 Feature Request: possibility to enable pathing through rocks when calculating a path](https://github.com/eladyaniv01/SC2MapAnalysis/issues/46)
628 | * [#45 Feature Request: Add air grid](https://github.com/eladyaniv01/SC2MapAnalysis/issues/45)
629 | * [#44 Feature Request: add a setting for a custom default weight for the pathing grid](https://github.com/eladyaniv01/SC2MapAnalysis/issues/44)
630 | * [#39 Feature : add path_sensitivity for returning a sliced path (every nth point )](https://github.com/eladyaniv01/SC2MapAnalysis/issues/39)
631 | * [#38 pathfiner should return a list of Point2 and not numpy array](https://github.com/eladyaniv01/SC2MapAnalysis/issues/38)
632 |
633 |
Code Changes
634 |
635 | ### Debugger
636 | * Now inspects stack and will not save on tests
637 | * Will no longer circular call map_data for plotting
638 |
639 | ### Pather
640 | * Radius for resource blockers is now 2, this passes all tests
641 |
642 | ### MapData
643 |
644 | * Grouped methods in map_data for better readablitiy
645 | * Moved `get_sets_with_mutual_elements` to utils
646 | * Resource_blockers are now calculated with original coords
647 | * Removed usage of neighbores , instead adding influence with radius
648 |
649 |
650 | ### [0.0.42](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.40...v0.0.42) (2020-08-07)
651 |
652 | * feat Version bump cli (#50) ([b753d34](https://github.com/eladyaniv01/SC2MapAnalysis/commit/b753d3442421dd524d1c0043c4794f46b5a0b082)), closes [#50](https://github.com/eladyaniv01/SC2MapAnalysis/issues/50)
653 |
654 | ### Features
655 |
656 | * **cli interface for version bump:** Bump Version + changelog generation ([20b1e70](https://github.com/eladyaniv01/SC2MapAnalysis/commit/20b1e70693a3aef37eba068fb38965c82d076716))
657 |
658 | ### [0.0.24](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.23...v0.0.24) (2020-08-07)
659 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/MapAnalyzer/Debugger.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | import os
3 | import sys
4 | import warnings
5 | from typing import Any, Dict, Optional, Tuple, TYPE_CHECKING, Union
6 |
7 | import numpy as np
8 | from loguru import logger
9 | from numpy import ndarray
10 | from sc2 import BotAI
11 | from sc2.position import Point2, Point3
12 |
13 | from .constants import COLORS, LOG_FORMAT, LOG_MODULE
14 |
15 | if TYPE_CHECKING:
16 | from MapAnalyzer.MapData import MapData
17 |
18 |
19 | class LocalLogFilter:
20 | def __init__(self, module_name: str, level: str = "ERROR") -> None:
21 | self.module_name = module_name
22 | self.level = level
23 |
24 | def __call__(self, record: Dict[str, Any]) -> bool:
25 | levelno = logger.level(self.level).no
26 | if self.module_name.lower() in record["name"].lower():
27 | return record["level"].no >= levelno
28 | return False
29 |
30 |
31 | class LogFilter:
32 | def __init__(self, level: str = "ERROR") -> None:
33 | self.level = level
34 |
35 | def __call__(self, record: Dict[str, Any]) -> bool:
36 | levelno = logger.level(self.level).no
37 | if 'sc2.' not in record["name"].lower():
38 | return record["level"].no >= levelno
39 | return False
40 |
41 |
42 | class MapAnalyzerDebugger:
43 | """
44 | MapAnalyzerDebugger
45 | """
46 |
47 | def __init__(self, map_data: "MapData", loglevel: str = "ERROR") -> None:
48 | self.map_data = map_data
49 | self.warnings = warnings
50 | self.warnings.filterwarnings('ignore', category=DeprecationWarning)
51 | self.warnings.filterwarnings('ignore', category=RuntimeWarning)
52 | self.local_log_filter = LocalLogFilter(module_name=LOG_MODULE, level=loglevel)
53 | self.log_format = LOG_FORMAT
54 | self.log_filter = LogFilter(level=loglevel)
55 | logger.add(sys.stderr, format=self.log_format, filter=self.log_filter)
56 |
57 | @staticmethod
58 | def scatter(*args, **kwargs):
59 | import matplotlib.pyplot as plt
60 | plt.scatter(*args, **kwargs)
61 |
62 | @staticmethod
63 | def show():
64 | import matplotlib.pyplot as plt
65 | plt.show()
66 |
67 | @staticmethod
68 | def close():
69 | import matplotlib.pyplot as plt
70 | plt.close(fig='all')
71 |
72 | @staticmethod
73 | def save(filename: str) -> bool:
74 |
75 | for i in inspect.stack():
76 | if 'test_suite.py' in str(i):
77 | logger.info(f"Skipping save operation on test runs")
78 | logger.debug(f"index = {inspect.stack().index(i)} {i}")
79 | return True
80 | import matplotlib.pyplot as plt
81 | full_path = os.path.join(os.path.abspath("."), f"{filename}")
82 | plt.savefig(f"{filename}.png")
83 | logger.debug(f"Plot Saved to {full_path}")
84 |
85 | def plot_regions(self,
86 | fontdict: Dict[str, Union[str, int]]) -> None:
87 | """"""
88 | import matplotlib.pyplot as plt
89 | for lbl, reg in self.map_data.regions.items():
90 | c = COLORS[lbl]
91 | fontdict["color"] = 'black'
92 | fontdict["backgroundcolor"] = 'black'
93 | # if c == 'black':
94 | # fontdict["backgroundcolor"] = 'white'
95 | plt.text(
96 | reg.center[0],
97 | reg.center[1],
98 | reg.label,
99 | bbox=dict(fill=True, alpha=0.9, edgecolor=fontdict["backgroundcolor"], linewidth=2),
100 | fontdict=fontdict,
101 | )
102 | # random color for each perimeter
103 | x, y = zip(*reg.perimeter_points)
104 | plt.scatter(x, y, c=c, marker="1", s=300)
105 | for corner in reg.corner_points:
106 | plt.scatter(corner[0], corner[1], marker="v", c="red", s=150)
107 |
108 | def plot_vision_blockers(self) -> None:
109 | """
110 | plot vbs
111 | """
112 | import matplotlib.pyplot as plt
113 |
114 | for vb in self.map_data.vision_blockers:
115 | plt.text(vb[0], vb[1], "X")
116 |
117 | x, y = zip(*self.map_data.vision_blockers)
118 | plt.scatter(x, y, color="r")
119 |
120 | def plot_normal_resources(self) -> None:
121 | """
122 | # todo: account for gold minerals and rich gas
123 | """
124 | import matplotlib.pyplot as plt
125 | for mfield in self.map_data.mineral_fields:
126 | plt.scatter(mfield.position[0], mfield.position[1], color="blue")
127 | for gasgeyser in self.map_data.normal_geysers:
128 | plt.scatter(
129 | gasgeyser.position[0],
130 | gasgeyser.position[1],
131 | color="yellow",
132 | marker=r"$\spadesuit$",
133 | s=500,
134 | edgecolors="g",
135 | )
136 |
137 | def plot_chokes(self) -> None:
138 | """
139 | compute Chokes
140 | """
141 | import matplotlib.pyplot as plt
142 | for choke in self.map_data.map_chokes:
143 | x, y = zip(*choke.points)
144 | cm = choke.center
145 | if choke.is_ramp:
146 | fontdict = {"family": "serif", "weight": "bold", "size": 15}
147 | plt.text(cm[0], cm[1], f"R<{[r.label for r in choke.regions]}>", fontdict=fontdict,
148 | bbox=dict(fill=True, alpha=0.4, edgecolor="cyan", linewidth=8))
149 | plt.scatter(x, y, color="w")
150 | elif choke.is_vision_blocker:
151 |
152 | fontdict = {"family": "serif", "size": 10}
153 | plt.text(cm[0], cm[1], f"VB<>", fontdict=fontdict,
154 | bbox=dict(fill=True, alpha=0.3, edgecolor="red", linewidth=2))
155 | plt.scatter(x, y, marker=r"$\heartsuit$", s=100, edgecolors="b", alpha=0.3)
156 |
157 | else:
158 | fontdict = {"family": "serif", "size": 10}
159 | plt.text(cm[0], cm[1], f"C<{choke.id}>", fontdict=fontdict,
160 | bbox=dict(fill=True, alpha=0.3, edgecolor="red", linewidth=2))
161 | plt.scatter(x, y, marker=r"$\heartsuit$", s=100, edgecolors="r", alpha=0.3)
162 | walls = [choke.side_a, choke.side_b]
163 | x, y = zip(*walls)
164 | fontdict = {"family": "serif", "size": 5}
165 | if 'unregistered' not in str(choke.id).lower():
166 | plt.text(choke.side_a[0], choke.side_a[1], f"C<{choke.id}sA>", fontdict=fontdict,
167 | bbox=dict(fill=True, alpha=0.5, edgecolor="green", linewidth=2))
168 | plt.text(choke.side_b[0], choke.side_b[1], f"C<{choke.id}sB>", fontdict=fontdict,
169 | bbox=dict(fill=True, alpha=0.5, edgecolor="red", linewidth=2))
170 | else:
171 | plt.text(choke.side_a[0], choke.side_a[1], f"sA>", fontdict=fontdict,
172 | bbox=dict(fill=True, alpha=0.5, edgecolor="green", linewidth=2))
173 | plt.text(choke.side_b[0], choke.side_b[1], f"sB>", fontdict=fontdict,
174 | bbox=dict(fill=True, alpha=0.5, edgecolor="red", linewidth=2))
175 | plt.scatter(x, y, marker=r"$\spadesuit$", s=50, edgecolors="b", alpha=0.5)
176 |
177 | def plot_overlord_spots(self):
178 | import matplotlib.pyplot as plt
179 | for spot in self.map_data.overlord_spots:
180 | plt.scatter(spot[0], spot[1], marker="X", color="black")
181 |
182 | def plot_map(
183 | self, fontdict: dict = None, figsize: int = 20
184 | ) -> None:
185 | """
186 |
187 | Plot map
188 |
189 | """
190 |
191 | if not fontdict:
192 | fontdict = {"family": "serif", "weight": "bold", "size": 25}
193 | import matplotlib.pyplot as plt
194 | plt.figure(figsize=(figsize, figsize))
195 | self.plot_regions(fontdict=fontdict)
196 | # some maps has no vision blockers
197 | if len(self.map_data.vision_blockers) > 0:
198 | self.plot_vision_blockers()
199 | self.plot_normal_resources()
200 | self.plot_chokes()
201 | fontsize = 25
202 |
203 | plt.style.use("ggplot")
204 | plt.imshow(self.map_data.region_grid.astype(float), origin="lower")
205 | plt.imshow(self.map_data.terrain_height, alpha=1, origin="lower", cmap="terrain")
206 | x, y = zip(*self.map_data.nonpathable_indices_stacked)
207 | plt.scatter(x, y, color="grey")
208 | ax = plt.gca()
209 | for tick in ax.xaxis.get_major_ticks():
210 | tick.label1.set_fontsize(fontsize)
211 | tick.label1.set_fontweight("bold")
212 | for tick in ax.yaxis.get_major_ticks():
213 | tick.label1.set_fontsize(fontsize)
214 | tick.label1.set_fontweight("bold")
215 | plt.grid()
216 |
217 |
218 | def plot_influenced_path(self, start: Union[Tuple[float, float], Point2],
219 | goal: Union[Tuple[float, float], Point2],
220 | weight_array: ndarray,
221 | large: bool = False,
222 | smoothing: bool = False,
223 | name: Optional[str] = None,
224 | fontdict: dict = None) -> None:
225 | import matplotlib.pyplot as plt
226 | from mpl_toolkits.axes_grid1 import make_axes_locatable
227 | from matplotlib.cm import ScalarMappable
228 | if not fontdict:
229 | fontdict = {"family": "serif", "weight": "bold", "size": 20}
230 | plt.style.use(["ggplot", "bmh"])
231 | org = "lower"
232 | if name is None:
233 | name = self.map_data.map_name
234 | arr = weight_array.copy()
235 | path = self.map_data.pathfind(start, goal,
236 | grid=arr,
237 | large=large,
238 | smoothing=smoothing,
239 | sensitivity=1)
240 | ax: plt.Axes = plt.subplot(1, 1, 1)
241 | if path is not None:
242 | path = np.flipud(path) # for plot align
243 | logger.info("Found")
244 | x, y = zip(*path)
245 | ax.scatter(x, y, s=3, c='green')
246 | else:
247 | logger.info("Not Found")
248 |
249 | x, y = zip(*[start, goal])
250 | ax.scatter(x, y)
251 |
252 | influence_cmap = plt.cm.get_cmap("afmhot")
253 | ax.text(start[0], start[1], f"Start {start}")
254 | ax.text(goal[0], goal[1], f"Goal {goal}")
255 | ax.imshow(self.map_data.path_arr, alpha=0.5, origin=org)
256 | ax.imshow(self.map_data.terrain_height, alpha=0.5, origin=org, cmap='bone')
257 | arr = np.where(arr == np.inf, 0, arr).T
258 | ax.imshow(arr, origin=org, alpha=0.3, cmap=influence_cmap)
259 | divider = make_axes_locatable(ax)
260 | cax = divider.append_axes("right", size="5%", pad=0.05)
261 | sc = ScalarMappable(cmap=influence_cmap)
262 | sc.set_array(arr)
263 | sc.autoscale()
264 | cbar = plt.colorbar(sc, cax=cax)
265 | cbar.ax.set_ylabel('Pathing Cost', rotation=270, labelpad=25, fontdict=fontdict)
266 | plt.title(f"{name}", fontdict=fontdict, loc='right')
267 | plt.grid()
268 |
269 | def plot_influenced_path_nydus(self, start: Union[Tuple[float, float], Point2],
270 | goal: Union[Tuple[float, float], Point2],
271 | weight_array: ndarray,
272 | large: bool = False,
273 | smoothing: bool = False,
274 | name: Optional[str] = None,
275 | fontdict: dict = None) -> None:
276 | import matplotlib.pyplot as plt
277 | from mpl_toolkits.axes_grid1 import make_axes_locatable
278 | from matplotlib.cm import ScalarMappable
279 | if not fontdict:
280 | fontdict = {"family": "serif", "weight": "bold", "size": 20}
281 | plt.style.use(["ggplot", "bmh"])
282 | org = "lower"
283 | if name is None:
284 | name = self.map_data.map_name
285 | arr = weight_array.copy()
286 | paths = self.map_data.pathfind_with_nyduses(start, goal,
287 | grid=arr,
288 | large=large,
289 | smoothing=smoothing,
290 | sensitivity=1)
291 | ax: plt.Axes = plt.subplot(1, 1, 1)
292 | if paths is not None:
293 | for i in range(len(paths[0])):
294 | path = np.flipud(paths[0][i]) # for plot align
295 | logger.info("Found")
296 | x, y = zip(*path)
297 | ax.scatter(x, y, s=3, c='green')
298 | else:
299 | logger.info("Not Found")
300 |
301 | x, y = zip(*[start, goal])
302 | ax.scatter(x, y)
303 |
304 | influence_cmap = plt.cm.get_cmap("afmhot")
305 | ax.text(start[0], start[1], f"Start {start}")
306 | ax.text(goal[0], goal[1], f"Goal {goal}")
307 | ax.imshow(self.map_data.path_arr, alpha=0.5, origin=org)
308 | ax.imshow(self.map_data.terrain_height, alpha=0.5, origin=org, cmap='bone')
309 | arr = np.where(arr == np.inf, 0, arr).T
310 | ax.imshow(arr, origin=org, alpha=0.3, cmap=influence_cmap)
311 | divider = make_axes_locatable(ax)
312 | cax = divider.append_axes("right", size="5%", pad=0.05)
313 | sc = ScalarMappable(cmap=influence_cmap)
314 | sc.set_array(arr)
315 | sc.autoscale()
316 | cbar = plt.colorbar(sc, cax=cax)
317 | cbar.ax.set_ylabel('Pathing Cost', rotation=270, labelpad=25, fontdict=fontdict)
318 | plt.title(f"{name}", fontdict=fontdict, loc='right')
319 | plt.grid()
320 |
321 | @staticmethod
322 | def draw_influence_in_game(bot: BotAI,
323 | grid: np.ndarray,
324 | lower_threshold: int,
325 | upper_threshold: int,
326 | color: Tuple[int, int, int],
327 | size: int) -> None:
328 | height: float = bot.get_terrain_z_height(bot.start_location)
329 | for x, y in zip(*np.where((grid > lower_threshold) & (grid < upper_threshold))):
330 | pos: Point3 = Point3((x, y, height))
331 | if grid[x, y] == np.inf:
332 | val: int = 9999
333 | else:
334 | val: int = int(grid[x, y])
335 | bot.client.debug_text_world(str(val), pos, color, size)
336 |
--------------------------------------------------------------------------------
/MapAnalyzer/Pather.py:
--------------------------------------------------------------------------------
1 | from typing import List, Optional, Tuple, TYPE_CHECKING
2 |
3 | import numpy as np
4 |
5 | from loguru import logger
6 | from numpy import ndarray
7 | from sc2.ids.unit_typeid import UnitTypeId as UnitID
8 | from sc2.position import Point2
9 |
10 | from MapAnalyzer.exceptions import OutOfBoundsException, PatherNoPointsException
11 | from MapAnalyzer.Region import Region
12 | from MapAnalyzer.utils import change_destructable_status_in_grid
13 | from .cext import astar_path, astar_path_with_nyduses
14 | from .destructibles import *
15 |
16 | if TYPE_CHECKING:
17 | from MapAnalyzer.MapData import MapData
18 |
19 |
20 | def _bounded_circle(center, radius, shape):
21 | xx, yy = np.ogrid[:shape[0], :shape[1]]
22 | circle = (xx - center[0]) ** 2 + (yy - center[1]) ** 2
23 | return np.nonzero(circle <= radius ** 2)
24 |
25 |
26 | def draw_circle(c, radius, shape=None):
27 | center = np.array(c)
28 | upper_left = np.ceil(center - radius).astype(int)
29 | lower_right = np.floor(center + radius).astype(int) + 1
30 |
31 | if shape is not None:
32 | # Constrain upper_left and lower_right by shape boundary.
33 | upper_left = np.maximum(upper_left, 0)
34 | lower_right = np.minimum(lower_right, np.array(shape))
35 |
36 | shifted_center = center - upper_left
37 | bounding_shape = lower_right - upper_left
38 |
39 | rr, cc = _bounded_circle(shifted_center, radius, bounding_shape)
40 | return rr + upper_left[0], cc + upper_left[1]
41 |
42 |
43 | class MapAnalyzerPather:
44 | """"""
45 |
46 | def __init__(self, map_data: "MapData") -> None:
47 | self.map_data = map_data
48 |
49 | nonpathable_indices = np.where(self.map_data.bot.game_info.pathing_grid.data_numpy == 0)
50 | self.nonpathable_indices_stacked = np.column_stack(
51 | (nonpathable_indices[1], nonpathable_indices[0])
52 | )
53 | self.connectivity_graph = None # set later by MapData
54 |
55 | self._set_default_grids()
56 | self.terrain_height = self.map_data.terrain_height.copy().T
57 |
58 | def _set_default_grids(self):
59 | # need to consider the placement arr because our base minerals, geysers and townhall
60 | # are not pathable in the pathing grid
61 | # we manage those manually so they are accurate through the game
62 | self.default_grid = np.fmax(self.map_data.path_arr, self.map_data.placement_arr).T
63 |
64 | # Fixing platforms on Submarine which reapers can climb onto not being pathable
65 | # Don't use the entire name because we also use the modified maps
66 | # with different names
67 | if "Submarine" in self.map_data.map_name:
68 | self.default_grid[116, 43] = 1
69 | self.default_grid[51, 120] = 1
70 |
71 | self.default_grid_nodestr = self.default_grid.copy()
72 |
73 | self.destructables_included = {}
74 | self.minerals_included = {}
75 |
76 | # set rocks and mineral walls to pathable in the beginning
77 | # these will be set nonpathable when updating grids for the destructables
78 | # that still exist
79 | for dest in self.map_data.bot.destructables:
80 | self.destructables_included[dest.position] = dest
81 | if "unbuildable" not in dest.name.lower() and "acceleration" not in dest.name.lower():
82 | change_destructable_status_in_grid(self.default_grid, dest, 0)
83 | change_destructable_status_in_grid(self.default_grid_nodestr, dest, 1)
84 |
85 | # set each geyser as non pathable, these don't update during the game
86 | for geyser in self.map_data.bot.vespene_geyser:
87 | left_bottom = geyser.position.offset((-1.5, -1.5))
88 | x_start = int(left_bottom[0])
89 | y_start = int(left_bottom[1])
90 | x_end = int(x_start + 3)
91 | y_end = int(y_start + 3)
92 | self.default_grid[x_start:x_end, y_start:y_end] = 0
93 | self.default_grid_nodestr[x_start:x_end, y_start:y_end] = 0
94 |
95 | for mineral in self.map_data.bot.mineral_field:
96 | self.minerals_included[mineral.position] = mineral
97 | x1 = int(mineral.position[0])
98 | x2 = x1 - 1
99 | y = int(mineral.position[1])
100 |
101 | self.default_grid[x1, y] = 0
102 | self.default_grid[x2, y] = 0
103 | self.default_grid_nodestr[x1, y] = 0
104 | self.default_grid_nodestr[x2, y] = 0
105 |
106 | def set_connectivity_graph(self):
107 | connectivity_graph = {}
108 | for region in self.map_data.regions.values():
109 | if connectivity_graph.get(region) is None:
110 | connectivity_graph[region] = []
111 | for connected_region in region.connected_regions:
112 | if connected_region not in connectivity_graph.get(region):
113 | connectivity_graph[region].append(connected_region)
114 | self.connectivity_graph = connectivity_graph
115 |
116 | def find_all_paths(self, start: Region, goal: Region, path: Optional[List[Region]] = None) -> List[List[Region]]:
117 | if path is None:
118 | path = []
119 | graph = self.connectivity_graph
120 | path = path + [start]
121 | if start == goal:
122 | return [path]
123 | if start not in graph:
124 | return []
125 | paths = []
126 | for node in graph[start]:
127 | if node not in path:
128 | newpaths = self.find_all_paths(node, goal, path)
129 | for newpath in newpaths:
130 | paths.append(newpath)
131 | return paths
132 |
133 | def _add_non_pathables_ground(self, grid: ndarray, include_destructables: bool = True) -> ndarray:
134 | ret_grid = grid.copy()
135 | nonpathables = self.map_data.bot.structures.not_flying
136 | nonpathables.extend(self.map_data.bot.enemy_structures.not_flying)
137 | nonpathables = nonpathables.filter(
138 | lambda x: (x.type_id != UnitID.SUPPLYDEPOTLOWERED or x.is_active)
139 | and (x.type_id != UnitID.CREEPTUMOR or not x.is_ready))
140 |
141 | for obj in nonpathables:
142 | size = 1
143 | if obj.type_id in buildings_2x2:
144 | size = 2
145 | elif obj.type_id in buildings_3x3:
146 | size = 3
147 | elif obj.type_id in buildings_5x5:
148 | size = 5
149 | left_bottom = obj.position.offset((-size / 2, -size / 2))
150 | x_start = int(left_bottom[0])
151 | y_start = int(left_bottom[1])
152 | x_end = int(x_start + size)
153 | y_end = int(y_start + size)
154 |
155 | ret_grid[x_start:x_end, y_start:y_end] = 0
156 |
157 | # townhall sized buildings should have their corner spots pathable
158 | if size == 5:
159 | ret_grid[x_start, y_start] = 1
160 | ret_grid[x_start, y_end - 1] = 1
161 | ret_grid[x_end - 1, y_start] = 1
162 | ret_grid[x_end - 1, y_end - 1] = 1
163 |
164 | if len(self.minerals_included) != self.map_data.bot.mineral_field.amount:
165 |
166 | new_positions = set(m.position for m in self.map_data.bot.mineral_field)
167 | old_mf_positions = set(self.minerals_included)
168 |
169 | missing_positions = old_mf_positions - new_positions
170 | for mf_position in missing_positions:
171 | x1 = int(mf_position[0])
172 | x2 = x1 - 1
173 | y = int(mf_position[1])
174 |
175 | ret_grid[x1, y] = 1
176 | ret_grid[x2, y] = 1
177 |
178 | self.default_grid[x1, y] = 1
179 | self.default_grid[x2, y] = 1
180 |
181 | self.default_grid_nodestr[x1, y] = 1
182 | self.default_grid_nodestr[x2, y] = 1
183 |
184 | del self.minerals_included[mf_position]
185 |
186 | if include_destructables and len(self.destructables_included) != self.map_data.bot.destructables.amount:
187 | new_positions = set(d.position for d in self.map_data.bot.destructables)
188 | old_dest_positions = set(self.destructables_included)
189 | missing_positions = old_dest_positions - new_positions
190 |
191 | for dest_position in missing_positions:
192 | dest = self.destructables_included[dest_position]
193 | change_destructable_status_in_grid(ret_grid, dest, 1)
194 | change_destructable_status_in_grid(self.default_grid, dest, 1)
195 |
196 | del self.destructables_included[dest_position]
197 |
198 | return ret_grid
199 |
200 | def find_eligible_point(self, point: Tuple[float, float], grid: np.ndarray, terrain_height: np.ndarray, max_distance: float) -> Optional[Tuple[int, int]]:
201 | """
202 | User may give a point that is in a nonpathable grid cell, for example inside a building or
203 | inside rocks. The desired behavior is to move the point a bit so for example we can start or finish
204 | next to the building the original point was inside of.
205 | To make sure that we don't accidentally for example offer a point that is on low ground when the
206 | first target was on high ground, we first try to find a point that maintains the terrain height.
207 | After that we check for points on other terrain heights.
208 | """
209 | point = (int(point[0]), int(point[1]))
210 |
211 | if grid[point] == np.inf:
212 | target_height = terrain_height[point]
213 | disk = tuple(draw_circle(point, max_distance, shape=grid.shape))
214 | # Using 8 for the threshold in case there is some variance on the same level
215 | # Proper levels have a height difference of 16
216 | same_height_cond = np.logical_and(np.abs(terrain_height[disk] - target_height) < 8, grid[disk] < np.inf)
217 |
218 | if np.any(same_height_cond):
219 | possible_points = np.column_stack((disk[0][same_height_cond], disk[1][same_height_cond]))
220 | closest_point_index = np.argmin(np.sum((possible_points - point) ** 2, axis=1))
221 | return tuple(possible_points[closest_point_index])
222 | else:
223 | diff_height_cond = grid[disk] < np.inf
224 | if np.any(diff_height_cond):
225 | possible_points = np.column_stack((disk[0][diff_height_cond], disk[1][diff_height_cond]))
226 | closest_point_index = np.argmin(np.sum((possible_points - point) ** 2, axis=1))
227 | return tuple(possible_points[closest_point_index])
228 | else:
229 | return None
230 | return point
231 |
232 | def lowest_cost_points_array(self, from_pos: tuple, radius: float, grid: np.ndarray) -> Optional[np.ndarray]:
233 | """For use with evaluations that use numpy arrays
234 | example: # Closest point to unit furthest from target
235 | distances = cdist([[unitpos, targetpos]], lowest_points, "sqeuclidean")
236 | lowest_points[(distances[0] - distances[1]).argmin()]
237 | - 140 µs per loop
238 | """
239 |
240 | disk = tuple(draw_circle(from_pos, radius, shape=grid.shape))
241 | if len(disk[0]) == 0:
242 | return None
243 |
244 | arrmin = np.min(grid[disk])
245 | cond = grid[disk] == arrmin
246 | return np.column_stack((disk[0][cond], disk[1][cond]))
247 |
248 | def find_lowest_cost_points(self, from_pos: Point2, radius: float, grid: np.ndarray) -> Optional[List[Point2]]:
249 | lowest = self.lowest_cost_points_array(from_pos, radius, grid)
250 |
251 | if lowest is None:
252 | return None
253 |
254 | return list(map(Point2, lowest))
255 |
256 | def get_base_pathing_grid(self, include_destructables: bool = True) -> ndarray:
257 | if include_destructables:
258 | return self.default_grid.copy()
259 | else:
260 | return self.default_grid_nodestr.copy()
261 |
262 | def get_climber_grid(self, default_weight: float = 1, include_destructables: bool = True) -> ndarray:
263 | """Grid for units like reaper / colossus """
264 | grid = self.get_base_pathing_grid(include_destructables)
265 | grid = np.where(self.map_data.c_ext_map.climber_grid != 0, 1, grid)
266 | grid = self._add_non_pathables_ground(grid=grid, include_destructables=include_destructables)
267 | grid = np.where(grid != 0, default_weight, np.inf).astype(np.float32)
268 | return grid
269 |
270 | def get_clean_air_grid(self, default_weight: float = 1) -> ndarray:
271 | clean_air_grid = np.zeros(shape=self.default_grid.shape).astype(np.float32)
272 | area = self.map_data.bot.game_info.playable_area
273 | clean_air_grid[area.x:(area.x + area.width), area.y:(area.y + area.height)] = 1
274 | return np.where(clean_air_grid == 1, default_weight, np.inf).astype(np.float32)
275 |
276 | def get_air_vs_ground_grid(self, default_weight: float) -> ndarray:
277 | grid = self.get_pyastar_grid(default_weight=default_weight, include_destructables=True)
278 | # set non pathable points inside map bounds to value 1
279 | area = self.map_data.bot.game_info.playable_area
280 | start_x = area.x
281 | end_x = area.x + area.width
282 | start_y = area.y
283 | end_y = area.y + area.height
284 | grid[start_x:end_x, start_y:end_y] = np.where(grid[start_x:end_x, start_y:end_y] == np.inf, 1, default_weight)
285 |
286 | return grid
287 |
288 | def get_pyastar_grid(self, default_weight: float = 1, include_destructables: bool = True) -> ndarray:
289 | grid = self.get_base_pathing_grid(include_destructables)
290 | grid = self._add_non_pathables_ground(grid=grid, include_destructables=include_destructables)
291 |
292 | grid = np.where(grid != 0, default_weight, np.inf).astype(np.float32)
293 | return grid
294 |
295 | def pathfind(self, start: Tuple[float, float], goal: Tuple[float, float], grid: Optional[ndarray] = None,
296 | large: bool = False,
297 | smoothing: bool = False,
298 | sensitivity: int = 1) -> Optional[List[Point2]]:
299 | if grid is None:
300 | logger.warning("Using the default pyastar grid as no grid was provided.")
301 | grid = self.get_pyastar_grid()
302 |
303 | if start is not None and goal is not None:
304 | start = round(start[0]), round(start[1])
305 | start = self.find_eligible_point(start, grid, self.terrain_height, 10)
306 | goal = round(goal[0]), round(goal[1])
307 | goal = self.find_eligible_point(goal, grid, self.terrain_height, 10)
308 | else:
309 | logger.warning(PatherNoPointsException(start=start, goal=goal))
310 | return None
311 |
312 | # find_eligible_point didn't find any pathable nodes nearby
313 | if start is None or goal is None:
314 | return None
315 |
316 | path = astar_path(grid, start, goal, large, smoothing)
317 |
318 | if path is not None:
319 | # Remove the starting point from the path.
320 | # Make sure the goal node is the last node even if we are
321 | # skipping points
322 | complete_path = list(map(Point2, path))
323 | skipped_path = complete_path[0:-1:sensitivity]
324 | if skipped_path:
325 | skipped_path.pop(0)
326 |
327 | skipped_path.append(complete_path[-1])
328 |
329 | return skipped_path
330 | else:
331 | logger.debug(f"No Path found s{start}, g{goal}")
332 | return None
333 |
334 | def pathfind_with_nyduses(self, start: Tuple[float, float], goal: Tuple[float, float],
335 | grid: Optional[ndarray] = None,
336 | large: bool = False,
337 | smoothing: bool = False,
338 | sensitivity: int = 1) -> Optional[Tuple[List[List[Point2]], Optional[List[int]]]]:
339 | if grid is None:
340 | logger.warning("Using the default pyastar grid as no grid was provided.")
341 | grid = self.get_pyastar_grid()
342 |
343 | if start is not None and goal is not None:
344 | start = round(start[0]), round(start[1])
345 | start = self.find_eligible_point(start, grid, self.terrain_height, 10)
346 | goal = round(goal[0]), round(goal[1])
347 | goal = self.find_eligible_point(goal, grid, self.terrain_height, 10)
348 | else:
349 | logger.warning(PatherNoPointsException(start=start, goal=goal))
350 | return None
351 |
352 | # find_eligible_point didn't find any pathable nodes nearby
353 | if start is None or goal is None:
354 | return None
355 |
356 | nydus_units = self.map_data.bot.structures.of_type([UnitTypeId.NYDUSNETWORK, UnitTypeId.NYDUSCANAL]).ready
357 | nydus_positions = [nydus.position for nydus in nydus_units]
358 |
359 | paths = astar_path_with_nyduses(grid, start, goal,
360 | nydus_positions,
361 | large, smoothing)
362 | if paths is not None:
363 | returned_path = []
364 | nydus_tags = None
365 | if len(paths) == 1:
366 | path = list(map(Point2, paths[0]))
367 | skipped_path = path[0:-1:sensitivity]
368 | if skipped_path:
369 | skipped_path.pop(0)
370 | skipped_path.append(path[-1])
371 | returned_path.append(skipped_path)
372 | else:
373 | first_path = list(map(Point2, paths[0]))
374 | first_skipped_path = first_path[0:-1:sensitivity]
375 | if first_skipped_path:
376 | first_skipped_path.pop(0)
377 | first_skipped_path.append(first_path[-1])
378 | returned_path.append(first_skipped_path)
379 |
380 | enter_nydus_unit = nydus_units.filter(lambda x: x.position.rounded == first_path[-1]).first
381 | enter_nydus_tag = enter_nydus_unit.tag
382 |
383 | second_path = list(map(Point2, paths[1]))
384 | exit_nydus_unit = nydus_units.filter(lambda x: x.position.rounded == second_path[0]).first
385 | exit_nydus_tag = exit_nydus_unit.tag
386 | nydus_tags = [enter_nydus_tag, exit_nydus_tag]
387 |
388 | second_skipped_path = second_path[0:-1:sensitivity]
389 | second_skipped_path.append(second_path[-1])
390 | returned_path.append(second_skipped_path)
391 |
392 | return returned_path, nydus_tags
393 | else:
394 | logger.debug(f"No Path found s{start}, g{goal}")
395 | return None
396 |
397 | def add_cost(
398 | self,
399 | position: Tuple[float, float],
400 | radius: float,
401 | arr: ndarray,
402 | weight: float = 100,
403 | safe: bool = True,
404 | initial_default_weights: float = 0,
405 | ) -> ndarray:
406 | disk = tuple(draw_circle(position, radius, arr.shape))
407 |
408 | arr: ndarray = self._add_disk_to_grid(
409 | position, arr, disk, weight, safe, initial_default_weights
410 | )
411 |
412 | return arr
413 |
414 | def add_cost_to_multiple_grids(
415 | self,
416 | position: Tuple[float, float],
417 | radius: float,
418 | arrays: List[ndarray],
419 | weight: float = 100,
420 | safe: bool = True,
421 | initial_default_weights: float = 0,
422 | ) -> List[ndarray]:
423 | """
424 | Add the same cost to multiple grids, this is so the disk is only calculated once
425 | """
426 | disk = tuple(draw_circle(position, radius, arrays[0].shape))
427 |
428 | for i, arr in enumerate(arrays):
429 | arrays[i] = self._add_disk_to_grid(
430 | position, arrays[i], disk, weight, safe, initial_default_weights
431 | )
432 |
433 | return arrays
434 |
435 | @staticmethod
436 | def _add_disk_to_grid(
437 | position: Tuple[float, float],
438 | arr: ndarray,
439 | disk: Tuple,
440 | weight: float = 100,
441 | safe: bool = True,
442 | initial_default_weights: float = 0,
443 | ) -> ndarray:
444 | # if we don't touch any cell origins due to a small radius, add at least the cell
445 | # the given position is in
446 | if (
447 | len(disk[0]) == 0
448 | and 0 <= position[0] < arr.shape[0]
449 | and 0 <= position[1] < arr.shape[1]
450 | ):
451 | disk = (int(position[0]), int(position[1]))
452 |
453 | if initial_default_weights > 0:
454 | arr[disk] = np.where(arr[disk] == 1, initial_default_weights, arr[disk])
455 |
456 | arr[disk] += weight
457 | if safe and np.any(arr[disk] < 1):
458 | logger.warning(
459 | "You are attempting to set weights that are below 1. falling back to the minimum (1)"
460 | )
461 | arr[disk] = np.where(arr[disk] < 1, 1, arr[disk])
462 |
463 | return arr
464 |
--------------------------------------------------------------------------------
/MapAnalyzer/Polygon.py:
--------------------------------------------------------------------------------
1 | from functools import lru_cache
2 | from typing import List, Set, Tuple, TYPE_CHECKING, Union
3 |
4 | import numpy as np
5 | from loguru import logger
6 | from numpy import int64, ndarray
7 | from sc2.position import Point2
8 | from scipy.ndimage import center_of_mass
9 |
10 | if TYPE_CHECKING:
11 | from MapAnalyzer import MapData, Region
12 |
13 |
14 | class Buildables:
15 | """
16 |
17 | Represents the Buildable Points in a :class:`.Polygon`,
18 |
19 | "Lazy" class that will only update information when it is needed
20 |
21 | Tip:
22 | :class:`.BuildablePoints` that belong to a :class:`.ChokeArea`
23 |
24 | are always the edges, this is useful for walling off
25 |
26 | """
27 |
28 | def __init__(self, polygon):
29 | self.polygon = polygon
30 | self.points = None
31 |
32 | @property
33 | def free_pct(self) -> float:
34 | """
35 |
36 | A simple method for knowing what % of the points is left available out of the total
37 |
38 | """
39 | if self.points is None:
40 | logger.warning("BuildablePoints needs to update first")
41 | self.update()
42 | return len(self.points) / len(self.polygon.points)
43 |
44 | def update(self) -> None:
45 | """
46 |
47 | To be called only by :class:`.Polygon`, this ensures that updates are done in a lazy fashion,
48 |
49 | the update is evaluated only when there is need for the information, otherwise it is ignored
50 |
51 | """
52 | parr = self.polygon.map_data.points_to_numpy_array(self.polygon.points)
53 | # passing safe false to reduce the warnings,
54 | # which are irrelevant in this case
55 | [self.polygon.map_data.add_cost(position=(unit.position.x, unit.position.y), radius=unit.radius * 0.9,
56 | grid=parr,
57 | safe=False)
58 | for unit in
59 | self.polygon.map_data.bot.all_units.not_flying]
60 | buildable_indices = np.where(parr == 1)
61 | buildable_points = []
62 | _points = list(self.polygon.map_data.indices_to_points(buildable_indices))
63 | placement_grid = self.polygon.map_data.placement_arr.T
64 | for p in _points:
65 | if p[0] < placement_grid.shape[0] and p[1] < placement_grid.shape[1]:
66 | if placement_grid[p] == 1:
67 | buildable_points.append(p)
68 | self.points = list(map(Point2, buildable_points))
69 |
70 |
71 | class Polygon:
72 | """
73 |
74 | Base Class for Representing an "Area"
75 |
76 | """
77 |
78 | # noinspection PyProtectedMember
79 | def __init__(self, map_data: "MapData", array: ndarray) -> None: # pragma: no cover
80 | self.map_data = map_data
81 | self.array = array
82 | self.indices = np.where(self.array == 1)
83 | self._clean_points = self.map_data.indices_to_points(self.indices)
84 | self.points = set([Point2(p) for p in
85 | self._clean_points]) # this is to serve data for map data compile, the accurate
86 | # calculation will be done on _set_points
87 | self._set_points()
88 | self.id = None # TODO
89 | self.is_choke = False
90 | self.is_ramp = False
91 | self.is_vision_blocker = False
92 | self.is_region = False
93 | self.areas = [] # set by map_data / Region
94 | self.map_data.polygons.append(self)
95 | self._buildables = Buildables(polygon=self)
96 |
97 | @property
98 | def top(self):
99 | return max(self.points, key=lambda x: (x[1], 0))
100 |
101 | @property
102 | def bottom(self):
103 | return min(self.points, key=lambda x: (x[1], 0))
104 |
105 | @property
106 | def right(self):
107 | return max(self.points, key=lambda x: (x[0], 0))
108 |
109 | @property
110 | def left(self):
111 | return min(self.points, key=lambda x: (x[0], 0))
112 |
113 | def _set_points(self):
114 |
115 | points = [p for p in self._clean_points]
116 | points.extend(self.corner_points)
117 | points.extend(self.perimeter_points)
118 | self.points = set([Point2((int(p[0]), int(p[1]))) for p in points])
119 | self.indices = self.map_data.points_to_indices(self.points)
120 |
121 | @property
122 | def buildables(self) -> Buildables:
123 | """
124 |
125 | :rtype: :class:`.BuildablePoints`
126 |
127 | Is a responsible for holding and updating the buildable points of it's respected :class:`.Polygon`
128 |
129 | """
130 | self._buildables.update()
131 | return self._buildables
132 |
133 | @property
134 | def regions(self) -> List["Region"]:
135 | """
136 |
137 | :rtype: List[:class:`.Region`]
138 |
139 | Filters out every Polygon that is not a region, and is inside / bordering with ``self``
140 |
141 | """
142 | from MapAnalyzer.Region import Region
143 | if len(self.areas) > 0:
144 | return [r for r in self.areas if isinstance(r, Region)]
145 | return []
146 |
147 | def calc_areas(self) -> None:
148 | # This is called by MapData, at a specific point in the sequence of compiling the map
149 | # this method uses where_all which means
150 | # it should be called at the end of the map compilation when areas are populated
151 | points = self.perimeter_points
152 | areas = self.areas
153 | for point in points:
154 | point = int(point[0]), int(point[1])
155 | new_areas = self.map_data.where_all(point)
156 | if self in new_areas:
157 | new_areas.pop(new_areas.index(self))
158 | areas.extend(new_areas)
159 | self.areas = list(set(areas))
160 |
161 | def plot(self, testing: bool = False) -> None: # pragma: no cover
162 | """
163 |
164 | plot
165 |
166 | """
167 | import matplotlib.pyplot as plt
168 | plt.style.use("ggplot")
169 |
170 | plt.imshow(self.array, origin="lower")
171 | if testing:
172 | return
173 | plt.show()
174 |
175 | @property
176 | @lru_cache()
177 | def nodes(self) -> List[Point2]:
178 | """
179 |
180 | List of :class:`.Point2`
181 |
182 | """
183 | return [p for p in self.points]
184 |
185 | @property
186 | @lru_cache()
187 | def corner_array(self) -> ndarray:
188 | """
189 |
190 | :rtype: :class:`.ndarray`
191 |
192 | """
193 |
194 | from skimage.feature import corner_harris, corner_peaks
195 |
196 | array = corner_peaks(
197 | corner_harris(self.array), min_distance=self.map_data.corner_distance, threshold_rel=0.01)
198 | return array
199 |
200 | @property
201 | @lru_cache()
202 | def width(self) -> float:
203 | """
204 |
205 | Lazy width calculation, will be approx 0.5 < x < 1.5 of real width
206 |
207 | """
208 | pl = list(self.perimeter_points)
209 | s1 = min(pl)
210 | s2 = max(pl)
211 | x1, y1 = s1[0], s1[1]
212 | x2, y2 = s2[0], s2[1]
213 | return np.math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
214 |
215 | @property
216 | @lru_cache()
217 | def corner_points(self) -> List[Point2]:
218 | """
219 |
220 | :rtype: List[:class:`.Point2`]
221 |
222 | """
223 | points = [Point2((int(p[0]), int(p[1]))) for p in self.corner_array if self.is_inside_point(Point2(p))]
224 | return points
225 |
226 | @property
227 | def clean_points(self) -> List[Tuple[int64, int64]]:
228 | # For internal usage
229 |
230 | return list(self._clean_points) # needs to be array-like for numpy calculations
231 |
232 | @property
233 | def center(self) -> Point2:
234 | """
235 |
236 | Since the center is always going to be a ``float``,
237 |
238 | and for performance considerations we use integer coordinates.
239 |
240 | We will return the closest point registered
241 |
242 | """
243 |
244 | cm = self.map_data.closest_towards_point(points=self.clean_points, target=center_of_mass(self.array))
245 | return cm
246 |
247 | @lru_cache()
248 | def is_inside_point(self, point: Union[Point2, tuple]) -> bool:
249 | """
250 |
251 | Query via Set(Point2) ''fast''
252 |
253 | """
254 | if isinstance(point, Point2):
255 | point = point.rounded
256 | if point in self.points:
257 | return True
258 | return False
259 |
260 | @lru_cache()
261 | def is_inside_indices(
262 | self, point: Union[Point2, tuple]
263 | ) -> bool: # pragma: no cover
264 | """
265 |
266 | Query via 2d np.array ''slower''
267 |
268 | """
269 | if isinstance(point, Point2):
270 | point = point.rounded
271 | return point[0] in self.indices[0] and point[1] in self.indices[1]
272 |
273 | @property
274 | def perimeter(self) -> np.ndarray:
275 | """
276 |
277 | The perimeter is interpolated between inner and outer cell-types using broadcasting
278 |
279 | """
280 | isolated_region = self.array
281 | xx, yy = np.gradient(isolated_region)
282 | edge_indices = np.argwhere(xx ** 2 + yy ** 2 > 0.1)
283 | return edge_indices
284 |
285 | @property
286 | def perimeter_points(self) -> Set[Tuple[int64, int64]]:
287 | """
288 |
289 | Useful method for getting perimeter points
290 |
291 | """
292 | li = [Point2((int(p[0]), int(p[1]))) for p in self.perimeter]
293 | return set(li)
294 |
295 | @property
296 | def area(self) -> int:
297 | """
298 |
299 | Sum of all points
300 |
301 | """
302 | return len(self.points)
303 |
304 | def __repr__(self) -> str:
305 | return f""
306 |
--------------------------------------------------------------------------------
/MapAnalyzer/Region.py:
--------------------------------------------------------------------------------
1 | from functools import lru_cache
2 |
3 | from typing import List, TYPE_CHECKING
4 |
5 | import numpy as np
6 | from sc2.position import Point2
7 |
8 | from MapAnalyzer.Polygon import Polygon
9 | from MapAnalyzer.constructs import MDRamp, ChokeArea
10 |
11 | if TYPE_CHECKING:
12 | from MapAnalyzer import MapData
13 |
14 |
15 | class Region(Polygon):
16 | """
17 | Higher order "Area" , all of the maps can be summed up by it's :class:`.Region`
18 |
19 | Tip:
20 | A :class:`.Region` may contain other :class:`.Polygon` inside it,
21 |
22 | Such as :class:`.ChokeArea` and :class:`.MDRamp`.
23 |
24 | But it will never share a point with another :class:`.Region`
25 |
26 | """
27 | def __init__(
28 | self,
29 | map_data: 'MapData',
30 | array: np.ndarray,
31 | label: int,
32 | map_expansions: List[Point2],
33 | ) -> None:
34 | super().__init__(map_data=map_data, array=array)
35 | self.label = label
36 | self.is_region = True
37 | self.bases = [
38 | base
39 | for base in map_expansions
40 | if self.is_inside_point((base.rounded[0], base.rounded[1]))
41 | ] # will be set later by mapdata
42 | self.region_vision_blockers = [] # will be set later by mapdata
43 | self.region_vb = []
44 |
45 |
46 | @property
47 | def region_ramps(self) -> List[MDRamp]:
48 | """
49 |
50 | Property access to :class:`.MDRamp` of this region
51 |
52 | """
53 | return [r for r in self.areas if r.is_ramp]
54 |
55 | @property
56 | def region_chokes(self) -> List[ChokeArea]:
57 | """
58 |
59 | Property access to :class:`.ChokeArea` of this region
60 |
61 | """
62 | return [r for r in self.areas if r.is_choke]
63 |
64 | @property
65 | @lru_cache()
66 | def connected_regions(self):
67 | """
68 |
69 | Provides a list of :class:`.Region` that are connected by chokes to ``self``
70 |
71 | """
72 | connected_regions = []
73 | for choke in self.region_chokes:
74 | for region in choke.regions:
75 | if region is not self and region not in connected_regions:
76 | connected_regions.append(region)
77 | return connected_regions
78 |
79 | def plot_perimeter(self, self_only: bool = True) -> None:
80 | """
81 |
82 | Debug Method plot_perimeter
83 |
84 | """
85 | import matplotlib.pyplot as plt
86 |
87 | plt.style.use("ggplot")
88 |
89 | x, y = zip(*self.perimeter)
90 | plt.scatter(x, y)
91 | plt.title(f"Region {self.label}")
92 | if self_only: # pragma: no cover
93 | plt.grid()
94 |
95 | def _plot_corners(self) -> None:
96 | import matplotlib.pyplot as plt
97 | plt.style.use("ggplot")
98 | for corner in self.corner_points:
99 | plt.scatter(corner[0], corner[1], marker="v", c="red", s=150)
100 |
101 | def _plot_ramps(self) -> None:
102 | import matplotlib.pyplot as plt
103 | plt.style.use("ggplot")
104 | for ramp in self.region_ramps:
105 | plt.text(
106 | # fixme make ramp attr compatible and not reversed
107 | ramp.top_center[0],
108 | ramp.top_center[1],
109 | f"R<{[r.label for r in ramp.regions]}>",
110 | bbox=dict(fill=True, alpha=0.3, edgecolor="cyan", linewidth=8),
111 | )
112 | # ramp.plot(testing=True)
113 | x, y = zip(*ramp.points)
114 | plt.scatter(x, y, color="w")
115 |
116 | def _plot_vision_blockers(self) -> None:
117 | import matplotlib.pyplot as plt
118 |
119 | plt.style.use("ggplot")
120 | for vb in self.map_data.vision_blockers:
121 | if self.is_inside_point(point=vb):
122 | plt.text(vb[0], vb[1], "X", c="r")
123 |
124 | def _plot_minerals(self) -> None:
125 | import matplotlib.pyplot as plt
126 |
127 | plt.style.use("ggplot")
128 | for mineral_field in self.map_data.mineral_fields:
129 | if self.is_inside_point(mineral_field.position.rounded):
130 | plt.scatter(
131 | mineral_field.position[0], mineral_field.position[1], color="blue"
132 | )
133 |
134 | def _plot_geysers(self) -> None:
135 | import matplotlib.pyplot as plt
136 |
137 | plt.style.use("ggplot")
138 | for gasgeyser in self.map_data.normal_geysers:
139 | if self.is_inside_point(gasgeyser.position.rounded):
140 | plt.scatter(
141 | gasgeyser.position[0],
142 | gasgeyser.position[1],
143 | color="yellow",
144 | marker=r"$\spadesuit$",
145 | s=500,
146 | edgecolors="g",
147 | )
148 |
149 | def plot(self, self_only: bool = True, testing: bool = False) -> None:
150 | """
151 |
152 | Debug Method plot
153 |
154 | """
155 | import matplotlib.pyplot as plt
156 |
157 | plt.style.use("ggplot")
158 | self._plot_geysers()
159 | self._plot_minerals()
160 | self._plot_ramps()
161 | self._plot_vision_blockers()
162 | self._plot_corners()
163 | if testing:
164 | self.plot_perimeter(self_only=False)
165 | return
166 | if self_only: # pragma: no cover
167 | self.plot_perimeter(self_only=True)
168 | else: # pragma: no cover
169 | self.plot_perimeter(self_only=False)
170 |
171 | @property
172 | def base_locations(self) -> List[Point2]:
173 | """
174 |
175 | base_locations inside ``self``
176 |
177 | """
178 | return self.bases
179 |
180 | def __repr__(self) -> str: # pragma: no cover
181 | return "Region " + str(self.label)
182 |
--------------------------------------------------------------------------------
/MapAnalyzer/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | # flake8: noqa
3 | from .MapData import MapData
4 | from .Polygon import Polygon
5 | from .Region import Region
6 | from .constructs import ChokeArea, MDRamp, VisionBlockerArea
7 | from pkg_resources import get_distribution, DistributionNotFound
8 |
9 | try:
10 | __version__ = get_distribution('sc2mapanalyzer')
11 | except DistributionNotFound:
12 | __version__ = 'dev'
13 |
--------------------------------------------------------------------------------
/MapAnalyzer/cext/__init__.py:
--------------------------------------------------------------------------------
1 | from .wrapper import astar_path, astar_path_with_nyduses, CMapInfo, CMapChoke
2 |
--------------------------------------------------------------------------------
/MapAnalyzer/cext/mapanalyzerext.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/cext/mapanalyzerext.so
--------------------------------------------------------------------------------
/MapAnalyzer/cext/wrapper.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | try:
4 | from .mapanalyzerext import astar as ext_astar, astar_with_nydus as ext_astar_nydus, get_map_data as ext_get_map_data
5 | except ImportError:
6 | from mapanalyzerext import astar as ext_astar, astar_with_nydus as ext_astar_nydus, get_map_data as ext_get_map_data
7 |
8 | from typing import Optional, Tuple, Union, List, Set
9 | from sc2.position import Point2, Rect
10 |
11 |
12 | class CMapChoke:
13 | """
14 | CMapChoke holds the choke data coming from c extension
15 | main_line pair of floats representing the middle points of the sides of the choke
16 | lines all the lines from side to side
17 | side1 points on side1
18 | side2 points on side2
19 | pixels all the points inside the choke area, should include the sides and the points inside
20 | min_length minimum distance between the sides of the choke
21 | id an integer to represent the choke
22 | """
23 | main_line: Tuple[Tuple[float, float], Tuple[float, float]]
24 | lines: List[Tuple[Tuple[int, int], Tuple[int, int]]]
25 | side1: List[Tuple[int, int]]
26 | side2: List[Tuple[int, int]]
27 | pixels: Set[Tuple[int, int]]
28 | min_length: float
29 | id: int
30 |
31 | def __init__(self, choke_id, main_line, lines, side1, side2, pixels, min_length):
32 | self.id = choke_id
33 | self.main_line = main_line
34 | self.lines = lines
35 | self.side1 = side1
36 | self.side2 = side2
37 | self.pixels = set(pixels)
38 | self.min_length = min_length
39 |
40 | def __repr__(self) -> str:
41 | return f"[{self.id}]CMapChoke; {len(self.pixels)}"
42 |
43 |
44 | # each map can have a list of exceptions, each
45 | # exception should be a type where we can index into a grid
46 | # grid[ex[0], ex[1]] = ...
47 | # meshgrid is used to build rectangular areas we can alter
48 | # in one go
49 | climber_grid_exceptions = {
50 | "DeathAura": [
51 | np.meshgrid(range(36, 49), range(118, 127)),
52 | np.meshgrid(range(143, 154), range(61, 70))
53 | ]
54 | }
55 |
56 |
57 | def astar_path(
58 | weights: np.ndarray,
59 | start: Tuple[int, int],
60 | goal: Tuple[int, int],
61 | large: bool = False,
62 | smoothing: bool = False) -> Union[np.ndarray, None]:
63 | # For the heuristic to be valid, each move must have a positive cost.
64 | # Demand costs above 1 so floating point inaccuracies aren't a problem
65 | # when comparing costs
66 | if weights.min(axis=None) < 1:
67 | raise ValueError("Minimum cost to move must be above or equal to 1, but got %f" % (
68 | weights.min(axis=None)))
69 | # Ensure start is within bounds.
70 | if (start[0] < 0 or start[0] >= weights.shape[0] or
71 | start[1] < 0 or start[1] >= weights.shape[1]):
72 | raise ValueError(f"Start of {start} lies outside grid.")
73 | # Ensure goal is within bounds.
74 | if (goal[0] < 0 or goal[0] >= weights.shape[0] or
75 | goal[1] < 0 or goal[1] >= weights.shape[1]):
76 | raise ValueError(f"Goal of {goal} lies outside grid.")
77 |
78 | height, width = weights.shape
79 | start_idx = np.ravel_multi_index(start, (height, width))
80 | goal_idx = np.ravel_multi_index(goal, (height, width))
81 |
82 | path = ext_astar(
83 | weights.flatten(), height, width, start_idx, goal_idx, large, smoothing
84 | )
85 |
86 | return path
87 |
88 | def astar_path_with_nyduses(weights: np.ndarray,
89 | start: Tuple[int, int],
90 | goal: Tuple[int, int],
91 | nydus_positions: List[Point2],
92 | large: bool = False,
93 | smoothing: bool = False) -> Union[List[np.ndarray], None]:
94 | # For the heuristic to be valid, each move must have a positive cost.
95 | # Demand costs above 1 so floating point inaccuracies aren't a problem
96 | # when comparing costs
97 | if weights.min(axis=None) < 1:
98 | raise ValueError("Minimum cost to move must be above or equal to 1, but got %f" % (
99 | weights.min(axis=None)))
100 | # Ensure start is within bounds.
101 | if (start[0] < 0 or start[0] >= weights.shape[0] or
102 | start[1] < 0 or start[1] >= weights.shape[1]):
103 | raise ValueError(f"Start of {start} lies outside grid.")
104 | # Ensure goal is within bounds.
105 | if (goal[0] < 0 or goal[0] >= weights.shape[0] or
106 | goal[1] < 0 or goal[1] >= weights.shape[1]):
107 | raise ValueError(f"Goal of {goal} lies outside grid.")
108 |
109 | height, width = weights.shape
110 | start_idx = np.ravel_multi_index(start, (height, width))
111 | goal_idx = np.ravel_multi_index(goal, (height, width))
112 | nydus_array = np.zeros((len(nydus_positions),), dtype=np.int32)
113 |
114 | for index, pos in enumerate(nydus_positions):
115 | nydus_idx = np.ravel_multi_index((int(pos.x), int(pos.y)), (height, width))
116 | nydus_array[index] = nydus_idx
117 |
118 | path = ext_astar_nydus(weights.flatten(), height, width, nydus_array.flatten(),
119 | start_idx, goal_idx, large, smoothing)
120 |
121 | return path
122 |
123 |
124 | class CMapInfo:
125 | climber_grid: np.ndarray
126 | overlord_spots: Optional[List[Point2]]
127 | chokes: List[CMapChoke]
128 |
129 | def __init__(self, walkable_grid: np.ndarray, height_map: np.ndarray, playable_area: Rect, map_name: str):
130 | """
131 | walkable_grid and height_map are matrices of type uint8
132 | """
133 |
134 | # grids are transposed and the c extension atm calls the y axis the x axis and vice versa
135 | # so switch the playable area limits around
136 | c_start_y = int(playable_area.x)
137 | c_end_y = int(playable_area.x + playable_area.width)
138 | c_start_x = int(playable_area.y)
139 | c_end_x = int(playable_area.y + playable_area.height)
140 |
141 | self.climber_grid, overlord_data, choke_data = self._get_map_data(walkable_grid, height_map,
142 | c_start_y,
143 | c_end_y,
144 | c_start_x,
145 | c_end_x)
146 |
147 | # some maps may have places where the current method for building the climber grid isn't correct
148 | for map_exception in climber_grid_exceptions:
149 | if map_exception.lower() in map_name.lower():
150 | for exceptions in climber_grid_exceptions[map_exception]:
151 | self.climber_grid[exceptions[0], exceptions[1]] = 0
152 |
153 | break
154 |
155 | self.overlord_spots = list(map(Point2, overlord_data))
156 | self.chokes = []
157 | id_counter = 0
158 | for c in choke_data:
159 | self.chokes.append(CMapChoke(id_counter, c[0], c[1], c[2], c[3], c[4], c[5]))
160 | id_counter += 1
161 |
162 | @staticmethod
163 | def _get_map_data(walkable_grid: np.ndarray, height_map: np.ndarray,
164 | start_y: int,
165 | end_y: int,
166 | start_x: int,
167 | end_x: int):
168 | height, width = walkable_grid.shape
169 | return ext_get_map_data(walkable_grid.flatten(), height_map.flatten(), height, width,
170 | start_y, end_y, start_x, end_x)
171 |
172 |
--------------------------------------------------------------------------------
/MapAnalyzer/constants.py:
--------------------------------------------------------------------------------
1 | MIN_REGION_AREA = 25
2 | MAX_REGION_AREA = 8500
3 | BINARY_STRUCTURE = 2
4 | RESOURCE_BLOCKER_RADIUS_FACTOR = 2
5 | GEYSER_RADIUS_FACTOR = 1.055
6 | NONPATHABLE_RADIUS_FACTOR = 0.8
7 | CORNER_MIN_DISTANCE = 3
8 | LOG_MODULE = "MapAnalyzer"
9 | COLORS = {
10 | 0 : "azure",
11 | 1 : 'black',
12 | 2 : 'red',
13 | 3 : 'green',
14 | 4 : 'blue',
15 | 5 : 'orange',
16 | 6 : 'saddlebrown',
17 | 7 : 'lime',
18 | 8 : 'navy',
19 | 9 : 'purple',
20 | 10: 'olive',
21 | 11: 'aquamarine',
22 | 12: 'indigo',
23 | 13: 'tan',
24 | 14: 'skyblue',
25 | 15: "wheat",
26 | 16: "azure",
27 | 17: "azure",
28 | 18: "azure",
29 | 19: "azure",
30 | 20: "azure"
31 | }
32 | LOG_FORMAT = "{time:YY:MM:DD:HH:mm:ss}|" \
33 | "{level: <8}|{name: ^15}|" \
34 | "{function: ^15}|" \
35 | "{line: >4}|" \
36 | " {level.icon} {message}"
37 |
--------------------------------------------------------------------------------
/MapAnalyzer/constructs.py:
--------------------------------------------------------------------------------
1 | from functools import lru_cache
2 | from typing import Optional, TYPE_CHECKING
3 |
4 | import numpy as np
5 | from loguru import logger
6 | from sc2.game_info import Ramp as sc2Ramp
7 | from sc2.position import Point2
8 |
9 | from .Polygon import Polygon
10 | from .cext import CMapChoke
11 |
12 | if TYPE_CHECKING: # pragma: no cover
13 | from .MapData import MapData
14 |
15 |
16 | class ChokeArea(Polygon):
17 | """
18 |
19 | Base class for all chokes
20 |
21 | """
22 |
23 | def __init__(self, array: np.ndarray, map_data: "MapData") -> None:
24 | super().__init__(map_data=map_data, array=array)
25 | self.main_line = None
26 | self.id = 'Unregistered'
27 | self.md_pl_choke = None
28 | self.is_choke = True
29 | self.ramp = None
30 | self.side_a = None
31 | self.side_b = None
32 |
33 | @property
34 | def corner_walloff(self):
35 | return sorted(list(self.points), key=lambda x: x.distance_to_point2(self.center), reverse=True)[:2]
36 |
37 | @lru_cache()
38 | def same_height(self, p1, p2):
39 | return self.map_data.terrain_height[p1] == self.map_data.terrain_height[p2]
40 |
41 | def __repr__(self) -> str: # pragma: no cover
42 | return f"<[{self.id}]ChokeArea[size={self.area}]>"
43 |
44 |
45 | class RawChoke(ChokeArea):
46 | """
47 | Chokes found in the C extension where the terrain generates a choke point
48 | """
49 |
50 | def __init__(self, array: np.ndarray, map_data: "MapData", raw_choke: CMapChoke) -> None:
51 | super().__init__(map_data=map_data, array=array)
52 |
53 | self.main_line = raw_choke.main_line
54 | self.id = raw_choke.id
55 | self.md_pl_choke = raw_choke
56 |
57 |
58 | self.side_a = Point2((int(round(self.main_line[0][0])), int(round(self.main_line[0][1]))))
59 | self.side_b = Point2((int(round(self.main_line[1][0])), int(round(self.main_line[1][1]))))
60 |
61 | self.points.add(self.side_a)
62 | self.points.add(self.side_b)
63 | self.indices = self.map_data.points_to_indices(self.points)
64 |
65 | def __repr__(self) -> str: # pragma: no cover
66 | return f"<[{self.id}]RawChoke[size={self.area}]>"
67 |
68 |
69 | class MDRamp(ChokeArea):
70 | """
71 |
72 | Wrapper for :class:`sc2.game_info.Ramp`,
73 |
74 | is responsible for calculating the relevant :class:`.Region`
75 | """
76 |
77 | def __init__(self, map_data: "MapData", array: np.ndarray, ramp: sc2Ramp) -> None:
78 | super().__init__(map_data=map_data, array=array)
79 | self.is_ramp = True
80 | self.ramp = ramp
81 | self.offset = Point2((0.5, 0.5))
82 | self.points.add(Point2(self.middle_walloff_depot.rounded))
83 | self._set_sides()
84 |
85 | def _set_sides(self):
86 | ramp_dir = self.ramp.bottom_center - self.ramp.top_center
87 | perpendicular_dir = Point2((-ramp_dir[1], ramp_dir[0])).normalized
88 | step_size = 1
89 |
90 | current = self.ramp.top_center.offset(ramp_dir / 2)
91 | side_a = current.rounded
92 | next_point = current.rounded
93 | while next_point in self.points:
94 | side_a = next_point
95 | current = current.offset(perpendicular_dir*step_size)
96 | next_point = current.rounded
97 |
98 | self.side_a = side_a
99 |
100 | current = self.ramp.top_center.offset(ramp_dir / 2)
101 | side_b = current.rounded
102 | next_point = current.rounded
103 | while next_point in self.points:
104 | side_b = next_point
105 | current = current.offset(-perpendicular_dir * step_size)
106 | next_point = current.rounded
107 |
108 | self.side_b = side_b
109 |
110 | @property
111 | def corner_walloff(self):
112 | raw_points = sorted(list(self.points), key=lambda x: x.distance_to_point2(self.bottom_center), reverse=True)[:2]
113 | offset_points = [p.offset(self.offset) for p in raw_points]
114 | offset_points.extend(raw_points)
115 | return offset_points
116 |
117 | @property
118 | def middle_walloff_depot(self):
119 | raw_points = sorted(list(self.points), key=lambda x: x.distance_to_point2(self.bottom_center), reverse=True)
120 | # TODO its white board time, need to figure out some geometric intuition here
121 | dist = self.map_data.distance(raw_points[0], raw_points[1])
122 | r = dist ** 0.5
123 | if dist / 2 >= r:
124 | intersect = (raw_points[0] + raw_points[1]) / 2
125 | return intersect.offset(self.offset)
126 |
127 | intersects = raw_points[0].circle_intersection(p=raw_points[1], r=r)
128 | # p = self.map_data.closest_towards_point(points=self.buildables.points, target=self.top_center)
129 | pt = max(intersects, key=lambda p: p.distance_to_point2(self.bottom_center))
130 | return pt.offset(self.offset)
131 |
132 | def closest_region(self, region_list):
133 | """
134 |
135 | Will return the closest region with respect to self
136 |
137 | """
138 | return min(region_list,
139 | key=lambda area: min(self.map_data.distance(area.center, point) for point in self.perimeter_points))
140 |
141 | def set_regions(self):
142 | """
143 |
144 | Method for calculating the relevant :class:`.Region`
145 |
146 | TODO:
147 | Make this a private method
148 |
149 | """
150 | from MapAnalyzer.Region import Region
151 | for p in self.perimeter_points:
152 | areas = self.map_data.where_all(p)
153 | for area in areas:
154 | # edge case = its a VisionBlockerArea (and also on the perimeter) so we grab the touching Regions
155 | if isinstance(area, VisionBlockerArea):
156 | for sub_area in area.areas:
157 | # add it to our Areas
158 | if isinstance(sub_area, Region) and sub_area not in self.areas:
159 | self.areas.append(sub_area)
160 | # add ourselves to it's Areas
161 | if isinstance(sub_area, Region) and self not in sub_area.areas:
162 | sub_area.areas.append(self)
163 |
164 | # standard case
165 | if isinstance(area, Region) and area not in self.areas:
166 | self.areas.append(area)
167 | # add ourselves to the Region Area's
168 | if isinstance(area, Region) and self not in area.areas:
169 | area.areas.append(self)
170 |
171 | if len(self.regions) < 2:
172 | region_list = list(self.map_data.regions.values())
173 | region_list.remove(self.regions[0])
174 | closest_region = self.closest_region(region_list=region_list)
175 | assert (closest_region not in self.regions)
176 | self.areas.append(closest_region)
177 |
178 | @property
179 | def top_center(self) -> Point2:
180 | """
181 |
182 | Alerts when sc2 fails to provide a top_center, and fallback to :meth:`.center`
183 |
184 | """
185 | if self.ramp.top_center is not None:
186 | return self.ramp.top_center
187 | else:
188 | logger.debug(f"No top_center found for {self}, falling back to `center`")
189 | return self.center
190 |
191 | @property
192 | def bottom_center(self) -> Point2:
193 | """
194 |
195 | Alerts when sc2 fails to provide a bottom_center, and fallback to :meth:`.center`
196 |
197 | """
198 | if self.ramp.bottom_center is not None:
199 | return self.ramp.bottom_center
200 | else:
201 | logger.debug(f"No bottom_center found for {self}, falling back to `center`")
202 | return self.center
203 |
204 | def __repr__(self) -> str: # pragma: no cover
205 | return f""
206 |
207 | def __str__(self):
208 | return f"R[{self.area}]"
209 |
210 |
211 | class VisionBlockerArea(ChokeArea):
212 | """
213 |
214 | VisionBlockerArea are areas containing tiles that hide the units that stand in it,
215 |
216 | (for example, bushes)
217 |
218 | Units that attack from within a :class:`VisionBlockerArea`
219 |
220 | cannot be targeted by units that do not stand inside
221 | """
222 |
223 | def __init__(self, map_data: "MapData", array: np.ndarray) -> None:
224 | super().__init__(map_data=map_data, array=array)
225 | self.is_vision_blocker = True
226 | self._set_sides()
227 |
228 | def _set_sides(self):
229 | org = self.top
230 | pts = [self.bottom, self.right, self.left]
231 | res = self.map_data.closest_towards_point(points=pts, target=org)
232 | self.side_a = int(round((res[0] + org[0]) / 2)), int(round((res[1] + org[1]) / 2))
233 | if res != self.bottom:
234 | org = self.bottom
235 | pts = [self.top, self.right, self.left]
236 | res = self.map_data.closest_towards_point(points=pts, target=org)
237 | self.side_b = int(round((res[0] + org[0]) / 2)), int(round((res[1] + org[1]) / 2))
238 | else:
239 | self.side_b = int(round((self.right[0] + self.left[0]) / 2)), int(round((self.right[1] + self.left[1]) / 2))
240 | points = list(self.points)
241 | points.append(self.side_a)
242 | points.append(self.side_b)
243 | self.points = set([Point2((int(p[0]), int(p[1]))) for p in points])
244 | self.indices = self.map_data.points_to_indices(self.points)
245 |
246 | def __repr__(self): # pragma: no cover
247 | return f""
248 |
--------------------------------------------------------------------------------
/MapAnalyzer/decorators.py:
--------------------------------------------------------------------------------
1 | import contextlib
2 | import functools
3 | import sys
4 | import threading
5 | from functools import partial
6 | from typing import Any, Callable, Dict, Optional, Tuple
7 |
8 | # import tqdm
9 | from loguru import logger
10 | from tqdm import tqdm as std_tqdm
11 | from tqdm.contrib import DummyTqdmFile
12 |
13 | tqdm = partial(std_tqdm, dynamic_ncols=True)
14 |
15 | def logger_wraps(*, entry=True, exit=True, level="INFO"):
16 | def wrapper(func):
17 | name = func.__name__
18 |
19 | @functools.wraps(func)
20 | def wrapped(*args, **kwargs):
21 | logger_ = logger.opt(depth=1)
22 | if entry:
23 | logger_.log(level, "Entering '{}' (args={}, kwargs={})", name, args, kwargs)
24 | result = func(*args, **kwargs)
25 | if exit:
26 | logger_.log(level, "Exiting '{}' (result={})", name, result)
27 | return result
28 |
29 | return wrapped
30 |
31 | return wrapper
32 |
33 |
34 | @contextlib.contextmanager
35 | def std_out_err_redirect_tqdm():
36 | orig_out_err = sys.stdout, sys.stderr
37 | try:
38 | sys.stdout, sys.stderr = map(DummyTqdmFile, orig_out_err)
39 | yield orig_out_err[0]
40 | # Relay exceptions
41 | except Exception as exc:
42 | raise exc
43 | # Always restore sys.stdout/err if necessary
44 | finally:
45 | sys.stdout, sys.stderr = orig_out_err
46 |
47 |
48 | def provide_progress_bar(function: Callable, estimated_time: int, tstep: float = 0.2,
49 | tqdm_kwargs: Optional[Dict[str, str]] = None, args: Optional[Tuple["MapData"]] = None,
50 | kwargs: Optional[Dict[Any, Any]] = None) -> None:
51 | """Tqdm wrapper for a long-running function
52 | args:
53 | function - function to run
54 | estimated_time - how long you expect the function to take
55 | tstep - time delta (seconds) for progress bar updates
56 | tqdm_kwargs - kwargs to construct the progress bar
57 | args - args to pass to the function
58 | kwargs - keyword args to pass to the function
59 | ret:
60 | function(*args, **kwargs)
61 | """
62 | if tqdm_kwargs is None:
63 | tqdm_kwargs = {}
64 | if args is None:
65 | args = []
66 | if kwargs is None:
67 | kwargs = {}
68 | ret = [None] # Mutable var so the function can store its return value
69 |
70 | # with std_out_err_redirect_tqdm() as orig_stdout:
71 |
72 | def myrunner(func, ret_val, *r_args, **r_kwargs):
73 | ret_val[0] = func(*r_args, **r_kwargs)
74 |
75 | thread = threading.Thread(target=myrunner, args=(function, ret) + tuple(args), kwargs=kwargs)
76 | pbar = tqdm(total=estimated_time, **tqdm_kwargs)
77 | thread.start()
78 | while thread.is_alive():
79 | thread.join(timeout=tstep)
80 | pbar.update(tstep)
81 | pbar.close()
82 | return ret[0]
83 |
84 |
85 | def progress_wrapped(estimated_time: int, desc: str = "Progress", tstep: float = 0.2,
86 | tqdm_kwargs: None = None) -> Callable:
87 | """Decorate a function to add a progress bar"""
88 |
89 | if tqdm_kwargs is None:
90 | # tqdm_kwargs = {"bar_format": '{desc}: {percentage:3.0f}%|{bar}| {n:.1f}/{total:.1f} [{elapsed}<{remaining}]'}
91 | tqdm_kwargs = {}
92 |
93 | def real_decorator(function):
94 | @functools.wraps(function)
95 | def wrapper(*args, **kwargs):
96 | tqdm_kwargs['desc'] = desc
97 | return provide_progress_bar(function, estimated_time=estimated_time, tstep=tstep, tqdm_kwargs=tqdm_kwargs,
98 | args=args, kwargs=kwargs)
99 |
100 | return wrapper
101 |
102 | return real_decorator
103 |
--------------------------------------------------------------------------------
/MapAnalyzer/destructibles.py:
--------------------------------------------------------------------------------
1 | """
2 | https://github.com/DrInfy/sharpy-sc2/blob/develop/sharpy/managers/unit_value.py
3 | """
4 | from sc2 import UnitTypeId
5 |
6 | buildings_2x2 = {
7 | UnitTypeId.SUPPLYDEPOT,
8 | UnitTypeId.PYLON,
9 | UnitTypeId.DARKSHRINE,
10 | UnitTypeId.PHOTONCANNON,
11 | UnitTypeId.SHIELDBATTERY,
12 | UnitTypeId.TECHLAB,
13 | UnitTypeId.STARPORTTECHLAB,
14 | UnitTypeId.FACTORYTECHLAB,
15 | UnitTypeId.BARRACKSTECHLAB,
16 | UnitTypeId.REACTOR,
17 | UnitTypeId.STARPORTREACTOR,
18 | UnitTypeId.FACTORYREACTOR,
19 | UnitTypeId.BARRACKSREACTOR,
20 | UnitTypeId.MISSILETURRET,
21 | UnitTypeId.SPORECRAWLER,
22 | UnitTypeId.SPIRE,
23 | UnitTypeId.GREATERSPIRE,
24 | UnitTypeId.SPINECRAWLER,
25 | }
26 |
27 | buildings_3x3 = {
28 | UnitTypeId.GATEWAY,
29 | UnitTypeId.WARPGATE,
30 | UnitTypeId.CYBERNETICSCORE,
31 | UnitTypeId.FORGE,
32 | UnitTypeId.ROBOTICSFACILITY,
33 | UnitTypeId.ROBOTICSBAY,
34 | UnitTypeId.TEMPLARARCHIVE,
35 | UnitTypeId.TWILIGHTCOUNCIL,
36 | UnitTypeId.TEMPLARARCHIVE,
37 | UnitTypeId.STARGATE,
38 | UnitTypeId.FLEETBEACON,
39 | UnitTypeId.ASSIMILATOR,
40 | UnitTypeId.ASSIMILATORRICH,
41 | UnitTypeId.SPAWNINGPOOL,
42 | UnitTypeId.ROACHWARREN,
43 | UnitTypeId.HYDRALISKDEN,
44 | UnitTypeId.BANELINGNEST,
45 | UnitTypeId.EVOLUTIONCHAMBER,
46 | UnitTypeId.NYDUSNETWORK,
47 | UnitTypeId.NYDUSCANAL,
48 | UnitTypeId.EXTRACTOR,
49 | UnitTypeId.EXTRACTORRICH,
50 | UnitTypeId.INFESTATIONPIT,
51 | UnitTypeId.ULTRALISKCAVERN,
52 | UnitTypeId.BARRACKS,
53 | UnitTypeId.ENGINEERINGBAY,
54 | UnitTypeId.FACTORY,
55 | UnitTypeId.GHOSTACADEMY,
56 | UnitTypeId.STARPORT,
57 | UnitTypeId.FUSIONREACTOR,
58 | UnitTypeId.BUNKER,
59 | UnitTypeId.ARMORY,
60 | UnitTypeId.REFINERY,
61 | UnitTypeId.REFINERYRICH,
62 | }
63 |
64 | buildings_5x5 = {
65 | UnitTypeId.NEXUS,
66 | UnitTypeId.HATCHERY,
67 | UnitTypeId.HIVE,
68 | UnitTypeId.LAIR,
69 | UnitTypeId.COMMANDCENTER,
70 | UnitTypeId.ORBITALCOMMAND,
71 | UnitTypeId.PLANETARYFORTRESS,
72 | }
73 |
74 | BUILDING_IDS = buildings_5x5.union(buildings_3x3).union(buildings_2x2)
75 |
76 |
77 | destructable_2x2 = {
78 | UnitTypeId.ROCKS2X2NONCONJOINED,
79 | UnitTypeId.DEBRIS2X2NONCONJOINED
80 | }
81 |
82 | destructable_4x4 = {
83 | UnitTypeId.DESTRUCTIBLECITYDEBRIS4X4,
84 | UnitTypeId.DESTRUCTIBLEDEBRIS4X4,
85 | UnitTypeId.DESTRUCTIBLEICE4X4,
86 | UnitTypeId.DESTRUCTIBLEROCK4X4,
87 | UnitTypeId.DESTRUCTIBLEROCKEX14X4,
88 | }
89 |
90 | destructable_4x2 = {
91 | UnitTypeId.DESTRUCTIBLECITYDEBRIS2X4HORIZONTAL,
92 | UnitTypeId.DESTRUCTIBLEICE2X4HORIZONTAL,
93 | UnitTypeId.DESTRUCTIBLEROCK2X4HORIZONTAL,
94 | UnitTypeId.DESTRUCTIBLEROCKEX12X4HORIZONTAL,
95 | }
96 |
97 | destructable_2x4 = {
98 | UnitTypeId.DESTRUCTIBLECITYDEBRIS2X4VERTICAL,
99 | UnitTypeId.DESTRUCTIBLEICE2X4VERTICAL,
100 | UnitTypeId.DESTRUCTIBLEROCK2X4VERTICAL,
101 | UnitTypeId.DESTRUCTIBLEROCKEX12X4VERTICAL,
102 | }
103 |
104 | destructable_6x2 = {
105 | UnitTypeId.DESTRUCTIBLECITYDEBRIS2X6HORIZONTAL,
106 | UnitTypeId.DESTRUCTIBLEICE2X6HORIZONTAL,
107 | UnitTypeId.DESTRUCTIBLEROCK2X6HORIZONTAL,
108 | UnitTypeId.DESTRUCTIBLEROCKEX12X6HORIZONTAL,
109 | }
110 |
111 | destructable_2x6 = {
112 | UnitTypeId.DESTRUCTIBLECITYDEBRIS2X6VERTICAL,
113 | UnitTypeId.DESTRUCTIBLEICE2X6VERTICAL,
114 | UnitTypeId.DESTRUCTIBLEROCK2X6VERTICAL,
115 | UnitTypeId.DESTRUCTIBLEROCKEX12X6VERTICAL,
116 | }
117 |
118 | destructable_4x12 = {
119 | UnitTypeId.DESTRUCTIBLEROCKEX1VERTICALHUGE,
120 | UnitTypeId.DESTRUCTIBLEICEVERTICALHUGE
121 | }
122 |
123 | destructable_12x4 = {
124 | UnitTypeId.DESTRUCTIBLEROCKEX1HORIZONTALHUGE,
125 | UnitTypeId.DESTRUCTIBLEICEHORIZONTALHUGE
126 | }
127 |
128 | destructable_6x6 = {
129 | UnitTypeId.DESTRUCTIBLECITYDEBRIS6X6,
130 | UnitTypeId.DESTRUCTIBLEDEBRIS6X6,
131 | UnitTypeId.DESTRUCTIBLEICE6X6,
132 | UnitTypeId.DESTRUCTIBLEROCK6X6,
133 | UnitTypeId.DESTRUCTIBLEROCKEX16X6,
134 | }
135 |
136 | destructable_BLUR = {
137 | UnitTypeId.DESTRUCTIBLECITYDEBRISHUGEDIAGONALBLUR,
138 | UnitTypeId.DESTRUCTIBLEDEBRISRAMPDIAGONALHUGEBLUR,
139 | UnitTypeId.DESTRUCTIBLEICEDIAGONALHUGEBLUR,
140 | UnitTypeId.DESTRUCTIBLEROCKEX1DIAGONALHUGEBLUR,
141 | UnitTypeId.DESTRUCTIBLERAMPDIAGONALHUGEBLUR,
142 | }
143 |
144 | destructable_ULBR = {
145 | UnitTypeId.DESTRUCTIBLECITYDEBRISHUGEDIAGONALULBR,
146 | UnitTypeId.DESTRUCTIBLEDEBRISRAMPDIAGONALHUGEULBR,
147 | UnitTypeId.DESTRUCTIBLEICEDIAGONALHUGEULBR,
148 | UnitTypeId.DESTRUCTIBLEROCKEX1DIAGONALHUGEULBR,
149 | UnitTypeId.DESTRUCTIBLERAMPDIAGONALHUGEULBR,
150 | }
151 |
--------------------------------------------------------------------------------
/MapAnalyzer/exceptions.py:
--------------------------------------------------------------------------------
1 | from typing import Tuple
2 |
3 |
4 | class CustomDeprecationWarning(BaseException):
5 | def __init__(self, oldarg=None, newarg=None):
6 | self.old = oldarg
7 | self.new = newarg
8 |
9 | def __str__(self) -> str:
10 | return f"[DeprecationWarning] Passing `{self.old}` argument is deprecated," \
11 | f" and will have no effect,\nUse `{self.new}` instead"
12 |
13 |
14 | class PatherNoPointsException(BaseException):
15 | def __init__(self, start, goal) -> None:
16 | super().__init__()
17 | self.start = start
18 | self.goal = goal
19 |
20 | def __str__(self) -> str:
21 | return f"[PatherNoPointsException]" \
22 | f"\nExpected: Start (pointlike), Goal (pointlike)," \
23 | f"\nGot: Start {self.start}, Goal {self.goal}."
24 |
25 |
26 | class OutOfBoundsException(BaseException):
27 | def __init__(self, p: Tuple[int, int]) -> None:
28 | super().__init__()
29 | self.point = p
30 |
31 | def __str__(self) -> str:
32 | return f"[OutOfBoundsException]Point {self.point} is not inside the grid. No influence added."
33 |
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/2000AtmospheresAIE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/2000AtmospheresAIE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/AbyssalReefLE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/AbyssalReefLE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/AutomatonLE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/AutomatonLE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/BerlingradAIE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/BerlingradAIE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/BlackburnAIE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/BlackburnAIE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/CuriousMindsAIE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/CuriousMindsAIE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/DeathAuraLE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/DeathAuraLE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/EphemeronLE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/EphemeronLE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/EternalEmpireLE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/EternalEmpireLE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/EverDreamLE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/EverDreamLE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/GlitteringAshesAIE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/GlitteringAshesAIE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/GoldenWallLE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/GoldenWallLE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/HardwireAIE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/HardwireAIE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/IceandChromeLE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/IceandChromeLE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/JagannathaAIE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/JagannathaAIE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/LightshadeAIE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/LightshadeAIE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/NightshadeLE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/NightshadeLE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/OxideAIE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/OxideAIE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/PillarsofGoldLE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/PillarsofGoldLE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/RomanticideAIE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/RomanticideAIE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/SimulacrumLE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/SimulacrumLE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/SubmarineLE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/SubmarineLE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/Triton.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/Triton.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/WorldofSleepersLE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/WorldofSleepersLE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/pickle_gameinfo/ZenLE.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/MapAnalyzer/pickle_gameinfo/ZenLE.xz
--------------------------------------------------------------------------------
/MapAnalyzer/settings.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
4 |
--------------------------------------------------------------------------------
/MapAnalyzer/utils.py:
--------------------------------------------------------------------------------
1 | import lzma
2 | import os
3 | import pickle
4 | import numpy as np
5 |
6 | from typing import List, Optional, TYPE_CHECKING, Union
7 |
8 | from s2clientprotocol.sc2api_pb2 import Response, ResponseObservation
9 | from sc2.bot_ai import BotAI
10 | from sc2.game_data import GameData
11 | from sc2.game_info import GameInfo, Ramp
12 | from sc2.game_state import GameState
13 | from sc2.position import Point2
14 | from sc2.unit import Unit
15 |
16 | from MapAnalyzer.constructs import MDRamp, VisionBlockerArea
17 | from .cext import CMapChoke
18 | from .destructibles import *
19 | from .settings import ROOT_DIR
20 |
21 | if TYPE_CHECKING:
22 | from MapAnalyzer.MapData import MapData
23 |
24 |
25 | def change_destructable_status_in_grid(grid: np.ndarray, unit: Unit, status: int):
26 | """
27 | Set destructable positions to status, modifies the grid in place
28 | """
29 | type_id = unit.type_id
30 | pos = unit.position
31 | name = unit.name
32 |
33 | # this is checked with name because the id of the small mineral destructables
34 | # has changed over patches and may cause problems
35 | if name == "MineralField450":
36 | x = int(pos[0]) - 1
37 | y = int(pos[1])
38 | grid[x:(x + 2), y] = status
39 | elif type_id in destructable_2x2:
40 | w = 2
41 | h = 2
42 | x = int(pos[0] - w/2)
43 | y = int(pos[1] - h/2)
44 | grid[x:(x + w), y:(y + h)] = status
45 | elif type_id in destructable_2x4:
46 | w = 2
47 | h = 4
48 | x = int(pos[0] - w / 2)
49 | y = int(pos[1] - h / 2)
50 | grid[x:(x + w), y:(y + h)] = status
51 | elif type_id in destructable_2x6:
52 | w = 2
53 | h = 6
54 | x = int(pos[0] - w / 2)
55 | y = int(pos[1] - h / 2)
56 | grid[x:(x + w), y:(y + h)] = status
57 | elif type_id in destructable_4x2:
58 | w = 4
59 | h = 2
60 | x = int(pos[0] - w / 2)
61 | y = int(pos[1] - h / 2)
62 | grid[x:(x + w), y:(y + h)] = status
63 | elif type_id in destructable_4x4:
64 | w = 4
65 | h = 4
66 | x = int(pos[0] - w / 2)
67 | y = int(pos[1] - h / 2)
68 | grid[x:(x + w), y:(y + h)] = status
69 | elif type_id in destructable_6x2:
70 | w = 6
71 | h = 2
72 | x = int(pos[0] - w / 2)
73 | y = int(pos[1] - h / 2)
74 | grid[x:(x + w), y:(y + h)] = status
75 | elif type_id in destructable_6x6:
76 | # for some reason on some maps like death aura
77 | # these rocks have a bit weird sizes
78 | # on golden wall this is an exact match
79 | # on death aura the height is one coordinate off
80 | # and it varies whether the position is centered too high or too low
81 | w = 6
82 | h = 6
83 | x = int(pos[0] - w / 2)
84 | y = int(pos[1] - h / 2)
85 | grid[x:(x + w), (y + 1):(y + h - 1)] = status
86 | grid[(x + 1):(x + w - 1), y:(y + h)] = status
87 | elif type_id in destructable_12x4:
88 | w = 12
89 | h = 4
90 | x = int(pos[0] - w / 2)
91 | y = int(pos[1] - h / 2)
92 | grid[x:(x + w), y:(y + h)] = status
93 | elif type_id in destructable_4x12:
94 | w = 4
95 | h = 12
96 | x = int(pos[0] - w / 2)
97 | y = int(pos[1] - h / 2)
98 | grid[x:(x + w), y:(y + h)] = status
99 | elif type_id in destructable_BLUR:
100 | x_ref = int(pos[0] - 5)
101 | y_pos = int(pos[1])
102 | grid[(x_ref + 6):(x_ref + 6 + 2), y_pos + 4] = status
103 | grid[(x_ref + 5):(x_ref + 5 + 4), y_pos + 3] = status
104 | grid[(x_ref + 4):(x_ref + 4 + 6), y_pos + 2] = status
105 | grid[(x_ref + 3):(x_ref + 3 + 7), y_pos + 1] = status
106 | grid[(x_ref + 2):(x_ref + 2 + 7), y_pos] = status
107 | grid[(x_ref + 1):(x_ref + 1 + 7), y_pos - 1] = status
108 | grid[(x_ref + 0):(x_ref + 0 + 7), y_pos - 2] = status
109 | grid[(x_ref + 0):(x_ref + 0 + 6), y_pos - 3] = status
110 | grid[(x_ref + 1):(x_ref + 1 + 4), y_pos - 4] = status
111 | grid[(x_ref + 2):(x_ref + 2 + 2), y_pos - 5] = status
112 |
113 | elif type_id in destructable_ULBR:
114 | x_ref = int(pos[0] - 5)
115 | y_pos = int(pos[1])
116 | grid[(x_ref + 6):(x_ref + 6 + 2), y_pos - 5] = status
117 | grid[(x_ref + 5):(x_ref + 5 + 4), y_pos - 4] = status
118 | grid[(x_ref + 4):(x_ref + 4 + 6), y_pos - 3] = status
119 | grid[(x_ref + 3):(x_ref + 3 + 7), y_pos - 2] = status
120 | grid[(x_ref + 2):(x_ref + 2 + 7), y_pos - 1] = status
121 | grid[(x_ref + 1):(x_ref + 1 + 7), y_pos] = status
122 | grid[(x_ref + 0):(x_ref + 0 + 7), y_pos + 1] = status
123 | grid[(x_ref + 0):(x_ref + 0 + 6), y_pos + 2] = status
124 | grid[(x_ref + 1):(x_ref + 1 + 4), y_pos + 3] = status
125 | grid[(x_ref + 2):(x_ref + 2 + 2), y_pos + 4] = status
126 |
127 | def fix_map_ramps(bot: BotAI):
128 | """
129 | following https://github.com/BurnySc2/python-sc2/blob/ffb9bd43dcbeb923d848558945a8c59c9662f435/sc2/game_info.py#L246
130 | to fix burnysc2 ramp objects by removing destructables
131 | """
132 | pathing_grid = bot.game_info.pathing_grid.data_numpy.T.copy()
133 | for dest in bot.destructables:
134 | change_destructable_status_in_grid(pathing_grid, dest, 1)
135 |
136 | pathing = np.ndenumerate(pathing_grid.T)
137 |
138 | def equal_height_around(tile):
139 | sliced = bot.game_info.terrain_height.data_numpy[tile[1] - 1: tile[1] + 2, tile[0] - 1: tile[0] + 2]
140 | return len(np.unique(sliced)) == 1
141 |
142 | map_area = bot.game_info.playable_area
143 | points = [
144 | Point2((a, b))
145 | for (b, a), value in pathing
146 | if value == 1
147 | and map_area.x <= a < map_area.x + map_area.width
148 | and map_area.y <= b < map_area.y + map_area.height
149 | and bot.game_info.placement_grid[(a, b)] == 0
150 | ]
151 | ramp_points = [point for point in points if not equal_height_around(point)]
152 | vision_blockers = set(point for point in points if equal_height_around(point))
153 | ramps = [Ramp(group, bot.game_info) for group in bot.game_info._find_groups(ramp_points)]
154 | return ramps, vision_blockers
155 |
156 |
157 | def get_sets_with_mutual_elements(list_mdchokes: List[CMapChoke],
158 | area: Optional[Union[MDRamp, VisionBlockerArea]] = None,
159 | base_choke: CMapChoke = None) -> List[List]:
160 | li = []
161 | if area:
162 | s1 = area.points
163 | else:
164 | s1 = base_choke.pixels
165 | for c in list_mdchokes:
166 | s2 = c.pixels
167 | s3 = s1 ^ s2
168 | if len(s3) <= 0.95*(len(s1) + len(s2)):
169 | li.append(c.id)
170 | return li
171 |
172 |
173 | def mock_map_data(map_file: str) -> "MapData":
174 | from MapAnalyzer.MapData import MapData
175 | with lzma.open(f"{map_file}", "rb") as f:
176 | raw_game_data, raw_game_info, raw_observation = pickle.load(f)
177 |
178 | bot = import_bot_instance(raw_game_data, raw_game_info, raw_observation)
179 | return MapData(bot=bot)
180 |
181 |
182 | def import_bot_instance(
183 | raw_game_data: Response,
184 | raw_game_info: Response,
185 | raw_observation: ResponseObservation,
186 | ) -> BotAI:
187 | """
188 | import_bot_instance DocString
189 | """
190 | bot = BotAI()
191 | game_data = GameData(raw_game_data.data)
192 | game_info = GameInfo(raw_game_info.game_info)
193 | game_state = GameState(raw_observation)
194 | # noinspection PyProtectedMember
195 | bot._initialize_variables()
196 | # noinspection PyProtectedMember
197 | bot._prepare_start(
198 | client=None, player_id=1, game_info=game_info, game_data=game_data
199 | )
200 | # noinspection PyProtectedMember
201 | bot._prepare_first_step()
202 | # noinspection PyProtectedMember
203 | bot._prepare_step(state=game_state, proto_game_info=raw_game_info)
204 | # noinspection PyProtectedMember
205 | bot._find_expansion_locations()
206 | return bot
207 |
208 |
209 | def get_map_files_folder() -> str:
210 | folder = ROOT_DIR
211 | subfolder = "pickle_gameinfo"
212 | return os.path.join(folder, subfolder)
213 |
214 |
215 | def get_map_file_list() -> List[str]:
216 | """
217 | easy way to produce less than all maps, for example if we want to test utils, we only need one MapData object
218 | """
219 | map_files_folder = get_map_files_folder()
220 | map_files = os.listdir(map_files_folder)
221 | li = []
222 | for map_file in map_files:
223 | li.append(os.path.join(map_files_folder, map_file))
224 | return li
225 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SC2MapAnalysis
2 |
3 |
4 | * 
5 | [master](https://github.com/eladyaniv01/SC2MapAnalysis/tree/master)
6 |
7 | *  [Changelog](https://github.com/eladyaniv01/SC2MapAnalysis/blob/master/CHANGELOG.md)
8 |
9 | * 
10 | [Documentation](https://eladyaniv01.github.io/SC2MapAnalysis/)
11 |
12 | Summary
13 | -------
14 | A standalone plugin for python SC2 api
15 |
16 |
17 | Why Do we need this ?
18 | =====================
19 |
20 | This module is inspired by plays like this one [TY map positioning](https://www.youtube.com/watch?v=NUQsAWIBTSk&start=458)
21 | (notice how the army splits into groups, covering different areas, tanks are tucked in corners, and so on)
22 |
23 | Hopefully with the interface provided here, you will be able to build plays like that one!
24 |
25 | it is meant to be a tool(extension) for [BurnySc2](https://github.com/BurnySc2/python-sc2/)
26 |
27 | Thanks A lot to [DrInfy](https://github.com/DrInfy) for solving one of the biggest challenges, finding rare choke points.
28 |
29 | check out his work
30 |
31 | * [Sharpy](https://github.com/DrInfy/sharpy-sc2) for rapid bot development.
32 |
33 | * [sc2pathlib](https://github.com/DrInfy/sc2-pathlib) a high performance rust module with python interface for pathfinding
34 |
35 |
36 | More Examples reside in the [Documentation](https://eladyaniv01.github.io/SC2MapAnalysis/)
37 |
38 | Example bot usage can be found in `dummybot.py`
39 |
40 | Example:
41 | ```python
42 | import pickle
43 | import lzma
44 | from MapData import MapData
45 | from utils import import_bot_instance
46 |
47 | #if its from BurnySc2 it is compressed
48 | # https://github.com/BurnySc2/python-sc2/tree/develop/test/pickle_data
49 | YOUR_FILE_PATH = 'some_directory/map_file'
50 | with lzma.open(YOUR_FILE_PATH, "rb") as f:
51 | raw_game_data, raw_game_info, raw_observation = pickle.load(f)
52 |
53 | # mocking a bot object to initalize the map, this is for when you want to do this while not in a game,
54 | # if you want to use it in a game just pass in the bot object like shown below
55 |
56 | bot = import_bot_instance(raw_import_bot_instancegame_data, raw_game_info, raw_observation)
57 |
58 |
59 | # And then you can instantiate a MapData Object like so
60 | map_data = MapData(bot)
61 |
62 |
63 | # plot the entire labeled map
64 | map_data.plot_map()
65 |
66 | # red dots or X are vision blockers,
67 | # ramps are marked with white dots
68 | # ramp top center is marked with '^'
69 | # gas geysers are yellow spades
70 | # MDRampss are marked with R
71 | # height span is with respect to : light = high , dark = low
72 | # ChokeArea is marked with green heart suites
73 | # Corners are marked with a red 'V'
74 | ```
75 |
76 |
77 |
78 |
79 | Tested Maps ( [AiArena](https://ai-arena.net/) and [SC2ai](https://sc2ai.net/) ladder map pool) :
80 | ```
81 | ['AbyssalReefLE.xz',
82 | 'AutomatonLE.xz',
83 | 'DeathAuraLE.xz', # currently fails on ground pathing
84 | 'EphemeronLE.xz',
85 | 'EternalEmpireLE.xz',
86 | 'EverDreamLE.xz',
87 | 'GoldenWallLE.xz',
88 | 'IceandChromeLE.xz',
89 | 'NightshadeLE.xz',
90 | 'PillarsofGoldLE.xz',
91 | 'SimulacrumLE.xz',
92 | 'SubmarineLE.xz',
93 | 'Triton.xz',
94 | 'WorldofSleepersLE.xz',
95 | 'ZenLE.xz']
96 | ```
97 |
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from tests.test_docs import map_data
4 |
5 | #
6 | # def pytest_collection_finish(session):
7 | # """Handle the pytest collection finish hook: configure pyannotate.
8 | # Explicitly delay importing `collect_types` until all tests have
9 | # been collected. This gives gevent a chance to monkey patch the
10 | # world before importing pyannotate.
11 | # """
12 | # from pyannotate_runtime import collect_types
13 | #
14 | # collect_types.init_types_collection()
15 | #
16 | #
17 | # @pytest.fixture(autouse=True)
18 | # def collect_types_fixture():
19 | # from pyannotate_runtime import collect_types
20 | #
21 | # collect_types.start()
22 | # yield
23 | # collect_types.stop()
24 | #
25 | #
26 | # def pytest_sessionfinish(session, exitstatus):
27 | # from pyannotate_runtime import collect_types
28 | #
29 | # collect_types.dump_stats("type_info.json")
30 |
31 |
32 | @pytest.fixture(autouse=True)
33 | def add_map_data(doctest_namespace):
34 | doctest_namespace["self"] = map_data
35 |
--------------------------------------------------------------------------------
/dummybot.py:
--------------------------------------------------------------------------------
1 | import random
2 | from typing import List
3 |
4 | import sc2
5 | from sc2.player import Bot, Computer
6 | from sc2.position import Point3, Point2
7 |
8 | from loguru import logger
9 |
10 | from MapAnalyzer import MapData
11 |
12 | GREEN = Point3((0, 255, 0))
13 | RED = Point3((255, 0, 0))
14 | BLUE = Point3((0, 0, 255))
15 | BLACK = Point3((0, 0, 0))
16 |
17 |
18 | class MATester(sc2.BotAI):
19 |
20 | def __init__(self):
21 | super().__init__()
22 | self.map_data = None
23 | # local settings for easy debug
24 | self.target = None
25 | self.base = None
26 | self.sens = 4
27 | self.hero_tag = None
28 | self.p0 = None
29 | self.p1 = None
30 | self.influence_grid = None
31 | self.ramp = None
32 | self.influence_points = None
33 | self.path = None
34 | logger.remove() # avoid duplicate logging
35 |
36 | async def on_start(self):
37 | self.map_data = MapData(self, loglevel="DEBUG", arcade=True)
38 |
39 | base = self.townhalls[0]
40 | self.base = reg_start = self.map_data.where_all(base.position_tuple)[0]
41 | reg_end = self.map_data.where_all(self.enemy_start_locations[0].position)[0]
42 | self.p0 = reg_start.center
43 | self.p1 = reg_end.center
44 | self.influence_grid = self.map_data.get_pyastar_grid()
45 | ramps = reg_end.region_ramps
46 | logger.info(ramps)
47 | if len(ramps) > 1:
48 | if self.map_data.distance(ramps[0].top_center, reg_end.center) < self.map_data.distance(ramps[1].top_center,
49 | reg_end.center):
50 | self.ramp = ramps[0]
51 | else:
52 | self.ramp = ramps[1]
53 | else:
54 | self.ramp = ramps[0]
55 |
56 | self.influence_points = [(self.ramp.top_center, 2), (Point2((66, 66)), 18)]
57 |
58 | self.influence_points = self._get_random_influence(25, 5)
59 | """Uncomment this code block to add random costs and make the path more complex"""
60 | # for tup in self.influence_points:
61 | # p = tup[0]
62 | # r = tup[1]
63 | # self.map_data.add_cost(p, r=r, arr=self.influence_grid)
64 |
65 | self.path = self.map_data.pathfind(start=self.p0, goal=self.p1, grid=self.influence_grid, sensitivity=self.sens)
66 | self.hero_tag = self.workers[0].tag
67 |
68 | def get_random_point(self, minx, maxx, miny, maxy):
69 | return (random.randint(minx, maxx), random.randint(miny, maxy))
70 |
71 | def _get_random_influence(self, n, r):
72 | pts = []
73 | for i in range(n):
74 | pts.append(
75 | (Point2(self.get_random_point(50, 130, 25, 175)), r))
76 | return pts
77 |
78 | def _draw_point_list(self, point_list: List = None, color=None, text=None, box_r=None) -> bool:
79 | if not color:
80 | color = GREEN
81 | h = self.get_terrain_z_height(self.townhalls[0])
82 | for p in point_list:
83 | p = Point2(p)
84 |
85 | pos = Point3((p.x, p.y, h))
86 | if box_r:
87 | p0 = Point3((pos.x - box_r, pos.y - box_r, pos.z + box_r)) + Point2((0.5, 0.5))
88 | p1 = Point3((pos.x + box_r, pos.y + box_r, pos.z - box_r)) + Point2((0.5, 0.5))
89 | self.client.debug_box_out(p0, p1, color=color)
90 | if text:
91 | self.client.debug_text_world(
92 | "\n".join([f"{text}", ]), pos, color=color, size=30,
93 | )
94 |
95 | def _draw_path_box(self, p, color):
96 | h = self.get_terrain_z_height(p)
97 | pos = Point3((p.x, p.y, h))
98 | box_r = 1
99 | p0 = Point3((pos.x - box_r, pos.y - box_r, pos.z + box_r)) + Point2((0.5, 0.5))
100 | p1 = Point3((pos.x + box_r, pos.y + box_r, pos.z - box_r)) + Point2((0.5, 0.5))
101 | self.client.debug_box_out(p0, p1, color=color)
102 |
103 | async def on_step(self, iteration: int):
104 |
105 | pos = self.map_data.bot.townhalls.ready.first.position
106 | areas = self.map_data.where_all(pos)
107 | # logger.debug(areas) # uncomment this to get the areas of starting position
108 | region = areas[0]
109 | # logger.debug(region)
110 | # logger.debug(region.points)
111 | list_points = list(region.points)
112 | logger.debug(type(list_points)) # uncomment this to log the region points Type
113 | logger.debug(list_points) # uncomment this to log the region points
114 | hero = self.workers.by_tag(self.hero_tag)
115 | dist = 1.5 * hero.calculate_speed() * 1.4
116 | if self.target is None:
117 | self.target = self.path.pop(0)
118 | logger.info(f"Distance to next step : {self.map_data.distance(hero.position, self.target)}")
119 | if self.map_data.distance(hero.position, self.target) > 1:
120 | hero.move(self.target)
121 |
122 | if self.map_data.distance(hero.position, self.target) <= dist:
123 | if len(self.path) > 0:
124 | self.target = self.path.pop(0)
125 | else:
126 | logger.info("Path Complete")
127 | self._draw_path_box(p=self.target, color=RED) # draw scouting SCV next move point in the path
128 | self._draw_path_box(p=hero.position, color=GREEN) # draw scouting SCV position
129 | self.client.debug_text_world(
130 | "\n".join([f"{hero.position}", ]), hero.position, color=BLUE, size=30,
131 | )
132 |
133 | self.client.debug_text_world(
134 | "\n".join([f"start {self.p0}", ]), Point2(self.p0), color=BLUE, size=30,
135 | )
136 | self.client.debug_text_world(
137 | "\n".join([f"end {self.p1}", ]), Point2(self.p1), color=RED, size=30,
138 | )
139 |
140 | """
141 | Drawing Buildable points of our home base ( Region )
142 | Feel free to try lifting the Command Center, or building structures to see how it updates
143 | """
144 | self._draw_point_list(self.base.buildables.points, text='*')
145 |
146 | """
147 | Drawing the path for our scouting SCV from our base to enemy's Main
148 | """
149 | self._draw_point_list(self.path, text='*', color=RED)
150 |
151 |
152 | def main():
153 | map = "DeathAuraLE"
154 | sc2.run_game(
155 | sc2.maps.get(map),
156 | [Bot(sc2.Race.Terran, MATester()), Computer(sc2.Race.Zerg, sc2.Difficulty.VeryEasy)],
157 | realtime=True
158 | )
159 |
160 |
161 | if __name__ == "__main__":
162 | main()
163 |
--------------------------------------------------------------------------------
/ladder_build.sh:
--------------------------------------------------------------------------------
1 | docker build . -f ./ladder_build/Dockerfile -t maextbuild
2 | id=$(docker create maextbuild)
3 | docker cp $id:./mapanalyzerext.so ./MapAnalyzer/cext/mapanalyzerext.so
4 | docker rm -v $id
5 |
6 |
--------------------------------------------------------------------------------
/ladder_build/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.9.5
2 |
3 | COPY . .
4 |
5 | RUN python3.9 setup.py build_ext --inplace
6 | RUN mv mapanalyzerext.*.so mapanalyzerext.so
7 |
8 | CMD ["/bin/bash"]
9 |
--------------------------------------------------------------------------------
/monkeytest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | pytest.main()
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sc2mapanalyzer",
3 | "version": "0.0.87",
4 | "dependencies": {
5 | "update": "^0.7.4"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/pf_perf.py:
--------------------------------------------------------------------------------
1 | import lzma
2 | import os
3 | import pickle
4 | import random
5 | from typing import List
6 | from platform import python_version
7 | import time
8 | from MapAnalyzer.MapData import MapData
9 | from MapAnalyzer.utils import import_bot_instance
10 |
11 | def get_random_point(minx, maxx, miny, maxy):
12 | return (random.randint(minx, maxx), random.randint(miny, maxy))
13 |
14 |
15 | def get_map_file_list() -> List[str]:
16 | """
17 | easy way to produce less than all maps, for example if we want to test utils, we only need one MapData object
18 | """
19 | subfolder = "MapAnalyzer"
20 | subfolder2 = "pickle_gameinfo"
21 | subfolder = os.path.join(subfolder, subfolder2)
22 | folder = os.path.abspath(".")
23 | map_files_folder = os.path.join(folder, subfolder)
24 | map_files = os.listdir(map_files_folder)
25 | li = []
26 | for map_file in map_files:
27 | li.append(os.path.join(map_files_folder, map_file))
28 | return li
29 |
30 |
31 | map_files = get_map_file_list()
32 | map_file = ""
33 | for mf in map_files:
34 | if 'goldenwall' in mf.lower():
35 | map_file = mf
36 | break
37 |
38 | with lzma.open(map_file, "rb") as f:
39 | raw_game_data, raw_game_info, raw_observation = pickle.load(f)
40 |
41 | bot = import_bot_instance(raw_game_data, raw_game_info, raw_observation)
42 | map_data = MapData(bot, loglevel="DEBUG")
43 |
44 | base = map_data.bot.townhalls[0]
45 | reg_start = map_data.where(base.position_tuple)
46 | reg_end = map_data.where(map_data.bot.enemy_start_locations[0].position)
47 | p0 = reg_start.center
48 | p1 = reg_end.center
49 | pts = []
50 | r = 10
51 | for i in range(50):
52 | pts.append(get_random_point(0, 200, 0, 200))
53 |
54 | arr = map_data.get_pyastar_grid(100)
55 | for p in pts:
56 | arr = map_data.add_cost(p, r, arr)
57 |
58 | start = time.perf_counter()
59 | path2 = map_data.pathfind(p0, p1, grid=arr)
60 | ext_time = time.perf_counter() - start
61 | print("extension astar time: {}".format(ext_time))
62 |
63 | start = time.perf_counter()
64 | nydus_path = map_data.pathfind_with_nyduses(p0, p1, grid=arr)
65 | nydus_time = time.perf_counter() - start
66 | print("nydus astar time: {}".format(nydus_time))
67 | print("compare to without nydus: {}".format(nydus_time / ext_time))
68 |
69 | map_data.plot_influenced_path(start=p0, goal=p1, weight_array=arr)
70 | map_data.plot_influenced_path_nydus(start=p0, goal=p1, weight_array=arr)
71 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | log_cli = True
3 | log_cli_level = INFO
4 | addopts = -p no:warnings --cov=MapAnalyzer --hypothesis-show-statistics --durations=0 --doctest-modules
5 |
--------------------------------------------------------------------------------
/requirements.dev.txt:
--------------------------------------------------------------------------------
1 | -r requirements.txt
2 | pytest
3 | pytest-html
4 | monkeytype
5 | mypy
6 | mpyq
7 | pytest-asyncio
8 | hypothesis
9 | pytest-benchmark
10 | sphinx
11 | sphinx-autodoc-typehints
12 | pytest-cov
13 | coverage
14 | codecov
15 | mutmut
16 | radon
--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------
1 | import lzma
2 | import os
3 | import pickle
4 | import random
5 | from typing import List
6 |
7 | from sc2.position import Point2
8 | import matplotlib.pyplot as plt
9 | from MapAnalyzer.MapData import MapData
10 | from MapAnalyzer.utils import import_bot_instance
11 |
12 |
13 | def get_random_point(minx, maxx, miny, maxy):
14 | return (random.randint(minx, maxx), random.randint(miny, maxy))
15 |
16 |
17 | def _get_random_influence(n, r):
18 | pts = []
19 | for i in range(n):
20 | pts.append(
21 | (Point2(get_random_point(50, 130, 25, 175)), r))
22 | return pts
23 | def get_map_file_list() -> List[str]:
24 | """
25 | easy way to produce less than all maps, for example if we want to test utils, we only need one MapData object
26 | """
27 | subfolder = "MapAnalyzer"
28 | subfolder2 = "pickle_gameinfo"
29 | subfolder = os.path.join(subfolder, subfolder2)
30 | folder = os.path.abspath(".")
31 | map_files_folder = os.path.join(folder, subfolder)
32 | map_files = os.listdir(map_files_folder)
33 | li = []
34 | for map_file in map_files:
35 | li.append(os.path.join(map_files_folder, map_file))
36 | return li
37 |
38 |
39 | map_files = get_map_file_list()
40 | for mf in map_files:
41 | if 'abys' in mf.lower():
42 | # if 1==1:
43 | # mf = random.choice(map_files)
44 | # if 'abys' in mf.lower():
45 | with lzma.open(mf, "rb") as f:
46 | raw_game_data, raw_game_info, raw_observation = pickle.load(f)
47 | bot = import_bot_instance(raw_game_data, raw_game_info, raw_observation)
48 | map_data = MapData(bot, loglevel="DEBUG")
49 | # base = map_data.bot.townhalls[0]
50 | # reg_start = map_data.where_all(base.position_tuple)[0]
51 | #
52 | # for choke in map_data.map_chokes:
53 | # x, y = zip(*choke.corner_walloff)
54 | # plt.scatter(x, y)
55 | #
56 | # reg_end = map_data.where_all(map_data.bot.enemy_start_locations[0].position)[0]
57 | # # p0 = Point2(reg_start.center)
58 | # # p1 = Point2(reg_end.center)
59 | # p0 = Point2((104, 153))
60 | # p1 = Point2((107, 140))
61 | # # influence_grid = map_data.get_clean_air_grid(default_weight=10)
62 | # influence_grid = map_data.get_pyastar_grid()
63 | # cost_point = (50, 130)
64 | # # cost_point = (107, 140)
65 | # influence_grid = map_data.add_cost(position=cost_point, radius=15, grid=influence_grid)
66 | # # cost_point = (107, 140)
67 | # influence_grid = map_data.add_cost(position=cost_point, radius=7, grid=influence_grid)
68 | # cost_point = (107, 140)
69 | # influence_grid = map_data.add_cost(position=cost_point, radius=17, grid=influence_grid)
70 | # # safe_points = map_data.find_lowest_cost_points(from_pos=cost_point, radius=14, grid=influence_grid)
71 | #
72 | # # logger.info(safe_points)
73 | #
74 | # # x, y = zip(*safe_points)
75 | # # plt.scatter(x, y, s=1)
76 | # path = map_data.pathfind(start=p0, goal=p1, grid=influence_grid, allow_diagonal=True)
77 | # for p in path:
78 | # assert (p)
79 | # from loguru import logger
80 | #
81 | # logger.info(len(path))
82 | # map_data.plot_influenced_path(start=p0, goal=p1, weight_array=influence_grid, allow_diagonal=True)
83 | # # map_data.save(filename=f"{mf}")
84 | # # # plt.close()
85 | # map_data.show()
86 | break
87 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from setuptools import setup, Extension
4 | from setuptools.command.build_ext import build_ext as _build_ext
5 |
6 | # https://stackoverflow.com/a/21621689/
7 | class build_ext(_build_ext):
8 | def finalize_options(self):
9 | _build_ext.finalize_options(self)
10 | # Prevent numpy from thinking it is still in its setup process:
11 | __builtins__.__NUMPY_SETUP__ = False
12 | import numpy
13 | self.include_dirs.append(numpy.get_include())
14 |
15 |
16 | mapping_module = Extension(
17 | 'mapanalyzerext', sources=['MapAnalyzer/cext/src/ma_ext.c'], extra_compile_args=["-DNDEBUG", "-O2"]
18 | )
19 | logger = logging.getLogger(__name__)
20 |
21 | requirements = [ # pragma: no cover
22 | "wheel",
23 | "numpy~=1.19.2",
24 | "Cython",
25 | "burnysc2",
26 | "matplotlib",
27 | "scipy",
28 | "loguru",
29 | "tqdm",
30 | "scikit-image",
31 | ]
32 | setup( # pragma: no cover
33 | name="sc2mapanalyzer",
34 | # version=f"{__version__}",
35 | version="0.0.87",
36 | install_requires=requirements,
37 | setup_requires=["wheel", "numpy~=1.19.2"],
38 | cmdclass={"build_ext": build_ext},
39 | ext_modules=[mapping_module],
40 | packages=["MapAnalyzer", "MapAnalyzer.cext"],
41 | extras_require={
42 | "dev": [
43 | "pytest",
44 | "pytest-html",
45 | "monkeytype",
46 | "mypy",
47 | "mpyq",
48 | "pytest-asyncio",
49 | "hypothesis",
50 | "pytest-benchmark",
51 | "sphinx",
52 | "sphinx-autodoc-typehints",
53 | "pytest-cov",
54 | "coverage",
55 | "codecov",
56 | "mutmut",
57 | "radon",
58 | ]
59 | },
60 | )
61 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eladyaniv01/SC2MapAnalysis/dbd2bdaaadcb41172e525ef08cef5a8b2fc103ec/tests/__init__.py
--------------------------------------------------------------------------------
/tests/mocksetup.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import random
4 | from random import randint
5 | from typing import Tuple, Iterable, List
6 |
7 | import pytest
8 | import tqdm
9 | from _pytest.python import Metafunc
10 | from hypothesis import given, settings, strategies as st
11 | from loguru import logger
12 |
13 | from MapAnalyzer.MapData import MapData
14 | from MapAnalyzer.Region import Region
15 | from MapAnalyzer.Polygon import Polygon
16 | from MapAnalyzer.constructs import ChokeArea, MDRamp, VisionBlockerArea
17 | from MapAnalyzer.utils import mock_map_data
18 | from _pytest.logging import caplog as _caplog
19 |
20 |
21 | # for merging pr from forks, git push :
22 | # pytest -v --disable-warnings
23 | # mutmut run --paths-to-mutate test_suite.py --runner pytest
24 | # radon cc . -a -nb (will dump only complexity score of B and below)
25 | # monkeytype run monkeytest.py
26 | # monkeytype list-modules
27 | # mutmut run --paths-to-mutate MapAnalyzer/MapData.py
28 |
29 | def get_random_point(minx: int, maxx: int, miny: int, maxy: int) -> Tuple[int, int]:
30 | return random.randint(minx, maxx), random.randint(miny, maxy)
31 |
32 |
33 | @pytest.fixture
34 | def caplog(_caplog=_caplog):
35 | class PropogateHandler(logging.Handler):
36 | def emit(self, record):
37 | logging.getLogger(record.name).handle(record)
38 |
39 | handler_id = logger.add(PropogateHandler(), format="{message}")
40 | yield _caplog
41 | logger.remove(handler_id)
42 |
43 |
44 | def get_map_datas() -> Iterable[MapData]:
45 | subfolder = "MapAnalyzer"
46 | subfolder2 = "pickle_gameinfo"
47 | subfolder = os.path.join(subfolder, subfolder2)
48 | if "tests" in os.path.abspath("."):
49 | folder = os.path.dirname(os.path.abspath("."))
50 | else:
51 | folder = os.path.abspath(".")
52 | map_files_folder = os.path.join(folder, subfolder)
53 | map_files = os.listdir(map_files_folder)
54 | for map_file in map_files:
55 | # EphemeronLE has a ramp with 3 regions which causes a test failure
56 | # https://github.com/eladyaniv01/SC2MapAnalysis/issues/110
57 | if 'ephemeron' not in map_file.lower():
58 | yield mock_map_data(map_file=os.path.join(map_files_folder, map_file))
59 |
--------------------------------------------------------------------------------
/tests/monkeytest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | pytest.main()
4 |
--------------------------------------------------------------------------------
/tests/pathing_grid.txt:
--------------------------------------------------------------------------------
1 | 000000000000000000000000000000000000000000
2 | 000000000000000000000000000000000000000000
3 | 000000000000000000000000000000000000000000
4 | 000111111111111111111111111111111111111000
5 | 000111111111111111111111111111111111111000
6 | 000111111111111111111111111111111111111000
7 | 000111111111111111111111111111111111111000
8 | 000111111111111111111111111111111111111000
9 | 000111111111111111111111111111111111111000
10 | 000111111111111111111111111111111111111000
11 | 000111111111111111111111111111111111111000
12 | 000111111111111111111111111111111111111000
13 | 000111111111000000000000000000000000000000
14 | 000111111111000000000000000000000000000000
15 | 000111111111000000000000000000000000000000
16 | 000111111111000000000000000000000000000000
17 | 000111111111000000000000000000000000000000
18 | 000111111111000000000000000000000000000000
19 | 000111111111000000000000000000000000000000
20 | 000111111111000000000000000000000000000000
21 | 000111111111000000000000000000000000000000
22 | 000111111111000000000000000000000000000000
23 | 000111111111000000000000000000000000000000
24 | 000111111111111111111111111111111111111000
25 | 000111111111111111111111111111111111111000
26 | 000111111111000000000000000000000000011000
27 | 000111111111000000000000000000000000011000
28 | 000111111111000000000000000000000000011000
29 | 000111111111000000000000000000000000011000
30 | 000111111111000000000000000000000000011000
31 | 000111111111000000000000000000000000011000
32 | 000111111111111111111111111111111111111000
33 | 000111111111111111111111111111111111111000
34 | 000111111111111111111111111111111111111000
35 | 000111111111111111111111111111111111111000
36 | 000111111111111111111111111111111111111000
37 | 000000000000000000000000000000000000000000
38 | 000000000000000000000000000000000000000000
39 | 000000000000000000000000000000000000000000
--------------------------------------------------------------------------------
/tests/test_c_extension.py:
--------------------------------------------------------------------------------
1 | from MapAnalyzer.cext import CMapInfo, astar_path, astar_path_with_nyduses
2 | import numpy as np
3 | from sc2.position import Rect, Point2
4 | import os
5 |
6 |
7 | def load_pathing_grid(file_name):
8 | file = open(file_name, 'r')
9 | lines = file.readlines()
10 |
11 | h = len(lines)
12 | w = len(lines[0]) - 1
13 |
14 | res = np.zeros((h, w))
15 | y = 0
16 | for line in lines:
17 | x = 0
18 | for char in line:
19 | if char == '\n':
20 | continue
21 | num = int(char)
22 | if num == 1:
23 | res[y, x] = 1
24 | x += 1
25 | y += 1
26 | file.close()
27 |
28 | return res.astype(np.uint8)
29 |
30 |
31 | def test_c_extension():
32 | script_dir = os.path.dirname(__file__)
33 | abs_file_path = os.path.join(script_dir, "pathing_grid.txt")
34 | walkable_grid = load_pathing_grid(abs_file_path)
35 |
36 | pathing_grid = np.where(walkable_grid == 0, np.inf, walkable_grid).astype(np.float32)
37 | path = astar_path(pathing_grid, (3, 3), (33, 38), False, False)
38 | assert(path is not None and path.shape[0] == 56)
39 |
40 | influenced_grid = pathing_grid.copy()
41 | influenced_grid[21:28, 5:20] = 100
42 | path2 = astar_path(influenced_grid, (3, 3), (33, 38), False, False)
43 |
44 | assert(path2 is not None and path2.shape[0] == 59)
45 |
46 | paths_no_nydus = astar_path_with_nyduses(influenced_grid, (3, 3), (33, 38), [], False, False)
47 |
48 | assert(paths_no_nydus is not None and paths_no_nydus[0].shape[0] == 59)
49 |
50 | nydus_positions = [
51 | Point2((6.5, 6.5)),
52 | Point2((29.5, 34.5))
53 | ]
54 |
55 | influenced_grid[5:8, 5:8] = np.inf
56 | influenced_grid[28:31, 33:36] = np.inf
57 |
58 | paths_nydus = astar_path_with_nyduses(influenced_grid, (3, 3), (33, 38), nydus_positions, False, False)
59 |
60 | assert (paths_nydus is not None and len(paths_nydus) == 2
61 | and len(paths_nydus[0]) + len(paths_nydus[1]) == 7)
62 |
63 | height_map = np.where(walkable_grid == 0, 24, 8).astype(np.uint8)
64 |
65 | playable_area = Rect([1, 1, 38, 38])
66 | map_info = CMapInfo(walkable_grid, height_map, playable_area, "CExtensionTest")
67 | assert(len(map_info.overlord_spots) == 2)
68 | assert(len(map_info.chokes) == 5)
69 |
70 | # testing that the main line actually exists, was a previous bug
71 | for choke in map_info.chokes:
72 | assert(choke.main_line[0] != choke.main_line[1])
73 |
74 |
--------------------------------------------------------------------------------
/tests/test_docs.py:
--------------------------------------------------------------------------------
1 | from .mocksetup import *
2 | from MapAnalyzer.utils import get_map_files_folder
3 | from MapAnalyzer.settings import ROOT_DIR
4 | import doctest
5 |
6 | goldenwall = os.path.join(get_map_files_folder(), 'GoldenWallLE.xz')
7 | map_data = mock_map_data(goldenwall)
8 |
9 |
10 | """
11 | uncomment and run the function below to test doc strings without running the entire test suite
12 | """
13 |
14 | # def test_docstrings() -> None:
15 | # test_files = []
16 | # for root, dirs, files in os.walk(ROOT_DIR):
17 | # for f in files:
18 | # if f.endswith('.py'):
19 | # test_files.append(os.path.join(root, f))
20 | # for f in test_files:
21 | # print(f)
22 | # doctest.testfile(f"{f}", extraglobs={'self': map_data}, verbose=True)
23 |
--------------------------------------------------------------------------------
/tests/test_pathihng.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import numpy as np
4 | from _pytest.logging import LogCaptureFixture
5 | from _pytest.python import Metafunc
6 | from sc2.position import Point2
7 |
8 | from MapAnalyzer import Region
9 | from MapAnalyzer.destructibles import *
10 | from MapAnalyzer.MapData import MapData
11 | from MapAnalyzer.utils import get_map_file_list, get_map_files_folder, mock_map_data
12 | from tests.mocksetup import get_map_datas, get_random_point, logger
13 |
14 | logger = logger
15 |
16 |
17 | # From https://docs.pytest.org/en/latest/example/parametrize.html#a-quick-port-of-testscenarios
18 | def pytest_generate_tests(metafunc: Metafunc) -> None:
19 | # noinspection PyGlobalUndefined
20 | global argnames
21 | idlist = []
22 | argvalues = []
23 | if metafunc.cls is not None:
24 | for scenario in metafunc.cls.scenarios:
25 | idlist.append(scenario[0])
26 | items = scenario[1].items()
27 | argnames = [x[0] for x in items]
28 | argvalues.append(([x[1] for x in items]))
29 | metafunc.parametrize(argnames, argvalues, ids=idlist, scope="class")
30 |
31 |
32 | def test_destructable_types() -> None:
33 | map_list = get_map_file_list()
34 | dest_types = set()
35 | for map in map_list:
36 | map_data = mock_map_data(map)
37 | for dest in map_data.bot.destructables:
38 | dest_types.add((dest.type_id, dest.name))
39 |
40 | rock_types = set()
41 | rock_types.update(destructable_ULBR)
42 | rock_types.update(destructable_BLUR)
43 | rock_types.update(destructable_6x2)
44 | rock_types.update(destructable_4x4)
45 | rock_types.update(destructable_2x4)
46 | rock_types.update(destructable_2x2)
47 | rock_types.update(destructable_2x6)
48 | rock_types.update(destructable_4x2)
49 | rock_types.update(destructable_4x12)
50 | rock_types.update(destructable_6x6)
51 | rock_types.update(destructable_12x4)
52 |
53 | for dest in dest_types:
54 | handled = False
55 | type_id = dest[0]
56 | name = dest[1].lower()
57 | if 'mineralfield450' in name:
58 | handled = True
59 | elif 'unbuildable' in name:
60 | handled = True
61 | elif 'acceleration' in name:
62 | handled = True
63 | elif 'inhibitor' in name:
64 | handled = True
65 | elif "dog" in name:
66 | handled = True
67 | elif "cleaningbot" in name:
68 | handled = True
69 | elif type_id in rock_types:
70 | handled = True
71 |
72 | assert handled, f"Destructable {type_id} with name {name} is not handled"
73 |
74 |
75 | def test_climber_grid() -> None:
76 | """assert that we can path through climb cells with climber grid,
77 | but not with normal grid"""
78 | path = os.path.join(get_map_files_folder(), 'GoldenWallLE.xz')
79 |
80 | map_data = mock_map_data(path)
81 | start = (150, 95)
82 | goal = (110, 40)
83 | grid = map_data.get_pyastar_grid()
84 | path = map_data.pathfind(start=start, goal=goal, grid=grid)
85 | assert (path is None)
86 | grid = map_data.get_climber_grid()
87 | path = map_data.pathfind(start=start, goal=goal, grid=grid)
88 | assert (path is None)
89 |
90 |
91 | def test_minerals_walls() -> None:
92 | # attempting to path through mineral walls in goldenwall should fail
93 | path = os.path.join(get_map_files_folder(), 'GoldenWallLE.xz')
94 | # logger.info(path)
95 | map_data = mock_map_data(path)
96 | start = (110, 95)
97 | goal = (110, 40)
98 | grid = map_data.get_pyastar_grid()
99 | path = map_data.pathfind(start=start, goal=goal, grid=grid)
100 | assert (path is None)
101 | # also test climber grid for nonpathables
102 | grid = map_data.get_climber_grid()
103 | path = map_data.pathfind(start=start, goal=goal, grid=grid)
104 | assert (path is None)
105 |
106 | # remove the mineral wall that is blocking pathing from the left player's base to the bottom
107 | # side of the map
108 | map_data.bot.destructables = map_data.bot.destructables.filter(lambda x: x.distance_to((46, 41)) > 5)
109 | grid = map_data.get_pyastar_grid()
110 | path = map_data.pathfind(start=start, goal=goal, grid=grid)
111 | assert (path is not None)
112 |
113 | # attempting to path through tight pathways near destructables should work
114 | path = os.path.join(get_map_files_folder(), 'AbyssalReefLE.xz')
115 | map_data = mock_map_data(path)
116 | start = (130, 25)
117 | goal = (125, 47)
118 | grid = map_data.get_pyastar_grid()
119 | path = map_data.pathfind(start=start, goal=goal, grid=grid)
120 | assert (path is not None)
121 |
122 |
123 | class TestPathing:
124 | """
125 | Test DocString
126 | """
127 | scenarios = [(f"Testing {md.bot.game_info.map_name}", {"map_data": md}) for md in get_map_datas()]
128 |
129 | def test_region_connectivity(self, map_data: MapData) -> None:
130 | base = map_data.bot.townhalls[0]
131 | region = map_data.where_all(base.position_tuple)[0]
132 | destination = map_data.where_all(map_data.bot.enemy_start_locations[0].position)[0]
133 | all_possible_paths = map_data.region_connectivity_all_paths(start_region=region,
134 | goal_region=destination)
135 | for p in all_possible_paths:
136 | assert (destination in p), f"destination = {destination}"
137 |
138 | bad_request = map_data.region_connectivity_all_paths(start_region=region,
139 | goal_region=destination,
140 | not_through=[destination])
141 | assert (bad_request == [])
142 |
143 | def test_handle_out_of_bounds_values(self, map_data: MapData) -> None:
144 | base = map_data.bot.townhalls[0]
145 | reg_start = map_data.where_all(base.position_tuple)[0]
146 | assert (isinstance(reg_start,
147 | Region)), f"reg_start = {reg_start}, base = {base}, position_tuple = {base.position_tuple}"
148 | reg_end = map_data.where_all(map_data.bot.enemy_start_locations[0].position)[0]
149 | p0 = reg_start.center
150 | p1 = reg_end.center
151 | pts = []
152 | r = 10
153 | for i in range(50):
154 | pts.append(get_random_point(-500, -250, -500, -250))
155 |
156 | arr = map_data.get_pyastar_grid()
157 | for p in pts:
158 | arr = map_data.add_cost(p, r, arr)
159 | path = map_data.pathfind(p0, p1, grid=arr)
160 | assert (path is not None), f"path = {path}"
161 |
162 | def test_handle_illegal_weights(self, map_data: MapData) -> None:
163 | base = map_data.bot.townhalls[0]
164 | reg_start = map_data.where_all(base.position_tuple)[0]
165 | assert (isinstance(reg_start,
166 | Region)), f"reg_start = {reg_start}, base = {base}, position_tuple = {base.position_tuple}"
167 | reg_end = map_data.where_all(map_data.bot.enemy_start_locations[0].position)[0]
168 | p0 = reg_start.center
169 | p1 = reg_end.center
170 | pts = []
171 | r = 10
172 | for i in range(10):
173 | pts.append(get_random_point(20, 180, 20, 180))
174 |
175 | arr = map_data.get_pyastar_grid()
176 | for p in pts:
177 | arr = map_data.add_cost(p, r, arr, weight=-100)
178 | path = map_data.pathfind(p0, p1, grid=arr)
179 | assert (path is not None), f"path = {path}"
180 |
181 | def test_find_lowest_cost_points(self, map_data: MapData) -> None:
182 | cr = 7
183 | safe_query_radius = 14
184 | expected_max_distance = 2 * safe_query_radius
185 |
186 | influence_grid = map_data.get_air_vs_ground_grid()
187 | cost_point = (50, 130)
188 | influence_grid = map_data.add_cost(position=cost_point, radius=cr, grid=influence_grid)
189 | safe_points = map_data.find_lowest_cost_points(from_pos=cost_point, radius=safe_query_radius,
190 | grid=influence_grid)
191 | assert (
192 | safe_points[0][0],
193 | np.integer), f"safe_points[0][0] = {safe_points[0][0]}, type {type(safe_points[0][0])}"
194 | assert isinstance(safe_points[0][1],
195 | np.integer), f"safe_points[0][1] = {safe_points[0][1]}, type {type(safe_points[0][1])}"
196 | cost = influence_grid[safe_points[0]]
197 | for p in safe_points:
198 | assert (influence_grid[
199 | p] == cost), f"grid type = air_vs_ground_grid, p = {p}, " \
200 | f"influence_grid[p] = {influence_grid[p]}, expected cost = {cost}"
201 | assert (map_data.distance(cost_point, p) < expected_max_distance)
202 |
203 | influence_grid = map_data.get_clean_air_grid()
204 | cost_point = (50, 130)
205 | influence_grid = map_data.add_cost(position=cost_point, radius=cr, grid=influence_grid)
206 | safe_points = map_data.find_lowest_cost_points(from_pos=cost_point, radius=safe_query_radius,
207 | grid=influence_grid)
208 | cost = influence_grid[safe_points[0]]
209 | for p in safe_points:
210 | assert (influence_grid[
211 | p] == cost), f"grid type = clean_air_grid, p = {p}, " \
212 | f"influence_grid[p] = {influence_grid[p]}, expected cost = {cost}"
213 | assert (map_data.distance(cost_point, p) < expected_max_distance)
214 |
215 | influence_grid = map_data.get_pyastar_grid()
216 | cost_point = (50, 130)
217 | influence_grid = map_data.add_cost(position=cost_point, radius=cr, grid=influence_grid)
218 | safe_points = map_data.find_lowest_cost_points(from_pos=cost_point, radius=safe_query_radius,
219 | grid=influence_grid)
220 | cost = influence_grid[safe_points[0]]
221 | for p in safe_points:
222 | assert (influence_grid[
223 | p] == cost), f"grid type = pyastar_grid, p = {p}, " \
224 | f"influence_grid[p] = {influence_grid[p]}, expected cost = {cost}"
225 | assert (map_data.distance(cost_point, p) < expected_max_distance)
226 |
227 | influence_grid = map_data.get_climber_grid()
228 | cost_point = (50, 130)
229 | influence_grid = map_data.add_cost(position=cost_point, radius=cr, grid=influence_grid)
230 | safe_points = map_data.find_lowest_cost_points(from_pos=cost_point, radius=safe_query_radius,
231 | grid=influence_grid)
232 | cost = influence_grid[safe_points[0]]
233 | for p in safe_points:
234 | assert (influence_grid[
235 | p] == cost), f"grid type = climber_grid, p = {p}, " \
236 | f"influence_grid[p] = {influence_grid[p]}, expected cost = {cost}"
237 | assert (map_data.distance(cost_point, p) < expected_max_distance)
238 |
239 | def test_clean_air_grid_smoothing(self, map_data: MapData) -> None:
240 | default_weight = 2
241 | base = map_data.bot.townhalls[0]
242 | reg_start = map_data.where_all(base.position_tuple)[0]
243 | reg_end = map_data.where_all(map_data.bot.enemy_start_locations[0].position)[0]
244 | p0 = Point2(reg_start.center)
245 | p1 = Point2(reg_end.center)
246 | grid = map_data.get_clean_air_grid(default_weight=default_weight)
247 | cost_points = [(87, 76), (108, 64), (97, 53)]
248 | cost_points = list(map(Point2, cost_points))
249 | for cost_point in cost_points:
250 | grid = map_data.add_cost(position=cost_point, radius=7, grid=grid)
251 | path = map_data.pathfind(start=p0, goal=p1, grid=grid, smoothing=True)
252 | assert (len(path) < 50)
253 |
254 | def test_clean_air_grid_no_smoothing(self, map_data: MapData) -> None:
255 | """
256 | non diagonal path should be longer, but still below 250
257 | """
258 | default_weight = 2
259 | base = map_data.bot.townhalls[0]
260 | reg_start = map_data.where_all(base.position_tuple)[0]
261 | reg_end = map_data.where_all(map_data.bot.enemy_start_locations[0].position)[0]
262 | p0 = Point2(reg_start.center)
263 | p1 = Point2(reg_end.center)
264 | grid = map_data.get_clean_air_grid(default_weight=default_weight)
265 | cost_points = [(87, 76), (108, 64), (97, 53)]
266 | cost_points = list(map(Point2, cost_points))
267 | for cost_point in cost_points:
268 | grid = map_data.add_cost(position=cost_point, radius=7, grid=grid)
269 | path = map_data.pathfind(start=p0, goal=p1, grid=grid, smoothing=False)
270 | assert (len(path) < 250)
271 |
272 | def test_air_vs_ground(self, map_data: MapData) -> None:
273 | default_weight = 99
274 | grid = map_data.get_air_vs_ground_grid(default_weight=default_weight)
275 | ramps = map_data.map_ramps
276 | path_array = map_data.pather.default_grid
277 | for ramp in ramps:
278 | for point in ramp.points:
279 | if path_array[point.x][point.y] == 1:
280 | assert (grid[point.x][point.y] == default_weight), f"point {point}"
281 |
282 | def test_sensitivity(self, map_data: MapData) -> None:
283 | base = map_data.bot.townhalls[0]
284 | reg_start = map_data.where_all(base.position_tuple)[0]
285 | reg_end = map_data.where_all(map_data.bot.enemy_start_locations[0].position)[0]
286 | p0 = reg_start.center
287 | p1 = reg_end.center
288 | arr = map_data.get_pyastar_grid()
289 | path_pure = map_data.pathfind(p0, p1, grid=arr)
290 | path_sensitive_5 = map_data.pathfind(p0, p1, grid=arr, sensitivity=5)
291 | path_sensitive_1 = map_data.pathfind(p0, p1, grid=arr, sensitivity=1)
292 | assert (len(path_sensitive_5) < len(path_pure))
293 | assert (p in path_pure for p in path_sensitive_5)
294 | assert (path_sensitive_1 == path_pure)
295 |
296 | def test_pathing_influence(self, map_data: MapData, caplog: LogCaptureFixture) -> None:
297 | logger.info(map_data)
298 | base = map_data.bot.townhalls[0]
299 | reg_start = map_data.where_all(base.position_tuple)[0]
300 | reg_end = map_data.where_all(map_data.bot.enemy_start_locations[0].position)[0]
301 | p0 = reg_start.center
302 | p1 = reg_end.center
303 | pts = []
304 | r = 10
305 | for i in range(50):
306 | pts.append(get_random_point(0, 200, 0, 200))
307 |
308 | arr = map_data.get_pyastar_grid()
309 | for p in pts:
310 | arr = map_data.add_cost(p, r, arr)
311 | path = map_data.pathfind(p0, p1, grid=arr)
312 | assert (path is not None)
313 |
--------------------------------------------------------------------------------
/tests/test_suite.py:
--------------------------------------------------------------------------------
1 | from random import randint
2 |
3 | from _pytest.python import Metafunc
4 | from hypothesis import given, settings
5 | from loguru import logger
6 | from sc2.position import Point2
7 |
8 | from MapAnalyzer.MapData import MapData
9 | from MapAnalyzer.utils import get_map_file_list, mock_map_data
10 | from tests.mocksetup import get_map_datas, random, st
11 |
12 |
13 | # From https://docs.pytest.org/en/latest/example/parametrize.html#a-quick-port-of-testscenarios
14 | def pytest_generate_tests(metafunc: Metafunc) -> None:
15 | # noinspection PyGlobalUndefined
16 | global argnames
17 | idlist = []
18 | argvalues = []
19 | if metafunc.cls is not None:
20 | for scenario in metafunc.cls.scenarios:
21 | idlist.append(scenario[0])
22 | items = scenario[1].items()
23 | argnames = [x[0] for x in items]
24 | argvalues.append(([x[1] for x in items]))
25 | metafunc.parametrize(argnames, argvalues, ids=idlist, scope="class")
26 |
27 |
28 | @given(st.integers(min_value=1, max_value=100), st.integers(min_value=1, max_value=100))
29 | @settings(max_examples=5, deadline=None, verbosity=3, print_blob=True)
30 | def test_mapdata(n, m):
31 | map_files = get_map_file_list()
32 | map_data = mock_map_data(random.choice(map_files))
33 | # map_data.plot_map()
34 | logger.info(f"Loaded Map : {map_data.bot.game_info.map_name}, n,m = {n}, {m}")
35 | # tuples
36 | points = [(i, j) for i in range(n + 1) for j in range(m + 1)]
37 | set_points = set(points)
38 | indices = map_data.points_to_indices(set_points)
39 | i = randint(0, n)
40 | j = randint(0, m)
41 | assert (i, j) in points
42 | assert (i, j) in set_points
43 | assert i in indices[0] and j in indices[1]
44 | new_points = map_data.indices_to_points(indices)
45 | assert new_points == set_points
46 |
47 | # Point2's
48 | points = [Point2((i, j)) for i in range(n + 1) for j in range(m + 1)]
49 |
50 | for point in points:
51 | assert (point is not None)
52 | set_points = set(points)
53 | indices = map_data.points_to_indices(set_points)
54 | i = randint(0, n)
55 | j = randint(0, m)
56 | assert (i, j) in points
57 | assert (i, j) in set_points
58 | assert i in indices[0] and j in indices[1]
59 | new_points = map_data.indices_to_points(indices)
60 | assert new_points == set_points
61 |
62 |
63 | class TestSanity:
64 | """
65 | Test DocString
66 | """
67 | scenarios = [(f"Testing {md.bot.game_info.map_name}", {"map_data": md}) for md in get_map_datas()]
68 |
69 | def test_polygon(self, map_data: MapData) -> None:
70 | for polygon in map_data.polygons:
71 | polygon.plot(testing=True)
72 | assert (polygon not in polygon.areas)
73 | assert (polygon.nodes == list(polygon.points))
74 | assert (polygon.width > 0)
75 | assert (polygon.area > 0)
76 | assert (polygon.is_inside_point(polygon.center))
77 |
78 | extended_pts = polygon.points.union(polygon.perimeter_points)
79 | assert (polygon.points == extended_pts)
80 |
81 | for point in extended_pts:
82 | assert (polygon.is_inside_point(point) is True)
83 |
84 | # https://github.com/BurnySc2/python-sc2/issues/62
85 | assert isinstance(point, Point2)
86 | assert (type(point[0] == int))
87 |
88 | for point in polygon.corner_points:
89 | assert (point in polygon.corner_array)
90 | assert (polygon.buildables.free_pct is not None)
91 |
92 | def test_regions(self, map_data: MapData) -> None:
93 | for region in map_data.regions.values():
94 | for p in region.points:
95 | assert (region in map_data.where_all(p)), f"expected {region}, got {map_data.where_all(p)}, point {p}"
96 | assert (region == map_data.where(region.center))
97 |
98 | # coverage
99 | region.plot(testing=True)
100 |
101 | def test_ramps(self, map_data: MapData) -> None:
102 | for ramp in map_data.map_ramps:
103 | # on some maps the ramp may be hit a region edge and ends up connecting to 3 regions
104 | # could think about whether this is desirable or if we should select the 2 main regions
105 | # the ramp is connecting
106 | assert (len(ramp.regions) == 2 or len(ramp.regions) == 3), f"ramp = {ramp}"
107 |
108 | def test_chokes(self, map_data: MapData) -> None:
109 | for choke in map_data.map_chokes:
110 | for p in choke.points:
111 | assert (choke in map_data.where_all(p)), \
112 | logger.error(f"