├── .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 | * ![build](https://github.com/eladyaniv01/SC2MapAnalysis/workflows/Build/badge.svg?branch=master) 5 | [master](https://github.com/eladyaniv01/SC2MapAnalysis/tree/master) 6 | 7 | * ![](https://img.shields.io/github/package-json/v/eladyaniv01/SC2MapAnalysis?color=blue&logo=EladYaniv01&style=plastic) [Changelog](https://github.com/eladyaniv01/SC2MapAnalysis/blob/master/CHANGELOG.md) 8 | 9 | * ![](https://img.shields.io/badge/Documentation-latest-green?style=plastic&logo=appveyor) 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"") 114 | assert (choke.side_a in choke.points), f"Choke {choke}, side a {choke.side_a} is not in choke points" 115 | assert (choke.side_b in choke.points), f"Choke {choke}, side b {choke.side_b} is not in choke points" 116 | 117 | def test_vision_blockers(self, map_data: MapData) -> None: 118 | all_chokes = map_data.map_chokes 119 | for vb in map_data.map_vision_blockers: 120 | assert (vb in all_chokes) 121 | for p in vb.points: 122 | assert (vb in map_data.where_all(p)), \ 123 | logger.error(f"") 125 | -------------------------------------------------------------------------------- /vb.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import subprocess 4 | from pathlib import Path 5 | 6 | __author__ = "Elad Yaniv" 7 | 8 | import click 9 | 10 | VERSION_REGEX = r"version=\"(\d*[.]\d*[.]\d*)" 11 | 12 | @click.group(help='Version Bump CLI') 13 | def vb(): 14 | pass 15 | 16 | 17 | def update_readme_to_sphinx(): 18 | import re 19 | 20 | regex = r"([#]\s)([A-Z]\w+)(\s?\n)" 21 | subst = "\\2\\n---------------\\n" 22 | with open("README.md", 'r') as f: 23 | r_parsed = f.read() 24 | title = "# QuickWalkThrough\n============" 25 | r_parsed = r_parsed.replace("# SC2MapAnalysis", title) 26 | r_result = re.sub(regex, subst, r_parsed, 0, re.MULTILINE) 27 | with open("README.md", 'w') as f: 28 | f.write(r_result) 29 | 30 | 31 | def parse_setup(): 32 | with open("setup.py", 'r') as f: 33 | setup_parsed = f.read() 34 | return setup_parsed 35 | 36 | 37 | @vb.command(help='sphinx make for gh pages') 38 | def makedocs(): 39 | p = Path() 40 | path = p.joinpath('docs').absolute() 41 | click.echo(click.style(f"calling {path}//make github", fg='green')) 42 | subprocess.check_call(f'{path}//make github', shell=True) 43 | 44 | 45 | @vb.command(help='print setup.py') 46 | def printsetup(): 47 | setup_parsed = parse_setup() 48 | click.echo(click.style(setup_parsed, fg='blue')) 49 | 50 | 51 | @vb.command(help='MonkeyType apply on list-modules') 52 | @click.option('--apply/--no-apply', default=False) 53 | def mt(apply): 54 | click.echo(click.style("This could take a few seconds", fg='blue')) 55 | encoded_modules = subprocess.check_output("monkeytype list-modules", shell=True) 56 | list_modules = encoded_modules.decode().split('\r\n') 57 | to_exclude = {'mocksetup'} 58 | if apply: 59 | for m in list_modules: 60 | if [x for x in to_exclude if x in m] == []: 61 | click.echo(click.style(f"Applying on {m}", fg='green')) 62 | subprocess.check_call(f'monkeytype apply {m}', shell=True) 63 | 64 | 65 | @vb.command(help='Get current version') 66 | def gv(): 67 | click.echo("Running git describe") 68 | subprocess.check_call('git describe') 69 | 70 | 71 | @vb.command(help='Bump Minor') 72 | def bumpminor(): 73 | setup_parsed = parse_setup() 74 | old_version_regex = VERSION_REGEX 75 | old_version = re.findall(old_version_regex, setup_parsed)[0] 76 | minor = re.findall(r"([.]\d*)", old_version)[-1] 77 | minor = minor.replace('.', '') 78 | click.echo(f"Current Version: " + click.style(old_version, fg='green')) 79 | click.echo(f"Minor Found: " + click.style(minor, fg='green')) 80 | bump = str(int(minor) + 1) 81 | click.echo(f"Bumping to : " + click.style(bump, fg='blue')) 82 | new_version = str(old_version).replace(minor, bump) 83 | click.echo(f"Updated Version: " + click.style(new_version, fg='red')) 84 | b_minor(new_version) 85 | 86 | 87 | def b_minor(new_version): 88 | setup_parsed = parse_setup() 89 | old_version_regex = VERSION_REGEX 90 | old_version = re.findall(old_version_regex, setup_parsed)[0] 91 | setup_updated = setup_parsed.replace(old_version, new_version) 92 | with open('setup.py', 'w') as f: 93 | f.write(setup_updated) 94 | 95 | curdir = os.getcwd() 96 | click.echo(click.style(curdir + '\\standard-version', fg='blue')) 97 | subprocess.check_call('git fetch', shell=True) 98 | subprocess.check_call('git pull', shell=True) 99 | subprocess.check_call('git add setup.py', shell=True) 100 | subprocess.check_call(f'git commit -m \" setup bump {new_version} \" ', shell=True) 101 | subprocess.check_call(f'standard-version --release-as {new_version}', shell=True) 102 | # subprocess.check_call('git push --follow-tags origin', shell=True) 103 | 104 | 105 | @vb.command(help='Custom git log command for last N days') 106 | @click.argument('days') 107 | def gh(days): 108 | click.echo( 109 | click.style("Showing last ", fg='blue') + click.style(days, fg='green') + click.style(" days summary", 110 | fg='blue')) 111 | subprocess.check_call('git fetch', shell=True) 112 | subprocess.check_call(f'git log --oneline --decorate --graph --all -{days}', shell=True) 113 | 114 | 115 | if __name__ == '__main__': 116 | vb(prog_name='python -m vb') 117 | --------------------------------------------------------------------------------