Repository: BurnySc2/python-sc2 Branch: develop Commit: d8c3558a1ddd Files: 256 Total size: 1.4 MB Directory structure: gitextract_5hf8q6mo/ ├── .github/ │ └── workflows/ │ ├── ci.yml │ └── docker-ci.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── data/ │ ├── README.md │ └── data.json ├── dockerfiles/ │ ├── Dockerfile │ ├── README.md │ ├── download_maps.sh │ ├── test_docker_image.sh │ └── test_new_python_candidate.sh ├── docs_generate/ │ ├── Makefile │ ├── bot_ai/ │ │ └── index.rst │ ├── client/ │ │ └── index.rst │ ├── conf.py │ ├── game_data/ │ │ └── index.rst │ ├── game_info/ │ │ └── index.rst │ ├── game_state/ │ │ └── index.rst │ ├── ids/ │ │ └── index.rst │ ├── index.rst │ ├── make.bat │ ├── pixel_map/ │ │ └── index.rst │ ├── position/ │ │ └── index.rst │ ├── protocol/ │ │ └── index.rst │ ├── score/ │ │ └── index.rst │ ├── text_files/ │ │ ├── docker.rst │ │ └── introduction.rst │ ├── unit/ │ │ └── index.rst │ ├── unit_command/ │ │ └── index.rst │ └── units/ │ └── index.rst ├── examples/ │ ├── __init__.py │ ├── arcade_bot.py │ ├── bot_vs_bot.py │ ├── competitive/ │ │ ├── README.md │ │ ├── __init__.py │ │ ├── bot.py │ │ ├── ladderbots.json │ │ └── run.py │ ├── distributed_workers.py │ ├── external_bot.py │ ├── fastreload.py │ ├── host_external_norestart.py │ ├── observer_easy_vs_easy.py │ ├── play_tvz.py │ ├── protoss/ │ │ ├── __init__.py │ │ ├── cannon_rush.py │ │ ├── find_adept_shades.py │ │ ├── threebase_voidray.py │ │ └── warpgate_push.py │ ├── simulate_fight_scenario.py │ ├── terran/ │ │ ├── __init__.py │ │ ├── cyclone_push.py │ │ ├── mass_reaper.py │ │ ├── onebase_battlecruiser.py │ │ ├── proxy_rax.py │ │ └── ramp_wall.py │ ├── too_slow_bot.py │ ├── watch_replay.py │ ├── worker_rush.py │ ├── worker_stack_bot.py │ └── zerg/ │ ├── __init__.py │ ├── banes_banes_banes.py │ ├── expand_everywhere.py │ ├── hydralisk_push.py │ ├── onebase_broodlord.py │ ├── worker_split.py │ └── zerg_rush.py ├── generate_dicts_from_data_json.py ├── generate_id_constants_from_stableid.py ├── pyproject.toml ├── sc2/ │ ├── __init__.py │ ├── action.py │ ├── bot_ai.py │ ├── bot_ai_internal.py │ ├── cache.py │ ├── client.py │ ├── constants.py │ ├── controller.py │ ├── data.py │ ├── data.pyi │ ├── dicts/ │ │ ├── __init__.py │ │ ├── generic_redirect_abilities.py │ │ ├── unit_abilities.py │ │ ├── unit_research_abilities.py │ │ ├── unit_tech_alias.py │ │ ├── unit_train_build_abilities.py │ │ ├── unit_trained_from.py │ │ ├── unit_unit_alias.py │ │ └── upgrade_researched_from.py │ ├── expiring_dict.py │ ├── game_data.py │ ├── game_info.py │ ├── game_state.py │ ├── generate_ids.py │ ├── ids/ │ │ ├── __init__.py │ │ ├── ability_id.py │ │ ├── buff_id.py │ │ ├── effect_id.py │ │ ├── id_version.py │ │ ├── unit_typeid.py │ │ └── upgrade_id.py │ ├── main.py │ ├── maps.py │ ├── observer_ai.py │ ├── paths.py │ ├── pixel_map.py │ ├── player.py │ ├── portconfig.py │ ├── position.py │ ├── power_source.py │ ├── protocol.py │ ├── proxy.py │ ├── py.typed │ ├── renderer.py │ ├── sc2process.py │ ├── score.py │ ├── unit.py │ ├── unit_command.py │ ├── units.py │ ├── versions.py │ └── wsl.py └── test/ ├── Dockerfile ├── __init__.py ├── autotest_bot.py ├── battery_overcharge_bot.py ├── benchmark_array_creation.py ├── benchmark_bot_ai_init.py ├── benchmark_distance_two_points.py ├── benchmark_distances_cdist.py ├── benchmark_distances_points_to_point.py ├── benchmark_distances_units.py ├── benchmark_prepare_units.py ├── damagetest_bot.py ├── generate_pickle_files_bot.py ├── pickle_data/ │ ├── 16-BitLE.xz │ ├── 2000AtmospheresAIE.xz │ ├── AbiogenesisLE.xz │ ├── AbyssalReefLE.xz │ ├── AcidPlantLE.xz │ ├── AcolyteLE.xz │ ├── AcropolisLE.xz │ ├── AncientCisternAIE.xz │ ├── Artana.xz │ ├── AscensiontoAiurLE.xz │ ├── AutomatonLE.xz │ ├── BackwaterLE.xz │ ├── Bandwidth.xz │ ├── BattleontheBoardwalkLE.xz │ ├── BelShirVestigeLE.xz │ ├── BerlingradAIE.xz │ ├── BlackburnAIE.xz │ ├── BlackpinkLE.xz │ ├── BlueshiftLE.xz │ ├── CactusValleyLE.xz │ ├── CatalystLE.xz │ ├── CeruleanFallLE.xz │ ├── CrystalCavern.xz │ ├── CuriousMindsAIE.xz │ ├── CyberForestLE.xz │ ├── DarknessSanctuaryLE.xz │ ├── DeathAura506.xz │ ├── DeathAuraLE.xz │ ├── DefendersLandingLE.xz │ ├── DigitalFrontier.xz │ ├── DiscoBloodbathLE.xz │ ├── DragonScalesAIE.xz │ ├── DreamcatcherLE.xz │ ├── EastwatchLE.xz │ ├── Ephemeron.xz │ ├── EphemeronLE.xz │ ├── Equilibrium513AIE.xz │ ├── EternalEmpire506.xz │ ├── EternalEmpireLE.xz │ ├── EverDream506.xz │ ├── EverDreamLE.xz │ ├── FractureLE.xz │ ├── FrostLE.xz │ ├── GlitteringAshesAIE.xz │ ├── GoldenAura513AIE.xz │ ├── GoldenWall506.xz │ ├── GoldenWallLE.xz │ ├── GoldenauraAIE.xz │ ├── Gresvan513AIE.xz │ ├── GresvanAIE.xz │ ├── HardLead513AIE.xz │ ├── HardwireAIE.xz │ ├── HonorgroundsLE.xz │ ├── IceandChrome506.xz │ ├── IceandChromeLE.xz │ ├── InfestationStationAIE.xz │ ├── InsideAndOutAIE.xz │ ├── InterloperLE.xz │ ├── JagannathaAIE.xz │ ├── KairosJunctionLE.xz │ ├── KingsCoveLE.xz │ ├── LightshadeAIE.xz │ ├── MechDepotLE.xz │ ├── MoondanceAIE.xz │ ├── NeonVioletSquareLE.xz │ ├── NewRepugnancyLE.xz │ ├── NewkirkPrecinctTE.xz │ ├── NightshadeLE.xz │ ├── Oceanborn513AIE.xz │ ├── OdysseyLE.xz │ ├── OldSunshine.xz │ ├── OxideAIE.xz │ ├── PaladinoTerminalLE.xz │ ├── ParaSiteLE.xz │ ├── PersephoneAIE.xz │ ├── PillarsofGold506.xz │ ├── PillarsofGoldLE.xz │ ├── PortAleksanderLE.xz │ ├── PrimusQ9.xz │ ├── ProximaStationLE.xz │ ├── PylonAIE.xz │ ├── RedshiftLE.xz │ ├── Reminiscence.xz │ ├── RomanticideAIE.xz │ ├── RoyalBloodAIE.xz │ ├── Sanglune.xz │ ├── SequencerLE.xz │ ├── SimulacrumLE.xz │ ├── SiteDelta513AIE.xz │ ├── StargazersAIE.xz │ ├── StasisLE.xz │ ├── Submarine506.xz │ ├── SubmarineLE.xz │ ├── TheTimelessVoid.xz │ ├── ThunderbirdLE.xz │ ├── TorchesAIE.xz │ ├── Treachery.xz │ ├── Triton.xz │ ├── Urzagol.xz │ ├── WaterfallAIE.xz │ ├── WintersGateLE.xz │ ├── WorldofSleepersLE.xz │ ├── YearZeroLE.xz │ └── ZenLE.xz ├── queries_test_bot.py ├── real_time_worker_production.py ├── replays/ │ ├── 20220223 - GAME 1 - Astrea vs SKillous - P vs P - Curious Minds LE.SC2Replay │ ├── 20220224 - GAME 1 - Dream vs Spirit - T vs T - Curious Minds LE.SC2Replay │ ├── 20220224 - GAME 1 - Serral vs Dark - Z vs Z - 2000 Atmospheres LE.SC2Replay │ ├── 20220225 - GAME 2 - Maru vs ByuN - T vs T - Hardwire LE.SC2Replay │ └── 20220227 - GAME 2 - Reynor vs Serral - Z vs Z - Berlingrad LE.SC2Replay ├── run_example_bots_vs_computer.py ├── run_example_bots_vs_each_other.py ├── test_directions.py ├── test_expiring_dict.py ├── test_pickled_data.py ├── test_pickled_ramp.py ├── test_replays.py ├── travis_test_script.py └── upgradestest_bot.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/ci.yml ================================================ # If you change the name, change the link on the README.md for the badge too name: Tests on: push: paths: - sc2/** - examples/** - test/** - docs_generate/** - .pre-commit-config.yaml - generate_dicts_from_data_json.py - generate_id_constants_from_stableid.py - uv.lock - pyproject.toml - README.md - .github/workflows/ci.yml pull_request: branches: - master - develop env: # Docker image version, see https://hub.docker.com/r/burnysc2/python-sc2-docker/tags # This version should always lack behind one version behind the docker-ci.yml because it is possible that it doesn't exist VERSION_NUMBER: "1.0.4" # TODO Change to '3.14' when a new image has been pushed LATEST_PYTHON_VERSION: "3.13" LATEST_SC2_VERSION: "4.10" jobs: run_pre_commit_hook: name: Run pre-commit hook runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v3 - name: Set up Python ${{ env.LATEST_PYTHON_VERSION }} uses: actions/setup-python@v4 with: python-version: ${{ env.LATEST_PYTHON_VERSION }} - name: Install uv run: pip install uv - name: Install dependencies run: | uv sync --frozen --no-cache --no-install-project uv run pre-commit install - name: Run pre-commit hooks run: uv run pre-commit run --all-files --hook-stage pre-push generate_dicts_from_data_json: name: Generate dicts from data.json runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v3 - name: Set up Python ${{ env.LATEST_PYTHON_VERSION }} uses: actions/setup-python@v4 with: python-version: ${{ env.LATEST_PYTHON_VERSION }} - name: Install uv run: pip install uv - name: Install dependencies run: | uv sync --frozen --no-cache --no-install-project uv run pre-commit install - name: Run generate dicts # Check if newly generated file is the same as existing file # Run pre-commit hook to format files, always return exit code 0 to not end CI run run: | mv sc2/dicts sc2/dicts_old uv run python generate_dicts_from_data_json.py uv run pre-commit run --all-files --hook-stage pre-push || true rm -rf sc2/dicts/__pycache__ sc2/dicts_old/__pycache__ - name: Upload generated dicts folder as artifact uses: actions/upload-artifact@v4 with: name: Generated_dicts path: sc2/dicts - name: Compare generated dict files # Exit code will be 0 if the results of both commands are equal run: | [[ `ls sc2/dicts | md5sum` == `ls sc2/dicts_old | md5sum` ]] run_pytest_tests: # Run pytest tests on pickle files (pre-generated SC2 API observations) name: Run pytest needs: [run_pre_commit_hook, generate_dicts_from_data_json] runs-on: ${{ matrix.os }} timeout-minutes: 15 strategy: fail-fast: false matrix: # Python 3.6 fails due to: https://www.python.org/dev/peps/pep-0563/ # If all type annotations were removed, this library should run in py3.6 and perhaps even 3.5 # Python 3.7 support has been dropped due to missing cached_property (new since Python 3.8) https://docs.python.org/3/library/functools.html#functools.cached_property # Python 3.8 support has been dropped because numpy >=1.26.0 requires Python >=3.9 (this numpy version is required to run python 3.12) # Python 3.9 support has been dropped since numpy >=2.1.0 (this numpy version is required to run python 3.13) os: [macos-latest, windows-latest, ubuntu-latest] python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} id: setup-python uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install uv run: pip install uv - name: Install dependencies run: uv sync --frozen --no-cache --no-install-project - name: Run pytest run: uv run python -m pytest test # Run benchmarks - name: Run benchmark benchmark_array_creation run: uv run python -m pytest test/benchmark_array_creation.py - name: Run benchmark benchmark_distance_two_points run: uv run python -m pytest test/benchmark_distance_two_points.py - name: Run benchmark benchmark_distances_cdist run: uv run python -m pytest test/benchmark_distances_cdist.py - name: Run benchmark benchmark_distances_points_to_point run: uv run python -m pytest test/benchmark_distances_points_to_point.py - name: Run benchmark benchmark_distances_units run: uv run python -m pytest test/benchmark_distances_units.py - name: Run benchmark benchmark_bot_ai_prepare_units run: uv run python -m pytest test/benchmark_prepare_units.py - name: Run benchmark benchmark_bot_ai_init run: uv run python -m pytest test/benchmark_bot_ai_init.py run_test_bots: # Run test bots that download the SC2 linux client and run it name: Run testbots linux needs: [run_pytest_tests] runs-on: ${{ matrix.os }} timeout-minutes: 20 strategy: # Do not allow this test to cancel. Finish all jobs regardless of error fail-fast: false matrix: os: [ubuntu-latest] python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] sc2-version: ["4.10"] env: IMAGE_NAME: burnysc2/python-sc2:local steps: # Copy data from repository - uses: actions/checkout@v3 - name: Print directories and files run: sudo apt-get install tree && tree - name: Load and build docker image # Build docker image from Dockerfile using specific python and sc2 version env: BUILD_ARGS: --build-arg PYTHON_VERSION=${{ matrix.python-version }} --build-arg SC2_VERSION=${{ matrix.sc2-version }} run: docker build -f test/Dockerfile -t $IMAGE_NAME $BUILD_ARGS --build-arg VERSION_NUMBER=${{ env.VERSION_NUMBER }} . - name: Run autotest_bot.py # Run bot and list resulting files (replay file, stable_id.json) run: | docker run -i -d --name my_container --env 'PYTHONPATH=/root/python-sc2' $IMAGE_NAME docker exec -i my_container bash -c "python test/travis_test_script.py test/autotest_bot.py" docker exec -i my_container bash -c "tree" docker rm -f my_container - name: Run upgradestest_bot.py run: | docker run -i -d --name my_container --env 'PYTHONPATH=/root/python-sc2' $IMAGE_NAME docker exec -i my_container bash -c "python test/travis_test_script.py test/upgradestest_bot.py" docker exec -i my_container bash -c "tree" docker rm -f my_container - name: Run damagetest_bot.py run: | docker run -i -d --name my_container --env 'PYTHONPATH=/root/python-sc2' $IMAGE_NAME docker exec -i my_container bash -c "python test/travis_test_script.py test/damagetest_bot.py" docker exec -i my_container bash -c "tree" docker rm -f my_container - name: Run queries_test_bot.py run: | docker run -i -d --name my_container --env 'PYTHONPATH=/root/python-sc2' $IMAGE_NAME docker exec -i my_container bash -c "python test/travis_test_script.py test/queries_test_bot.py" docker exec -i my_container bash -c "tree" docker rm -f my_container run_example_bots: # Run example bots against computer name: Run example bots against computer needs: [run_pytest_tests] runs-on: ubuntu-latest timeout-minutes: 60 env: IMAGE_NAME: burnysc2/python-sc2-docker:local steps: # Copy data from repository - uses: actions/checkout@v3 - name: Print directories and files run: sudo apt-get install tree && tree - name: Load and build docker image # Build docker image from Dockerfile using specific python and sc2 version env: BUILD_ARGS: --build-arg PYTHON_VERSION=${{ env.LATEST_PYTHON_VERSION }} --build-arg SC2_VERSION=${{ env.LATEST_SC2_VERSION }} run: docker build -f test/Dockerfile -t $IMAGE_NAME $BUILD_ARGS --build-arg VERSION_NUMBER=${{ env.VERSION_NUMBER }} . - name: Run example bots vs computer run: | docker run -i -d --name my_container --env 'PYTHONPATH=/root/python-sc2' $IMAGE_NAME docker exec -i my_container bash -c "python test/run_example_bots_vs_computer.py" docker exec -i my_container bash -c "tree" docker rm -f my_container # TODO Fix in main.py "run_multiple_games" or "a_run_multiple_games" or "a_run_multiple_games_nokill" # run_bot_vs_bot: # # Run bot vs bot # name: Run example bots against each other # needs: [run_pytest_tests] # timeout-minutes: 60 # env: # IMAGE_NAME: burnysc2/python-sc2-docker:local # # steps: # # Copy data from repository # - uses: actions/checkout@v3 # # - name: Print directories and files # run: | # sudo apt-get install tree # tree # # - name: Load and build docker image # # Build docker image from Dockerfile using specific python and sc2 version # env: # BUILD_ARGS: --build-arg PYTHON_VERSION=${{ env.LATEST_PYTHON_VERSION }} --build-arg SC2_VERSION=${{ env.LATEST_SC2_VERSION }} # run: | # docker build -f test/Dockerfile -t $IMAGE_NAME $BUILD_ARGS --build-arg VERSION_NUMBER=${{ env.VERSION_NUMBER }} . # # - name: Run example bots vs each other # run: | # docker run -i -d --name my_container --env 'PYTHONPATH=/root/python-sc2' $IMAGE_NAME # docker exec -i my_container bash -c "python test/run_example_bots_vs_each_other.py" # docker exec -i my_container bash -c "tree" # docker rm -f my_container run_coverage: # Run and upload coverage report # This coverage test does not cover the whole testing range, check /bat_files/rune_code_coverage.bat name: Run coverage needs: [run_test_bots, run_example_bots] runs-on: ubuntu-latest timeout-minutes: 30 env: IMAGE_NAME: burnysc2/python-sc2-docker:local steps: - uses: actions/checkout@v3 - name: Load and build docker image # Build docker image from Dockerfile using specific python and sc2 version env: BUILD_ARGS: --build-arg PYTHON_VERSION=${{ env.LATEST_PYTHON_VERSION }} --build-arg SC2_VERSION=${{ env.LATEST_SC2_VERSION }} run: docker build -f test/Dockerfile -t $IMAGE_NAME $BUILD_ARGS --build-arg VERSION_NUMBER=${{ env.VERSION_NUMBER }} . - name: Set up container run: | mkdir htmlcov docker run -i -d \ -v $(pwd)/htmlcov:/root/python-sc2/htmlcov \ --name my_container \ --env 'PYTHONPATH=/root/python-sc2/' \ --entrypoint /bin/bash \ $IMAGE_NAME echo "Install dev requirements because only non dev requirements exist in the docker image at the moment" docker exec -i my_container bash -c "pip install uv \ && uv sync --frozen --no-cache --no-install-project" - name: Run coverage on tests run: docker exec -i my_container bash -c "uv run pytest --cov=./" - name: Run coverage on autotest_bot.py run: docker exec -i my_container bash -c "uv run coverage run -a test/travis_test_script.py test/autotest_bot.py" - name: Run coverage on upgradestest_bot.py run: docker exec -i my_container bash -c "uv run coverage run -a test/travis_test_script.py test/upgradestest_bot.py" - name: Run coverage on damagetest_bot.py run: docker exec -i my_container bash -c "uv run coverage run -a test/travis_test_script.py test/damagetest_bot.py" - name: Run coverage on queries_test_bot.py run: docker exec -i my_container bash -c "uv run coverage run -a test/travis_test_script.py test/queries_test_bot.py" # Bots might run differently long each time and create flucuations in code coverage - better to mock behavior instead # - name: Run coverage on example bots # run: | # docker exec -i my_container bash -c "uv run coverage run -a test/run_example_bots_vs_computer.py" - name: Generate xml coverage file run: | docker exec -i my_container bash -c "uv run coverage xml" docker cp my_container:/root/python-sc2/coverage.xml $(pwd)/coverage.xml - name: Generate html coverage files in htmlcov/ folder run: | docker exec -i my_container bash -c "uv run coverage html" echo "Upload htmlcov folder because it was mounted in container, so it will be available in host machine" - name: Upload htmlcov/ folder as artifact uses: actions/upload-artifact@v4 with: name: Coverage_report path: htmlcov run_radon: name: Run radon complexity analysis needs: [run_test_bots, run_example_bots] runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v3 - name: Set up Python ${{ env.LATEST_PYTHON_VERSION }} uses: actions/setup-python@v4 with: python-version: ${{ env.LATEST_PYTHON_VERSION }} - name: Install uv run: pip install uv - name: Install dependencies run: uv sync --frozen --no-cache --no-install-project - name: Run uv radon run: uv run radon cc sc2/ -a -nb release_to_github_pages: name: GitHub Pages needs: [run_test_bots, run_example_bots] runs-on: ubuntu-latest timeout-minutes: 5 steps: - uses: actions/checkout@v3 - name: Set up Python ${{ env.LATEST_PYTHON_VERSION }} uses: actions/setup-python@v4 with: python-version: ${{ env.LATEST_PYTHON_VERSION }} - name: Install uv run: pip install uv - name: Install dependencies run: uv sync --frozen --no-cache --no-install-project - name: Build docs from scratch run: | echo "" > index.html mkdir -p docs cd docs_generate uv run sphinx-build -a -E -b html . ../docs - name: Debug-list all generated files run: sudo apt-get install tree && tree - name: Publish to Github Pages if: github.ref == 'refs/heads/develop' && github.event_name == 'push' uses: JamesIves/github-pages-deploy-action@releases/v4 with: ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} BASE_BRANCH: develop # The branch the action should deploy from. BRANCH: gh-pages # The branch the action should deploy to. FOLDER: docs # The folder the action should deploy. release_to_pypi: name: Pypi package release needs: [run_test_bots, run_example_bots] runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v3 - name: Set up Python ${{ env.LATEST_PYTHON_VERSION }} uses: actions/setup-python@v4 with: python-version: ${{ env.LATEST_PYTHON_VERSION }} - name: Install uv run: pip install uv - name: Remove test folder to not include it in package run: rm -rf test - name: Build package run: uv build - name: Publish to pypi if: github.ref == 'refs/heads/develop' && github.event_name == 'push' continue-on-error: true run: uv publish --token ${{ secrets.PYPI_PYTHON_SC2_TOKEN }} ================================================ FILE: .github/workflows/docker-ci.yml ================================================ name: Build and push Docker image # Only run if Dockerfile or docker-ci.yml changed on: push: paths: - dockerfiles/** - uv.lock - pyproject.toml - .github/workflows/docker-ci.yml pull_request: branches: - master - develop env: VERSION_NUMBER: "1.0.7" LATEST_PYTHON_VERSION: "3.14" LATEST_SC2_VERSION: "4.10" EXPERIMENTAL_PYTHON_VERSION: "3.15" jobs: download_sc2_maps: name: Download and cache sc2 maps runs-on: ${{ matrix.os }} timeout-minutes: 15 strategy: fail-fast: false matrix: os: [ubuntu-latest] steps: - uses: actions/checkout@v3 - name: Cache sc2 maps uses: actions/cache@v4 id: cache-sc2-maps with: path: | dockerfiles/maps key: ${{ runner.os }}-maps-${{ hashFiles('dockerfiles/maps/**') }} restore-keys: | ${{ runner.os }}-maps- - name: Download sc2 maps run: sh dockerfiles/download_maps.sh if: steps.cache-sc2-maps.outputs.cache-hit != 'true' run_test_docker_image: name: Run test_docker_image.sh needs: [download_sc2_maps] runs-on: ${{ matrix.os }} timeout-minutes: 30 strategy: fail-fast: false matrix: os: [ubuntu-latest] steps: - uses: actions/checkout@v3 - name: Cache sc2 maps uses: actions/cache@v4 id: cache-sc2-maps with: path: | dockerfiles/maps key: ${{ runner.os }}-maps-${{ hashFiles('dockerfiles/maps/**') }} restore-keys: | ${{ runner.os }}-maps- - name: Download sc2 maps run: sh dockerfiles/download_maps.sh if: steps.cache-sc2-maps.outputs.cache-hit != 'true' - name: Run shell script env: VERSION_NUMBER: ${{ env.VERSION_NUMBER }} PYTHON_VERSION: ${{ env.LATEST_PYTHON_VERSION }} SC2_VERSION: ${{ env.LATEST_SC2_VERSION }} run: sh dockerfiles/test_docker_image.sh run_test_new_python_version: name: Run test_new_python_candidate.sh needs: [download_sc2_maps] runs-on: ${{ matrix.os }} timeout-minutes: 30 strategy: fail-fast: false matrix: os: [ubuntu-latest] steps: - uses: actions/checkout@v3 - name: Cache sc2 maps uses: actions/cache@v4 id: cache-sc2-maps with: path: | dockerfiles/maps key: ${{ runner.os }}-maps-${{ hashFiles('dockerfiles/maps/**') }} restore-keys: | ${{ runner.os }}-maps- - name: Download sc2 maps run: sh dockerfiles/download_maps.sh if: steps.cache-sc2-maps.outputs.cache-hit != 'true' - name: Run shell script continue-on-error: true env: VERSION_NUMBER: ${{ env.VERSION_NUMBER }} PYTHON_VERSION: ${{ env.EXPERIMENTAL_PYTHON_VERSION }} SC2_VERSION: ${{ env.LATEST_SC2_VERSION }} run: sh dockerfiles/test_new_python_candidate.sh docker_build: name: Build docker image needs: [download_sc2_maps] runs-on: ${{ matrix.os }} timeout-minutes: 30 strategy: fail-fast: false matrix: os: [ubuntu-latest] python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] sc2-version: ["4.10"] env: IMAGE_NAME: burnysc2/python-sc2-docker:py_${{ matrix.python-version }}-sc2_${{ matrix.sc2-version }} BUILD_ARGS: --build-arg PYTHON_VERSION=${{ matrix.python-version }} --build-arg SC2_VERSION=${{ matrix.sc2-version }} steps: - uses: actions/checkout@v3 - name: Cache sc2 maps uses: actions/cache@v4 id: cache-sc2-maps with: path: | dockerfiles/maps key: ${{ runner.os }}-maps-${{ hashFiles('dockerfiles/maps/**') }} restore-keys: | ${{ runner.os }}-maps- - name: Download sc2 maps run: sh dockerfiles/download_maps.sh if: steps.cache-sc2-maps.outputs.cache-hit != 'true' - name: Build docker image run: docker build -f dockerfiles/Dockerfile -t $IMAGE_NAME-v$VERSION_NUMBER $BUILD_ARGS . - name: Run test bots on image run: | echo "Start container, override the default entrypoint" docker run -i -d \ --name test_container \ --env 'PYTHONPATH=/root/python-sc2/' \ --entrypoint /bin/bash \ $IMAGE_NAME-v$VERSION_NUMBER echo "Install python-sc2" docker exec -i test_container mkdir -p /root/python-sc2 docker cp pyproject.toml test_container:/root/python-sc2/ docker cp uv.lock test_container:/root/python-sc2/ docker cp sc2 test_container:/root/python-sc2/sc2 docker cp test test_container:/root/python-sc2/test docker cp examples test_container:/root/python-sc2/examples docker exec -i test_container bash -c "pip install uv \ && cd python-sc2 && uv sync --frozen --no-cache --no-install-project" echo "Run various test bots" docker exec -i test_container bash -c "cd python-sc2 && uv run python test/travis_test_script.py test/autotest_bot.py" docker exec -i test_container bash -c "cd python-sc2 && uv run python test/run_example_bots_vs_computer.py" - name: Login to DockerHub if: github.ref == 'refs/heads/develop' && github.event_name == 'push' uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Upload docker image if: github.ref == 'refs/heads/develop' && github.event_name == 'push' run: docker push $IMAGE_NAME-v$VERSION_NUMBER - name: Upload docker image as latest tag if: github.ref == 'refs/heads/develop' && github.event_name == 'push' && matrix.python-version == env.LATEST_PYTHON_VERSION && matrix.sc2-version == env.LATEST_SC2_VERSION run: | docker tag $IMAGE_NAME-v$VERSION_NUMBER burnysc2/python-sc2-docker:latest docker push burnysc2/python-sc2-docker:latest ================================================ FILE: .gitignore ================================================ # Misc .DS_Store # Python + mypy + pytest __pycache__/ *.pyc .mypy_cache/ .pytest_cache/ build/ dist/ *.egg-info/ .cache/ # SC2 things maps/ mini_games/ .hypothesis/ # Editors .idea/ .vscode/ # Code coverage .coverage /htmlcov docs/ .pyre ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: # Check yaml files like this one and github actions if they are valid - id: check-yaml # Check toml files like pyproject.toml if it is valid - id: check-toml # Check if python files are valid - id: check-ast - id: check-builtin-literals - id: check-docstring-first - id: debug-statements # Check github action workflow files - repo: https://github.com/sirosen/check-jsonschema rev: 0.22.0 hooks: - id: check-github-workflows # Convert relative to absolute imports - repo: https://github.com/MarcoGorelli/absolufy-imports rev: v0.3.1 hooks: - id: absolufy-imports - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: # Check for bad code - id: python-no-eval - id: python-no-log-warn # Enforce type annotation instead of comment annotation - id: python-use-type-annotations - repo: local hooks: # Autoformat code - id: ruff-format-check name: Check if files are formatted stages: [pre-push] language: system # Run the following command to fix: # uv run ruff format . entry: uv run ruff format . --check --diff pass_filenames: false - id: ruff-lint name: Lint files stages: [pre-push] language: system # Run the following command to fix: # uv run ruff check . --fix entry: uv run ruff check . pass_filenames: false - id: pyrefly name: Static types checking with pyrefly stages: [pre-push] language: system entry: uv run pyrefly check pass_filenames: false ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 Hannes Karppila Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![Actions Status](https://github.com/BurnySc2/python-sc2/workflows/Tests/badge.svg)](https://github.com/BurnySc2/python-sc2/actions) [![codecov](https://codecov.io/gh/BurnySc2/python-sc2/branch/develop/graph/badge.svg?token=Pq5XkKw5VC)](https://codecov.io/gh/BurnySc2/python-sc2) # A StarCraft II API Client for Python 3 An easy-to-use library for writing AI Bots for StarCraft II in Python 3. The ultimate goal is simplicity and ease of use, while still preserving all functionality. A really simple worker rush bot should be no more than twenty lines of code, not two hundred. However, this library intends to provide both high and low level abstractions. **This library (currently) covers only the raw scripted interface.** At this time I don't intend to add support for graphics-based interfaces. The [documentation can be found here](https://burnysc2.github.io/python-sc2/index.html). For bot authors, looking directly at the files in the [sc2 folder](/sc2) can also be of benefit: bot_ai.py, unit.py, units.py, client.py, game_info.py and game_state.py. Most functions in those files have docstrings, example usages and type hinting. I am planning to change this fork more radically than the main repository, for bot performance benefits and to add functions to help new bot authors. This may break older bots in the future, however I try to add deprecation warnings to give a heads up notification. This means that the [video tutorial made by sentdex](https://pythonprogramming.net/starcraft-ii-ai-python-sc2-tutorial/) is outdated and does no longer directly work with this fork. For a list of ongoing changes and differences to the main repository of Dentosal, [check here](https://github.com/BurnySc2/python-sc2/issues/4). ## Installation By installing this library you agree to be bound by the terms of the [AI and Machine Learning License](http://blzdistsc2-a.akamaihd.net/AI_AND_MACHINE_LEARNING_LICENSE.html). For this fork, you'll need Python 3.9 or newer. Install the pypi package: ``` pip install --upgrade burnysc2 ``` or directly from develop branch: ``` pip install --upgrade --force-reinstall https://github.com/BurnySc2/python-sc2/archive/develop.zip ``` Both commands will use the `sc2` library folder, so you will not be able to have Dentosal's and this fork installed at the same time, unless you use virtual environments. ## StarCraft II You'll need a StarCraft II executable. If you are running Windows or macOS, just install SC2 from [blizzard app](https://starcraft2.com/). ### Linux installation You can install StarCraft II on Linux with [Wine](https://www.winehq.org/), [Lutris](https://lutris.net/games/battlenet/) or even the [Linux binary](https://github.com/Blizzard/s2client-proto#downloads), but the latter is headless so you cannot actually see the game. Starcraft II can be directly installed from Battlenet once it is downloaded with Lutris. By default, it will be installed here: ``` /home/burny/Games/battlenet/drive_c/Program Files (x86)/StarCraft II/ ``` Next, set the following environment variables (either globally or within your development environment, e.g. Pycharm: `Run -> Edit Configurations -> Environment Variables`): ``` SC2PF=WineLinux WINE=/usr/bin/wine # Or a wine binary from lutris: # WINE=/home/burny/.local/share/lutris/runners/wine/lutris-4.20-x86_64/bin/wine64 # Default Lutris StarCraftII Installation path: SC2PATH='/home/burny/Games/battlenet/drive_c/Program Files (x86)/StarCraft II/' ``` ### WSL When running WSL in Windows, python-sc2 detects WSL by default and starts Windows Starcraft 2 instead of Linux Starcraft 2. If you wish to instead have the game played in Linux, you can disable this behavior by setting `SC2_WSL_DETECT` environment variable to "0". You can do this inside python with the following code: ```py import os os.environ["SC2_WSL_DETECT"] = "0" ``` WSL version 1 should not require any configuration. You may be asked to allow Python through your firewall. When running WSL version 2 you need to supply the following environment variables so that your bot can connect: ``` SC2CLIENTHOST= SC2SERVERHOST=0.0.0.0 ``` If you are adding these to your .bashrc, you may need to export your environment variables by adding: ```sh export SC2CLIENTHOST export SC2SERVERHOST ``` You can find your Windows IP using `ipconfig /all` from `PowerShell.exe` or `CMD.exe`. ## Maps You will need maps to run the library. #### Official maps Official Blizzard map downloads are available from [Blizzard/s2client-proto](https://github.com/Blizzard/s2client-proto#downloads). Extract these maps into their respective *subdirectories* in the SC2 maps directory. e.g. `install-dir/Maps/Ladder2017Season1/` #### Bot ladder maps Maps that are run on the [SC2 AI Arena Ladder](https://aiarena.net/) can be downloaded [from the SC2 AI Arena Wiki](https://aiarena.net/wiki/bot-development/getting-started/#wiki-toc-maps). **Extract these maps into the *root* of the SC2 maps directory** (otherwise ladder replays won't work). e.g. `install-dir/Maps/AcropolisLE.SC2Map` ### Running After installing the library, a StarCraft II executable, and some maps, you're ready to get started. Simply run a bot file to fire up an instance of StarCraft II with the bot running. For example: ```sh python examples/protoss/cannon_rush.py ``` ## Example As promised, worker rush in less than twenty lines: ```python from sc2 import maps from sc2.player import Bot, Computer from sc2.main import run_game from sc2.data import Race, Difficulty from sc2.bot_ai import BotAI class WorkerRushBot(BotAI): async def on_step(self, iteration: int): if iteration == 0: for worker in self.workers: worker.attack(self.enemy_start_locations[0]) run_game(maps.get("Abyssal Reef LE"), [ Bot(Race.Zerg, WorkerRushBot()), Computer(Race.Protoss, Difficulty.Medium) ], realtime=True) ``` This is probably the simplest bot that has any realistic chances of winning the game. I have ran it against the medium AI a few times, and once in a while, it wins. You can find more examples in the [`examples/`](/examples) folder. ## API Configuration Options The API supports a number of options for configuring how it operates. ### `unit_command_uses_self_do` Set this to 'True' if your bot is issueing commands using `self.do(Unit(Ability, Target))` instead of `Unit(Ability, Target)`. ```python class MyBot(BotAI): def __init__(self): self.unit_command_uses_self_do = True ``` ### `raw_affects_selection` Setting this to true improves bot performance by a little bit. ```python class MyBot(BotAI): def __init__(self): self.raw_affects_selection = True ``` ### `distance_calculation_method` The distance calculation method: - 0 for raw python - 1 for scipy pdist - 2 for scipy cdist ```python class MyBot(BotAI): def __init__(self): self.distance_calculation_method: int = 2 ``` ### `game_step` On game start or in any frame actually, you can set the game step. This controls how often your bot's `step` method is called. __Do not set this in the \_\_init\_\_ function as the client will not have been initialized yet!__ ```python class MyBot(BotAI): def __init__(self): pass # don't set it here! async def on_start(self): self.client.game_step: int = 2 ``` ## Community - Help and support You have questions but don't want to create an issue? Join the [SC2 AI Arena Discord server](https://discordapp.com/invite/zXHU4wM). Questions about this repository can be asked in text channel #python. There are discussions and questions about SC2 bot programming and this repository every day. ## Bug reports, feature requests and ideas If you have any issues, ideas or feedback, please create [a new issue](https://github.com/BurnySc2/python-sc2/issues/new). Pull requests are also welcome! ## Contributing & style guidelines Git commit messages use [imperative-style messages](https://stackoverflow.com/a/3580764/2867076), start with capital letter and do not have trailing commas. To run pre-commit hooks (which run autoformatting and autosort imports) you can run ```sh uv run pre-commit install uv run pre-commit run --all-files --hook-stage pre-push ``` ================================================ FILE: data/README.md ================================================ This data comes from dentosals tech tree and is only used to generate the dictionaries in /sc2/dicts/: https://github.com/BurnySc2/sc2-techtree If you see abilities missing, requirements wrong or anything else related to the /sc2/dicts/, please open and write an issue here: https://github.com/BurnySc2/sc2-techtree/issues/new ================================================ FILE: data/data.json ================================================ {"Ability":[{"id":1,"name":"SMART","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit"},{"id":2,"name":"TAUNT_TAUNT","cast_range":10.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit"},{"id":4,"name":"STOP_STOP","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3665},{"id":5,"name":"STOP_HOLDFIRESPECIAL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3665},{"id":6,"name":"STOP_CHEER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3665},{"id":7,"name":"STOP_DANCE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3665},{"id":16,"name":"MOVE_MOVE","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit","remaps_to_ability_id":3794},{"id":17,"name":"PATROL_PATROL","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit","remaps_to_ability_id":3795},{"id":18,"name":"HOLDPOSITION_HOLD","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3793},{"id":19,"name":"SCAN_MOVE","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit","remaps_to_ability_id":3674},{"id":20,"name":"MOVE_TURN","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit"},{"id":23,"name":"ATTACK_ATTACK","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit","remaps_to_ability_id":3674},{"id":24,"name":"ATTACK_ATTACKTOWARDS","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":25,"name":"ATTACK_ATTACKBARRAGE","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":26,"name":"EFFECT_SPRAY_TERRAN","cast_range":1.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point","remaps_to_ability_id":3684},{"id":28,"name":"EFFECT_SPRAY_ZERG","cast_range":1.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point","remaps_to_ability_id":3684},{"id":30,"name":"EFFECT_SPRAY_PROTOSS","cast_range":1.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point","remaps_to_ability_id":3684},{"id":36,"name":"BEHAVIOR_HOLDFIREON_GHOST","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3688},{"id":38,"name":"BEHAVIOR_HOLDFIREOFF_GHOST","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3689},{"id":40,"name":"MORPHTOINFESTEDTERRAN_INFESTEDTERRANS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":7,"produces_name":"INFESTORTERRAN"}}},{"id":42,"name":"EXPLODE_EXPLODE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":45,"name":"FLEETBEACONRESEARCH_RESEARCHINTERCEPTORLAUNCHSPEEDUPGRADE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":1,"upgrade_name":"CARRIERLAUNCHSPEEDUPGRADE"}}},{"id":46,"name":"RESEARCH_PHOENIXANIONPULSECRYSTALS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":99,"upgrade_name":"PHOENIXRANGEUPGRADE"}}},{"id":47,"name":"FLEETBEACONRESEARCH_TEMPESTRANGEUPGRADE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":100,"upgrade_name":"TEMPESTRANGEUPGRADE"}}},{"id":48,"name":"FLEETBEACONRESEARCH_RESEARCHVOIDRAYSPEEDUPGRADE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":288,"upgrade_name":"VOIDRAYSPEEDUPGRADE"}}},{"id":49,"name":"FLEETBEACONRESEARCH_TEMPESTRESEARCHGROUNDATTACKUPGRADE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":297,"upgrade_name":"TEMPESTGROUNDATTACKUPGRADE"}}},{"id":74,"name":"FUNGALGROWTH_FUNGALGROWTH","cast_range":10.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":76,"name":"GUARDIANSHIELD_GUARDIANSHIELD","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":78,"name":"EFFECT_REPAIR_MULE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"Unit","remaps_to_ability_id":3685},{"id":110,"name":"NEXUSTRAINMOTHERSHIP_MOTHERSHIP","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":10}}},{"id":140,"name":"FEEDBACK_FEEDBACK","cast_range":10.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":142,"name":"EFFECT_MASSRECALL_STRATEGICRECALL","cast_range":500.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point","remaps_to_ability_id":3686},{"id":146,"name":"HALLUCINATION_ARCHON","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":148,"name":"HALLUCINATION_COLOSSUS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":150,"name":"HALLUCINATION_HIGHTEMPLAR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":152,"name":"HALLUCINATION_IMMORTAL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":154,"name":"HALLUCINATION_PHOENIX","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":156,"name":"HALLUCINATION_PROBE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":158,"name":"HALLUCINATION_STALKER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":160,"name":"HALLUCINATION_VOIDRAY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":162,"name":"HALLUCINATION_WARPPRISM","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":164,"name":"HALLUCINATION_ZEALOT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":166,"name":"HARVEST_GATHER_MULE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit","remaps_to_ability_id":3666},{"id":167,"name":"HARVEST_RETURN_MULE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3667},{"id":171,"name":"CALLDOWNMULE_CALLDOWNMULE","cast_range":500.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit"},{"id":173,"name":"GRAVITONBEAM_GRAVITONBEAM","cast_range":4.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":174,"name":"CANCEL_GRAVITONBEAM","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":175,"name":"BUILDINPROGRESSNYDUSCANAL_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":181,"name":"SPAWNCHANGELING_SPAWNCHANGELING","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":12,"produces_name":"CHANGELING"}}},{"id":195,"name":"RALLY_BUILDING","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit","remaps_to_ability_id":3673},{"id":199,"name":"RALLY_MORPHING_UNIT","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit","remaps_to_ability_id":3673},{"id":203,"name":"RALLY_COMMANDCENTER","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit","remaps_to_ability_id":3690},{"id":207,"name":"RALLY_NEXUS","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit","remaps_to_ability_id":3690},{"id":211,"name":"RALLY_HATCHERY_UNITS","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit","remaps_to_ability_id":3673},{"id":212,"name":"RALLY_HATCHERY_WORKERS","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit","remaps_to_ability_id":3690},{"id":216,"name":"RESEARCH_GLIALREGENERATION","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":2,"upgrade_name":"GLIALRECONSTITUTION"}}},{"id":217,"name":"RESEARCH_TUNNELINGCLAWS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":3,"upgrade_name":"TUNNELINGCLAWS"}}},{"id":218,"name":"ROACHWARRENRESEARCH_ROACHSUPPLY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":120,"upgrade_name":"ROACHSUPPLY"}}},{"id":245,"name":"SAPSTRUCTURE_SAPSTRUCTURE","cast_range":0.25,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":249,"name":"NEURALPARASITE_NEURALPARASITE","cast_range":8.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":250,"name":"CANCEL_NEURALPARASITE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":251,"name":"EFFECT_INJECTLARVA","cast_range":0.10009765625,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":253,"name":"EFFECT_STIM_MARAUDER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3675},{"id":255,"name":"SUPPLYDROP_SUPPLYDROP","cast_range":500.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":263,"name":"RESEARCH_ANABOLICSYNTHESIS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":88,"upgrade_name":"ANABOLICSYNTHESIS"}}},{"id":265,"name":"RESEARCH_CHITINOUSPLATING","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":4,"upgrade_name":"CHITINOUSPLATING"}}},{"id":295,"name":"HARVEST_GATHER_SCV","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit","remaps_to_ability_id":3666},{"id":296,"name":"HARVEST_RETURN_SCV","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3667},{"id":298,"name":"HARVEST_GATHER_PROBE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit","remaps_to_ability_id":3666},{"id":299,"name":"HARVEST_RETURN_PROBE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3667},{"id":301,"name":"ATTACKWARPPRISM_ATTACKWARPPRISM","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit"},{"id":302,"name":"ATTACKWARPPRISM_ATTACKTOWARDS","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":303,"name":"ATTACKWARPPRISM_ATTACKBARRAGE","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":304,"name":"CANCEL_QUEUE1","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3671},{"id":305,"name":"CANCELSLOT_QUEUE1","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3672},{"id":306,"name":"CANCEL_QUEUE5","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3671},{"id":307,"name":"CANCELSLOT_QUEUE5","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3672},{"id":308,"name":"CANCEL_QUEUECANCELTOSELECTION","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3671},{"id":309,"name":"CANCELSLOT_QUEUECANCELTOSELECTION","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3672},{"id":312,"name":"CANCEL_QUEUEADDON","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3671},{"id":313,"name":"CANCELSLOT_ADDON","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3672},{"id":314,"name":"CANCEL_BUILDINPROGRESS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":315,"name":"HALT_BUILDING","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3660},{"id":316,"name":"EFFECT_REPAIR_SCV","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"Unit","remaps_to_ability_id":3685},{"id":318,"name":"TERRANBUILD_COMMANDCENTER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":18,"produces_name":"COMMANDCENTER"}}},{"id":319,"name":"TERRANBUILD_SUPPLYDEPOT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":19,"produces_name":"SUPPLYDEPOT"}}},{"id":320,"name":"TERRANBUILD_REFINERY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"BuildOnUnit":{"produces":20,"produces_name":"REFINERY"}}},{"id":321,"name":"TERRANBUILD_BARRACKS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":21,"produces_name":"BARRACKS"}}},{"id":322,"name":"TERRANBUILD_ENGINEERINGBAY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":22,"produces_name":"ENGINEERINGBAY"}}},{"id":323,"name":"TERRANBUILD_MISSILETURRET","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":23,"produces_name":"MISSILETURRET"}}},{"id":324,"name":"TERRANBUILD_BUNKER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":24,"produces_name":"BUNKER"}}},{"id":326,"name":"TERRANBUILD_SENSORTOWER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":25,"produces_name":"SENSORTOWER"}}},{"id":327,"name":"TERRANBUILD_GHOSTACADEMY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":26,"produces_name":"GHOSTACADEMY"}}},{"id":328,"name":"TERRANBUILD_FACTORY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":27,"produces_name":"FACTORY"}}},{"id":329,"name":"TERRANBUILD_STARPORT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":28,"produces_name":"STARPORT"}}},{"id":331,"name":"TERRANBUILD_ARMORY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":29,"produces_name":"ARMORY"}}},{"id":333,"name":"TERRANBUILD_FUSIONCORE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":30,"produces_name":"FUSIONCORE"}}},{"id":348,"name":"HALT_TERRANBUILD","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3660},{"id":380,"name":"EFFECT_STIM_MARINE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3675},{"id":382,"name":"BEHAVIOR_CLOAKON_GHOST","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3676},{"id":383,"name":"BEHAVIOR_CLOAKOFF_GHOST","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3677},{"id":386,"name":"MEDIVACHEAL_HEAL","cast_range":4.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":388,"name":"SIEGEMODE_SIEGEMODE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":32,"produces_name":"SIEGETANKSIEGED"}}},{"id":390,"name":"UNSIEGE_UNSIEGE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":33,"produces_name":"SIEGETANK"}}},{"id":392,"name":"BEHAVIOR_CLOAKON_BANSHEE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3676},{"id":393,"name":"BEHAVIOR_CLOAKOFF_BANSHEE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3677},{"id":394,"name":"LOAD_MEDIVAC","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit","remaps_to_ability_id":3668},{"id":396,"name":"UNLOADALLAT_MEDIVAC","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit","remaps_to_ability_id":3669},{"id":397,"name":"UNLOADUNIT_MEDIVAC","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3796},{"id":399,"name":"SCANNERSWEEP_SCAN","cast_range":500.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":401,"name":"YAMATO_YAMATOGUN","cast_range":10.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":403,"name":"MORPH_VIKINGASSAULTMODE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":34,"produces_name":"VIKINGASSAULT"}}},{"id":405,"name":"MORPH_VIKINGFIGHTERMODE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":35,"produces_name":"VIKINGFIGHTER"}}},{"id":407,"name":"LOAD_BUNKER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit","remaps_to_ability_id":3668},{"id":408,"name":"UNLOADALL_BUNKER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3664},{"id":410,"name":"UNLOADUNIT_BUNKER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3796},{"id":412,"name":"COMMANDCENTERTRANSPORT_COMMANDCENTERTRANSPORT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit","remaps_to_ability_id":3668},{"id":413,"name":"UNLOADALL_COMMANDCENTER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3664},{"id":415,"name":"UNLOADUNIT_COMMANDCENTER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3796},{"id":416,"name":"LOADALL_COMMANDCENTER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3663},{"id":417,"name":"LIFT_COMMANDCENTER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3679,"target":{"Morph":{"produces":36,"produces_name":"COMMANDCENTERFLYING"}}},{"id":419,"name":"LAND_COMMANDCENTER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3678,"target":{"MorphPlace":{"produces":18,"produces_name":"COMMANDCENTER"}}},{"id":421,"name":"BUILD_TECHLAB_BARRACKS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3682,"target":{"BuildInstant":{"produces":37}}},{"id":422,"name":"BUILD_REACTOR_BARRACKS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3683,"target":{"BuildInstant":{"produces":38}}},{"id":451,"name":"CANCEL_BARRACKSADDON","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":452,"name":"LIFT_BARRACKS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3679,"target":{"Morph":{"produces":46,"produces_name":"BARRACKSFLYING"}}},{"id":454,"name":"BUILD_TECHLAB_FACTORY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3682,"target":{"BuildInstant":{"produces":39}}},{"id":455,"name":"BUILD_REACTOR_FACTORY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3683,"target":{"BuildInstant":{"produces":40}}},{"id":484,"name":"CANCEL_FACTORYADDON","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":485,"name":"LIFT_FACTORY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3679,"target":{"Morph":{"produces":43,"produces_name":"FACTORYFLYING"}}},{"id":487,"name":"BUILD_TECHLAB_STARPORT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3682,"target":{"BuildInstant":{"produces":41}}},{"id":488,"name":"BUILD_REACTOR_STARPORT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3683,"target":{"BuildInstant":{"produces":42}}},{"id":517,"name":"CANCEL_STARPORTADDON","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":518,"name":"LIFT_STARPORT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3679,"target":{"Morph":{"produces":44,"produces_name":"STARPORTFLYING"}}},{"id":520,"name":"LAND_FACTORY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3678,"target":{"MorphPlace":{"produces":27,"produces_name":"FACTORY"}}},{"id":522,"name":"LAND_STARPORT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3678,"target":{"MorphPlace":{"produces":28,"produces_name":"STARPORT"}}},{"id":524,"name":"COMMANDCENTERTRAIN_SCV","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":45,"produces_name":"SCV"}}},{"id":554,"name":"LAND_BARRACKS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3678,"target":{"MorphPlace":{"produces":21,"produces_name":"BARRACKS"}}},{"id":556,"name":"MORPH_SUPPLYDEPOT_LOWER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":47,"produces_name":"SUPPLYDEPOTLOWERED"}}},{"id":558,"name":"MORPH_SUPPLYDEPOT_RAISE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":19,"produces_name":"SUPPLYDEPOT"}}},{"id":560,"name":"BARRACKSTRAIN_MARINE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":48,"produces_name":"MARINE"}}},{"id":561,"name":"BARRACKSTRAIN_REAPER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":49,"produces_name":"REAPER"}}},{"id":562,"name":"BARRACKSTRAIN_GHOST","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":50,"produces_name":"GHOST"}}},{"id":563,"name":"BARRACKSTRAIN_MARAUDER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":51,"produces_name":"MARAUDER"}}},{"id":590,"name":"FACTORYTRAIN_FACTORYTRAIN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":0,"produces_name":"Unknown"}}},{"id":591,"name":"FACTORYTRAIN_SIEGETANK","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":33,"produces_name":"SIEGETANK"}}},{"id":594,"name":"FACTORYTRAIN_THOR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":52,"produces_name":"THOR"}}},{"id":595,"name":"FACTORYTRAIN_HELLION","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":53,"produces_name":"HELLION"}}},{"id":596,"name":"TRAIN_HELLBAT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":484,"produces_name":"HELLIONTANK"}}},{"id":597,"name":"TRAIN_CYCLONE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":692,"produces_name":"CYCLONE"}}},{"id":614,"name":"FACTORYTRAIN_WIDOWMINE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":498,"produces_name":"WIDOWMINE"}}},{"id":620,"name":"STARPORTTRAIN_MEDIVAC","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":54,"produces_name":"MEDIVAC"}}},{"id":621,"name":"STARPORTTRAIN_BANSHEE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":55,"produces_name":"BANSHEE"}}},{"id":622,"name":"STARPORTTRAIN_RAVEN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":56,"produces_name":"RAVEN"}}},{"id":623,"name":"STARPORTTRAIN_BATTLECRUISER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":57,"produces_name":"BATTLECRUISER"}}},{"id":624,"name":"STARPORTTRAIN_VIKINGFIGHTER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":35,"produces_name":"VIKINGFIGHTER"}}},{"id":626,"name":"STARPORTTRAIN_LIBERATOR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":689,"produces_name":"LIBERATOR"}}},{"id":650,"name":"RESEARCH_HISECAUTOTRACKING","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":5,"upgrade_name":"HISECAUTOTRACKING"}}},{"id":651,"name":"RESEARCH_TERRANSTRUCTUREARMORUPGRADE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":6,"upgrade_name":"TERRANBUILDINGARMOR"}}},{"id":652,"name":"ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL1","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3698,"target":{"Research":{"upgrade":7,"upgrade_name":"TERRANINFANTRYWEAPONSLEVEL1"}}},{"id":653,"name":"ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL2","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3698,"target":{"Research":{"upgrade":8,"upgrade_name":"TERRANINFANTRYWEAPONSLEVEL2"}}},{"id":654,"name":"ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL3","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3698,"target":{"Research":{"upgrade":9,"upgrade_name":"TERRANINFANTRYWEAPONSLEVEL3"}}},{"id":655,"name":"RESEARCH_NEOSTEELFRAME","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":10,"upgrade_name":"NEOSTEELFRAME"}}},{"id":656,"name":"ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL1","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3697,"target":{"Research":{"upgrade":11,"upgrade_name":"TERRANINFANTRYARMORSLEVEL1"}}},{"id":657,"name":"ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL2","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3697,"target":{"Research":{"upgrade":12,"upgrade_name":"TERRANINFANTRYARMORSLEVEL2"}}},{"id":658,"name":"ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL3","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3697,"target":{"Research":{"upgrade":13,"upgrade_name":"TERRANINFANTRYARMORSLEVEL3"}}},{"id":710,"name":"BUILD_NUKE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":730,"name":"BARRACKSTECHLABRESEARCH_STIMPACK","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":15,"upgrade_name":"STIMPACK"}}},{"id":731,"name":"RESEARCH_COMBATSHIELD","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":16,"upgrade_name":"SHIELDWALL"}}},{"id":732,"name":"RESEARCH_CONCUSSIVESHELLS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":17,"upgrade_name":"PUNISHERGRENADES"}}},{"id":761,"name":"RESEARCH_INFERNALPREIGNITER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":19,"upgrade_name":"HIGHCAPACITYBARRELS"}}},{"id":763,"name":"FACTORYTECHLABRESEARCH_RESEARCHTRANSFORMATIONSERVOS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":98,"upgrade_name":"TRANSFORMATIONSERVOS"}}},{"id":764,"name":"RESEARCH_DRILLINGCLAWS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":122,"upgrade_name":"DRILLCLAWS"}}},{"id":765,"name":"FACTORYTECHLABRESEARCH_RESEARCHLOCKONRANGEUPGRADE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":123,"upgrade_name":"CYCLONELOCKONRANGEUPGRADE"}}},{"id":766,"name":"RESEARCH_SMARTSERVOS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":289,"upgrade_name":"SMARTSERVOS"}}},{"id":767,"name":"FACTORYTECHLABRESEARCH_RESEARCHARMORPIERCINGROCKETS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":290,"upgrade_name":"ARMORPIERCINGROCKETS"}}},{"id":768,"name":"RESEARCH_CYCLONERAPIDFIRELAUNCHERS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":291,"upgrade_name":"CYCLONERAPIDFIRELAUNCHERS"}}},{"id":769,"name":"RESEARCH_CYCLONELOCKONDAMAGE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":144,"upgrade_name":"CYCLONELOCKONDAMAGEUPGRADE"}}},{"id":770,"name":"FACTORYTECHLABRESEARCH_CYCLONERESEARCHHURRICANETHRUSTERS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":296,"upgrade_name":"HURRICANETHRUSTERS"}}},{"id":790,"name":"RESEARCH_BANSHEECLOAKINGFIELD","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":20,"upgrade_name":"BANSHEECLOAK"}}},{"id":793,"name":"RESEARCH_RAVENCORVIDREACTOR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":22,"upgrade_name":"RAVENCORVIDREACTOR"}}},{"id":796,"name":"STARPORTTECHLABRESEARCH_RESEARCHSEEKERMISSILE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":23,"upgrade_name":"HUNTERSEEKER"}}},{"id":797,"name":"STARPORTTECHLABRESEARCH_RESEARCHDURABLEMATERIALS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":24,"upgrade_name":"DURABLEMATERIALS"}}},{"id":799,"name":"RESEARCH_BANSHEEHYPERFLIGHTROTORS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":136,"upgrade_name":"BANSHEESPEED"}}},{"id":800,"name":"STARPORTTECHLABRESEARCH_RESEARCHLIBERATORAGMODE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":125,"upgrade_name":"LIBERATORMORPH"}}},{"id":802,"name":"STARPORTTECHLABRESEARCH_RESEARCHRAPIDDEPLOYMENT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":137,"upgrade_name":"MEDIVACRAPIDDEPLOYMENT"}}},{"id":803,"name":"RESEARCH_RAVENRECALIBRATEDEXPLOSIVES","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":138,"upgrade_name":"RAVENRECALIBRATEDEXPLOSIVES"}}},{"id":806,"name":"STARPORTTECHLABRESEARCH_RAVENRESEARCHENHANCEDMUNITIONS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":292,"upgrade_name":"RAVENENHANCEDMUNITIONS"}}},{"id":807,"name":"STARPORTTECHLABRESEARCH_RESEARCHRAVENINTERFERENCEMATRIX","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":300,"upgrade_name":"INTERFERENCEMATRIX"}}},{"id":820,"name":"RESEARCH_PERSONALCLOAKING","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":25,"upgrade_name":"PERSONALCLOAKING"}}},{"id":852,"name":"ARMORYRESEARCH_TERRANVEHICLEPLATINGLEVEL1","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":27,"upgrade_name":"TERRANVEHICLEARMORSLEVEL1"}}},{"id":853,"name":"ARMORYRESEARCH_TERRANVEHICLEPLATINGLEVEL2","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":28,"upgrade_name":"TERRANVEHICLEARMORSLEVEL2"}}},{"id":854,"name":"ARMORYRESEARCH_TERRANVEHICLEPLATINGLEVEL3","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":29,"upgrade_name":"TERRANVEHICLEARMORSLEVEL3"}}},{"id":855,"name":"ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL1","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3701,"target":{"Research":{"upgrade":30,"upgrade_name":"TERRANVEHICLEWEAPONSLEVEL1"}}},{"id":856,"name":"ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL2","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3701,"target":{"Research":{"upgrade":31,"upgrade_name":"TERRANVEHICLEWEAPONSLEVEL2"}}},{"id":857,"name":"ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL3","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3701,"target":{"Research":{"upgrade":32,"upgrade_name":"TERRANVEHICLEWEAPONSLEVEL3"}}},{"id":858,"name":"ARMORYRESEARCH_TERRANSHIPPLATINGLEVEL1","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":33,"upgrade_name":"TERRANSHIPARMORSLEVEL1"}}},{"id":859,"name":"ARMORYRESEARCH_TERRANSHIPPLATINGLEVEL2","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":34,"upgrade_name":"TERRANSHIPARMORSLEVEL2"}}},{"id":860,"name":"ARMORYRESEARCH_TERRANSHIPPLATINGLEVEL3","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":35,"upgrade_name":"TERRANSHIPARMORSLEVEL3"}}},{"id":861,"name":"ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL1","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3699,"target":{"Research":{"upgrade":36,"upgrade_name":"TERRANSHIPWEAPONSLEVEL1"}}},{"id":862,"name":"ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL2","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3699,"target":{"Research":{"upgrade":37,"upgrade_name":"TERRANSHIPWEAPONSLEVEL2"}}},{"id":863,"name":"ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL3","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3699,"target":{"Research":{"upgrade":38,"upgrade_name":"TERRANSHIPWEAPONSLEVEL3"}}},{"id":864,"name":"ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL1","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3700,"target":{"Research":{"upgrade":116,"upgrade_name":"TERRANVEHICLEANDSHIPARMORSLEVEL1"}}},{"id":865,"name":"ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL2","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3700,"target":{"Research":{"upgrade":117,"upgrade_name":"TERRANVEHICLEANDSHIPARMORSLEVEL2"}}},{"id":866,"name":"ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL3","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3700,"target":{"Research":{"upgrade":118,"upgrade_name":"TERRANVEHICLEANDSHIPARMORSLEVEL3"}}},{"id":880,"name":"PROTOSSBUILD_NEXUS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":59,"produces_name":"NEXUS"}}},{"id":881,"name":"PROTOSSBUILD_PYLON","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":60,"produces_name":"PYLON"}}},{"id":882,"name":"PROTOSSBUILD_ASSIMILATOR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"BuildOnUnit":{"produces":61,"produces_name":"ASSIMILATOR"}}},{"id":883,"name":"PROTOSSBUILD_GATEWAY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":62,"produces_name":"GATEWAY"}}},{"id":884,"name":"PROTOSSBUILD_FORGE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":63,"produces_name":"FORGE"}}},{"id":885,"name":"PROTOSSBUILD_FLEETBEACON","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":64,"produces_name":"FLEETBEACON"}}},{"id":886,"name":"PROTOSSBUILD_TWILIGHTCOUNCIL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":65,"produces_name":"TWILIGHTCOUNCIL"}}},{"id":887,"name":"PROTOSSBUILD_PHOTONCANNON","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":66,"produces_name":"PHOTONCANNON"}}},{"id":889,"name":"PROTOSSBUILD_STARGATE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":67,"produces_name":"STARGATE"}}},{"id":890,"name":"PROTOSSBUILD_TEMPLARARCHIVE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":68,"produces_name":"TEMPLARARCHIVE"}}},{"id":891,"name":"PROTOSSBUILD_DARKSHRINE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":69,"produces_name":"DARKSHRINE"}}},{"id":892,"name":"PROTOSSBUILD_ROBOTICSBAY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":70,"produces_name":"ROBOTICSBAY"}}},{"id":893,"name":"PROTOSSBUILD_ROBOTICSFACILITY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":71,"produces_name":"ROBOTICSFACILITY"}}},{"id":894,"name":"PROTOSSBUILD_CYBERNETICSCORE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":72,"produces_name":"CYBERNETICSCORE"}}},{"id":895,"name":"BUILD_SHIELDBATTERY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":1910,"produces_name":"SHIELDBATTERY"}}},{"id":910,"name":"PROTOSSBUILD_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3660},{"id":911,"name":"LOAD_WARPPRISM","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit","remaps_to_ability_id":3668},{"id":912,"name":"UNLOADALL_WARPPRISM","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3664},{"id":913,"name":"UNLOADALLAT_WARPPRISM","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit","remaps_to_ability_id":3669},{"id":914,"name":"UNLOADUNIT_WARPPRISM","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3796},{"id":916,"name":"GATEWAYTRAIN_ZEALOT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":73,"produces_name":"ZEALOT"}}},{"id":917,"name":"GATEWAYTRAIN_STALKER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":74,"produces_name":"STALKER"}}},{"id":919,"name":"GATEWAYTRAIN_HIGHTEMPLAR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":75,"produces_name":"HIGHTEMPLAR"}}},{"id":920,"name":"GATEWAYTRAIN_DARKTEMPLAR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":76,"produces_name":"DARKTEMPLAR"}}},{"id":921,"name":"GATEWAYTRAIN_SENTRY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":77,"produces_name":"SENTRY"}}},{"id":922,"name":"TRAIN_ADEPT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":311,"produces_name":"ADEPT"}}},{"id":946,"name":"STARGATETRAIN_PHOENIX","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":78,"produces_name":"PHOENIX"}}},{"id":948,"name":"STARGATETRAIN_CARRIER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":79,"produces_name":"CARRIER"}}},{"id":950,"name":"STARGATETRAIN_VOIDRAY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":80,"produces_name":"VOIDRAY"}}},{"id":954,"name":"STARGATETRAIN_ORACLE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":495,"produces_name":"ORACLE"}}},{"id":955,"name":"STARGATETRAIN_TEMPEST","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":496,"produces_name":"TEMPEST"}}},{"id":976,"name":"ROBOTICSFACILITYTRAIN_WARPPRISM","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":81,"produces_name":"WARPPRISM"}}},{"id":977,"name":"ROBOTICSFACILITYTRAIN_OBSERVER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":82,"produces_name":"OBSERVER"}}},{"id":978,"name":"ROBOTICSFACILITYTRAIN_COLOSSUS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":4,"produces_name":"COLOSSUS"}}},{"id":979,"name":"ROBOTICSFACILITYTRAIN_IMMORTAL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":83,"produces_name":"IMMORTAL"}}},{"id":994,"name":"TRAIN_DISRUPTOR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":694,"produces_name":"DISRUPTOR"}}},{"id":1006,"name":"NEXUSTRAIN_PROBE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":84,"produces_name":"PROBE"}}},{"id":1036,"name":"PSISTORM_PSISTORM","cast_range":9.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":1038,"name":"CANCEL_HANGARQUEUE5","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3671},{"id":1039,"name":"CANCELSLOT_HANGARQUEUE5","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3672},{"id":1040,"name":"BROODLORDQUEUE2_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3671},{"id":1041,"name":"BROODLORDQUEUE2_CANCELSLOT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3672},{"id":1042,"name":"BUILD_INTERCEPTORS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":1062,"name":"FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL1","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3695,"target":{"Research":{"upgrade":39,"upgrade_name":"PROTOSSGROUNDWEAPONSLEVEL1"}}},{"id":1063,"name":"FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL2","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3695,"target":{"Research":{"upgrade":40,"upgrade_name":"PROTOSSGROUNDWEAPONSLEVEL2"}}},{"id":1064,"name":"FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL3","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3695,"target":{"Research":{"upgrade":41,"upgrade_name":"PROTOSSGROUNDWEAPONSLEVEL3"}}},{"id":1065,"name":"FORGERESEARCH_PROTOSSGROUNDARMORLEVEL1","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3694,"target":{"Research":{"upgrade":42,"upgrade_name":"PROTOSSGROUNDARMORSLEVEL1"}}},{"id":1066,"name":"FORGERESEARCH_PROTOSSGROUNDARMORLEVEL2","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3694,"target":{"Research":{"upgrade":43,"upgrade_name":"PROTOSSGROUNDARMORSLEVEL2"}}},{"id":1067,"name":"FORGERESEARCH_PROTOSSGROUNDARMORLEVEL3","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3694,"target":{"Research":{"upgrade":44,"upgrade_name":"PROTOSSGROUNDARMORSLEVEL3"}}},{"id":1068,"name":"FORGERESEARCH_PROTOSSSHIELDSLEVEL1","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3696,"target":{"Research":{"upgrade":45,"upgrade_name":"PROTOSSSHIELDSLEVEL1"}}},{"id":1069,"name":"FORGERESEARCH_PROTOSSSHIELDSLEVEL2","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3696,"target":{"Research":{"upgrade":46,"upgrade_name":"PROTOSSSHIELDSLEVEL2"}}},{"id":1070,"name":"FORGERESEARCH_PROTOSSSHIELDSLEVEL3","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3696,"target":{"Research":{"upgrade":47,"upgrade_name":"PROTOSSSHIELDSLEVEL3"}}},{"id":1093,"name":"RESEARCH_GRAVITICBOOSTER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":48,"upgrade_name":"OBSERVERGRAVITICBOOSTER"}}},{"id":1094,"name":"RESEARCH_GRAVITICDRIVE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":49,"upgrade_name":"GRAVITICDRIVE"}}},{"id":1097,"name":"RESEARCH_EXTENDEDTHERMALLANCE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":50,"upgrade_name":"EXTENDEDTHERMALLANCE"}}},{"id":1099,"name":"ROBOTICSBAYRESEARCH_RESEARCHIMMORTALREVIVE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":121,"upgrade_name":"IMMORTALREVIVE"}}},{"id":1126,"name":"RESEARCH_PSISTORM","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":52,"upgrade_name":"PSISTORMTECH"}}},{"id":1152,"name":"ZERGBUILD_HATCHERY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":86,"produces_name":"HATCHERY"}}},{"id":1153,"name":"ZERGBUILD_CREEPTUMOR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":87,"produces_name":"CREEPTUMOR"}}},{"id":1154,"name":"ZERGBUILD_EXTRACTOR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"BuildOnUnit":{"produces":88,"produces_name":"EXTRACTOR"}}},{"id":1155,"name":"ZERGBUILD_SPAWNINGPOOL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":89,"produces_name":"SPAWNINGPOOL"}}},{"id":1156,"name":"ZERGBUILD_EVOLUTIONCHAMBER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":90,"produces_name":"EVOLUTIONCHAMBER"}}},{"id":1157,"name":"ZERGBUILD_HYDRALISKDEN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":91,"produces_name":"HYDRALISKDEN"}}},{"id":1158,"name":"ZERGBUILD_SPIRE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":92,"produces_name":"SPIRE"}}},{"id":1159,"name":"ZERGBUILD_ULTRALISKCAVERN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":93,"produces_name":"ULTRALISKCAVERN"}}},{"id":1160,"name":"ZERGBUILD_INFESTATIONPIT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":94,"produces_name":"INFESTATIONPIT"}}},{"id":1161,"name":"ZERGBUILD_NYDUSNETWORK","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":95,"produces_name":"NYDUSNETWORK"}}},{"id":1162,"name":"ZERGBUILD_BANELINGNEST","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":96,"produces_name":"BANELINGNEST"}}},{"id":1163,"name":"BUILD_LURKERDEN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":504,"produces_name":"LURKERDENMP"}}},{"id":1165,"name":"ZERGBUILD_ROACHWARREN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":97,"produces_name":"ROACHWARREN"}}},{"id":1166,"name":"ZERGBUILD_SPINECRAWLER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":98,"produces_name":"SPINECRAWLER"}}},{"id":1167,"name":"ZERGBUILD_SPORECRAWLER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":99,"produces_name":"SPORECRAWLER"}}},{"id":1182,"name":"ZERGBUILD_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3660},{"id":1183,"name":"HARVEST_GATHER_DRONE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit","remaps_to_ability_id":3666},{"id":1184,"name":"HARVEST_RETURN_DRONE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3667},{"id":1186,"name":"RESEARCH_ZERGMELEEWEAPONSLEVEL1","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3705,"target":{"Research":{"upgrade":53,"upgrade_name":"ZERGMELEEWEAPONSLEVEL1"}}},{"id":1187,"name":"RESEARCH_ZERGMELEEWEAPONSLEVEL2","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3705,"target":{"Research":{"upgrade":54,"upgrade_name":"ZERGMELEEWEAPONSLEVEL2"}}},{"id":1188,"name":"RESEARCH_ZERGMELEEWEAPONSLEVEL3","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3705,"target":{"Research":{"upgrade":55,"upgrade_name":"ZERGMELEEWEAPONSLEVEL3"}}},{"id":1189,"name":"RESEARCH_ZERGGROUNDARMORLEVEL1","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3704,"target":{"Research":{"upgrade":56,"upgrade_name":"ZERGGROUNDARMORSLEVEL1"}}},{"id":1190,"name":"RESEARCH_ZERGGROUNDARMORLEVEL2","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3704,"target":{"Research":{"upgrade":57,"upgrade_name":"ZERGGROUNDARMORSLEVEL2"}}},{"id":1191,"name":"RESEARCH_ZERGGROUNDARMORLEVEL3","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3704,"target":{"Research":{"upgrade":58,"upgrade_name":"ZERGGROUNDARMORSLEVEL3"}}},{"id":1192,"name":"RESEARCH_ZERGMISSILEWEAPONSLEVEL1","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3706,"target":{"Research":{"upgrade":59,"upgrade_name":"ZERGMISSILEWEAPONSLEVEL1"}}},{"id":1193,"name":"RESEARCH_ZERGMISSILEWEAPONSLEVEL2","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3706,"target":{"Research":{"upgrade":60,"upgrade_name":"ZERGMISSILEWEAPONSLEVEL2"}}},{"id":1194,"name":"RESEARCH_ZERGMISSILEWEAPONSLEVEL3","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3706,"target":{"Research":{"upgrade":61,"upgrade_name":"ZERGMISSILEWEAPONSLEVEL3"}}},{"id":1195,"name":"EVOLUTIONCHAMBERRESEARCH_EVOLVEPROPULSIVEPERISTALSIS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":304,"upgrade_name":"SECRETEDCOATING"}}},{"id":1216,"name":"UPGRADETOLAIR_LAIR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":100,"produces_name":"LAIR"}}},{"id":1217,"name":"CANCEL_MORPHLAIR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1218,"name":"UPGRADETOHIVE_HIVE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":101,"produces_name":"HIVE"}}},{"id":1219,"name":"CANCEL_MORPHHIVE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1220,"name":"UPGRADETOGREATERSPIRE_GREATERSPIRE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":102,"produces_name":"GREATERSPIRE"}}},{"id":1221,"name":"CANCEL_MORPHGREATERSPIRE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1223,"name":"RESEARCH_PNEUMATIZEDCARAPACE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":62,"upgrade_name":"OVERLORDSPEED"}}},{"id":1224,"name":"LAIRRESEARCH_EVOLVEVENTRALSACKS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":63,"upgrade_name":"OVERLORDTRANSPORT"}}},{"id":1225,"name":"RESEARCH_BURROW","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":64,"upgrade_name":"BURROW"}}},{"id":1252,"name":"RESEARCH_ZERGLINGADRENALGLANDS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":65,"upgrade_name":"ZERGLINGATTACKSPEED"}}},{"id":1253,"name":"RESEARCH_ZERGLINGMETABOLICBOOST","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":66,"upgrade_name":"ZERGLINGMOVEMENTSPEED"}}},{"id":1282,"name":"RESEARCH_GROOVEDSPINES","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":134,"upgrade_name":"EVOLVEGROOVEDSPINES"}}},{"id":1283,"name":"RESEARCH_MUSCULARAUGMENTS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":135,"upgrade_name":"EVOLVEMUSCULARAUGMENTS"}}},{"id":1284,"name":"HYDRALISKDENRESEARCH_RESEARCHFRENZY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":298,"upgrade_name":"FRENZY"}}},{"id":1312,"name":"RESEARCH_ZERGFLYERATTACKLEVEL1","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3703,"target":{"Research":{"upgrade":68,"upgrade_name":"ZERGFLYERWEAPONSLEVEL1"}}},{"id":1313,"name":"RESEARCH_ZERGFLYERATTACKLEVEL2","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3703,"target":{"Research":{"upgrade":69,"upgrade_name":"ZERGFLYERWEAPONSLEVEL2"}}},{"id":1314,"name":"RESEARCH_ZERGFLYERATTACKLEVEL3","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3703,"target":{"Research":{"upgrade":70,"upgrade_name":"ZERGFLYERWEAPONSLEVEL3"}}},{"id":1315,"name":"RESEARCH_ZERGFLYERARMORLEVEL1","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3702,"target":{"Research":{"upgrade":71,"upgrade_name":"ZERGFLYERARMORSLEVEL1"}}},{"id":1316,"name":"RESEARCH_ZERGFLYERARMORLEVEL2","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3702,"target":{"Research":{"upgrade":72,"upgrade_name":"ZERGFLYERARMORSLEVEL2"}}},{"id":1317,"name":"RESEARCH_ZERGFLYERARMORLEVEL3","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3702,"target":{"Research":{"upgrade":73,"upgrade_name":"ZERGFLYERARMORSLEVEL3"}}},{"id":1342,"name":"LARVATRAIN_DRONE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":104,"produces_name":"DRONE"}}},{"id":1343,"name":"LARVATRAIN_ZERGLING","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":105,"produces_name":"ZERGLING"}}},{"id":1344,"name":"LARVATRAIN_OVERLORD","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":106,"produces_name":"OVERLORD"}}},{"id":1345,"name":"LARVATRAIN_HYDRALISK","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":107,"produces_name":"HYDRALISK"}}},{"id":1346,"name":"LARVATRAIN_MUTALISK","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":108,"produces_name":"MUTALISK"}}},{"id":1348,"name":"LARVATRAIN_ULTRALISK","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":109,"produces_name":"ULTRALISK"}}},{"id":1351,"name":"LARVATRAIN_ROACH","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":110,"produces_name":"ROACH"}}},{"id":1352,"name":"LARVATRAIN_INFESTOR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":111,"produces_name":"INFESTOR"}}},{"id":1353,"name":"LARVATRAIN_CORRUPTOR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":112,"produces_name":"CORRUPTOR"}}},{"id":1354,"name":"LARVATRAIN_VIPER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":499,"produces_name":"VIPER"}}},{"id":1356,"name":"TRAIN_SWARMHOST","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Train":{"produces":494,"produces_name":"SWARMHOSTMP"}}},{"id":1372,"name":"MORPHTOBROODLORD_BROODLORD","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":114,"produces_name":"BROODLORD"}}},{"id":1373,"name":"CANCEL_MORPHBROODLORD","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1374,"name":"BURROWDOWN_BANELING","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3661,"target":{"Morph":{"produces":115,"produces_name":"BANELINGBURROWED"}}},{"id":1375,"name":"BURROWBANELINGDOWN_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1376,"name":"BURROWUP_BANELING","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3662,"target":{"Morph":{"produces":9,"produces_name":"BANELING"}}},{"id":1378,"name":"BURROWDOWN_DRONE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3661,"target":{"Morph":{"produces":116,"produces_name":"DRONEBURROWED"}}},{"id":1379,"name":"BURROWDRONEDOWN_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1380,"name":"BURROWUP_DRONE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3662,"target":{"Morph":{"produces":104,"produces_name":"DRONE"}}},{"id":1382,"name":"BURROWDOWN_HYDRALISK","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3661,"target":{"Morph":{"produces":117,"produces_name":"HYDRALISKBURROWED"}}},{"id":1383,"name":"BURROWHYDRALISKDOWN_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1384,"name":"BURROWUP_HYDRALISK","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3662,"target":{"Morph":{"produces":107,"produces_name":"HYDRALISK"}}},{"id":1386,"name":"BURROWDOWN_ROACH","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3661,"target":{"Morph":{"produces":118,"produces_name":"ROACHBURROWED"}}},{"id":1387,"name":"BURROWROACHDOWN_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1388,"name":"BURROWUP_ROACH","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3662,"target":{"Morph":{"produces":110,"produces_name":"ROACH"}}},{"id":1390,"name":"BURROWDOWN_ZERGLING","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3661,"target":{"Morph":{"produces":119,"produces_name":"ZERGLINGBURROWED"}}},{"id":1391,"name":"BURROWZERGLINGDOWN_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1392,"name":"BURROWUP_ZERGLING","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3662,"target":{"Morph":{"produces":105,"produces_name":"ZERGLING"}}},{"id":1394,"name":"BURROWDOWN_INFESTORTERRAN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3661,"target":{"Morph":{"produces":120,"produces_name":"INFESTORTERRANBURROWED"}}},{"id":1396,"name":"BURROWUP_INFESTORTERRAN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3662,"target":{"Morph":{"produces":7,"produces_name":"INFESTORTERRAN"}}},{"id":1406,"name":"LOAD_OVERLORD","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit","remaps_to_ability_id":3668},{"id":1408,"name":"UNLOADALLAT_OVERLORD","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit","remaps_to_ability_id":3669},{"id":1409,"name":"UNLOADUNIT_OVERLORD","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3796},{"id":1411,"name":"MERGEABLE_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1412,"name":"WARPABLE_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1413,"name":"WARPGATETRAIN_ZEALOT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"TrainPlace":{"produces":73,"produces_name":"ZEALOT"}}},{"id":1414,"name":"WARPGATETRAIN_STALKER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"TrainPlace":{"produces":74,"produces_name":"STALKER"}}},{"id":1416,"name":"WARPGATETRAIN_HIGHTEMPLAR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"TrainPlace":{"produces":75,"produces_name":"HIGHTEMPLAR"}}},{"id":1417,"name":"WARPGATETRAIN_DARKTEMPLAR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"TrainPlace":{"produces":76,"produces_name":"DARKTEMPLAR"}}},{"id":1418,"name":"WARPGATETRAIN_SENTRY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"TrainPlace":{"produces":77,"produces_name":"SENTRY"}}},{"id":1419,"name":"TRAINWARP_ADEPT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"TrainPlace":{"produces":311,"produces_name":"ADEPT"}}},{"id":1433,"name":"BURROWDOWN_QUEEN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3661,"target":{"Morph":{"produces":125,"produces_name":"QUEENBURROWED"}}},{"id":1434,"name":"BURROWQUEENDOWN_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1435,"name":"BURROWUP_QUEEN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3662,"target":{"Morph":{"produces":126,"produces_name":"QUEEN"}}},{"id":1437,"name":"LOAD_NYDUSNETWORK","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit","remaps_to_ability_id":3668},{"id":1438,"name":"UNLOADALL_NYDASNETWORK","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3664},{"id":1440,"name":"UNLOADUNIT_NYDASNETWORK","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3796},{"id":1442,"name":"EFFECT_BLINK_STALKER","cast_range":500.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point","remaps_to_ability_id":3687},{"id":1444,"name":"BURROWDOWN_INFESTOR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3661,"target":{"Morph":{"produces":127,"produces_name":"INFESTORBURROWED"}}},{"id":1445,"name":"BURROWINFESTORDOWN_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1446,"name":"BURROWUP_INFESTOR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3662,"target":{"Morph":{"produces":111,"produces_name":"INFESTOR"}}},{"id":1448,"name":"MORPH_OVERSEER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":129,"produces_name":"OVERSEER"}}},{"id":1449,"name":"CANCEL_MORPHOVERSEER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1450,"name":"UPGRADETOPLANETARYFORTRESS_PLANETARYFORTRESS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":130,"produces_name":"PLANETARYFORTRESS"}}},{"id":1451,"name":"CANCEL_MORPHPLANETARYFORTRESS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1455,"name":"RESEARCH_NEURALPARASITE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":101,"upgrade_name":"NEURALPARASITE"}}},{"id":1456,"name":"INFESTATIONPITRESEARCH_RESEARCHLOCUSTLIFETIMEINCREASE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":102,"upgrade_name":"LOCUSTLIFETIMEINCREASE"}}},{"id":1457,"name":"INFESTATIONPITRESEARCH_EVOLVEAMORPHOUSARMORCLOUD","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":299,"upgrade_name":"MICROBIALSHROUD"}}},{"id":1482,"name":"RESEARCH_CENTRIFUGALHOOKS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":75,"upgrade_name":"CENTRIFICALHOOKS"}}},{"id":1512,"name":"BURROWDOWN_ULTRALISK","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3661,"target":{"Morph":{"produces":131,"produces_name":"ULTRALISKBURROWED"}}},{"id":1514,"name":"BURROWUP_ULTRALISK","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3662,"target":{"Morph":{"produces":109,"produces_name":"ULTRALISK"}}},{"id":1516,"name":"UPGRADETOORBITAL_ORBITALCOMMAND","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":132,"produces_name":"ORBITALCOMMAND"}}},{"id":1517,"name":"CANCEL_MORPHORBITAL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1518,"name":"MORPH_WARPGATE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":133,"produces_name":"WARPGATE"}}},{"id":1519,"name":"UPGRADETOWARPGATE_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1520,"name":"MORPH_GATEWAY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":62,"produces_name":"GATEWAY"}}},{"id":1521,"name":"MORPHBACKTOGATEWAY_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1522,"name":"LIFT_ORBITALCOMMAND","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3679,"target":{"Morph":{"produces":134,"produces_name":"ORBITALCOMMANDFLYING"}}},{"id":1524,"name":"LAND_ORBITALCOMMAND","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3678,"target":{"MorphPlace":{"produces":132,"produces_name":"ORBITALCOMMAND"}}},{"id":1526,"name":"FORCEFIELD_FORCEFIELD","cast_range":9.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":1527,"name":"FORCEFIELD_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1528,"name":"MORPH_WARPPRISMPHASINGMODE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":136,"produces_name":"WARPPRISMPHASING"}}},{"id":1529,"name":"PHASINGMODE_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1530,"name":"MORPH_WARPPRISMTRANSPORTMODE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":81,"produces_name":"WARPPRISM"}}},{"id":1531,"name":"TRANSPORTMODE_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1532,"name":"RESEARCH_BATTLECRUISERWEAPONREFIT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":76,"upgrade_name":"BATTLECRUISERENABLESPECIALIZATIONS"}}},{"id":1533,"name":"FUSIONCORERESEARCH_RESEARCHBALLISTICRANGE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":140,"upgrade_name":"LIBERATORAGRANGEUPGRADE"}}},{"id":1534,"name":"FUSIONCORERESEARCH_RESEARCHRAPIDREIGNITIONSYSTEM","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":139,"upgrade_name":"MEDIVACINCREASESPEEDBOOST"}}},{"id":1535,"name":"FUSIONCORERESEARCH_RESEARCHMEDIVACENERGYUPGRADE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":21,"upgrade_name":"MEDIVACCADUCEUSREACTOR"}}},{"id":1562,"name":"CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL1","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3693,"target":{"Research":{"upgrade":78,"upgrade_name":"PROTOSSAIRWEAPONSLEVEL1"}}},{"id":1563,"name":"CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL2","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3693,"target":{"Research":{"upgrade":79,"upgrade_name":"PROTOSSAIRWEAPONSLEVEL2"}}},{"id":1564,"name":"CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL3","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3693,"target":{"Research":{"upgrade":80,"upgrade_name":"PROTOSSAIRWEAPONSLEVEL3"}}},{"id":1565,"name":"CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL1","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3692,"target":{"Research":{"upgrade":81,"upgrade_name":"PROTOSSAIRARMORSLEVEL1"}}},{"id":1566,"name":"CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL2","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3692,"target":{"Research":{"upgrade":82,"upgrade_name":"PROTOSSAIRARMORSLEVEL2"}}},{"id":1567,"name":"CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL3","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3692,"target":{"Research":{"upgrade":83,"upgrade_name":"PROTOSSAIRARMORSLEVEL3"}}},{"id":1568,"name":"RESEARCH_WARPGATE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":84,"upgrade_name":"WARPGATERESEARCH"}}},{"id":1571,"name":"CYBERNETICSCORERESEARCH_RESEARCHHALLUCINATION","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":85,"upgrade_name":"HALTECH"}}},{"id":1592,"name":"RESEARCH_CHARGE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":86,"upgrade_name":"CHARGE"}}},{"id":1593,"name":"RESEARCH_BLINK","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":87,"upgrade_name":"BLINKTECH"}}},{"id":1594,"name":"RESEARCH_ADEPTRESONATINGGLAIVES","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":130,"upgrade_name":"ADEPTPIERCINGATTACK"}}},{"id":1595,"name":"TWILIGHTCOUNCILRESEARCH_RESEARCHPSIONICSURGE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":301,"upgrade_name":"SUNDERINGIMPACT"}}},{"id":1596,"name":"TWILIGHTCOUNCILRESEARCH_RESEARCHAMPLIFIEDSHIELDING","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":302,"upgrade_name":"AMPLIFIEDSHIELDING"}}},{"id":1597,"name":"TWILIGHTCOUNCILRESEARCH_RESEARCHPSIONICAMPLIFIERS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":303,"upgrade_name":"PSIONICAMPLIFIERS"}}},{"id":1622,"name":"TACNUKESTRIKE_NUKECALLDOWN","cast_range":12.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":1623,"name":"CANCEL_NUKE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1628,"name":"EMP_EMP","cast_range":10.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":1632,"name":"TRAINQUEEN_QUEEN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":126}}},{"id":1662,"name":"BURROWCREEPTUMORDOWN_BURROWDOWN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3661,"target":{"Morph":{"produces":137,"produces_name":"CREEPTUMORBURROWED"}}},{"id":1664,"name":"TRANSFUSION_TRANSFUSION","cast_range":7.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":1666,"name":"TECHLABMORPH_TECHLABMORPH","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":5,"produces_name":"TECHLAB"}}},{"id":1668,"name":"BARRACKSTECHLABMORPH_TECHLABBARRACKS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":5,"produces_name":"TECHLAB"}}},{"id":1670,"name":"FACTORYTECHLABMORPH_TECHLABFACTORY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":5,"produces_name":"TECHLAB"}}},{"id":1672,"name":"STARPORTTECHLABMORPH_TECHLABSTARPORT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":5,"produces_name":"TECHLAB"}}},{"id":1674,"name":"REACTORMORPH_REACTORMORPH","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":6,"produces_name":"REACTOR"}}},{"id":1676,"name":"BARRACKSREACTORMORPH_REACTOR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":6,"produces_name":"REACTOR"}}},{"id":1678,"name":"FACTORYREACTORMORPH_REACTOR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":6,"produces_name":"REACTOR"}}},{"id":1680,"name":"STARPORTREACTORMORPH_REACTOR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":6,"produces_name":"REACTOR"}}},{"id":1682,"name":"ATTACK_REDIRECT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit","remaps_to_ability_id":3674},{"id":1683,"name":"EFFECT_STIM_MARINE_REDIRECT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3675},{"id":1684,"name":"EFFECT_STIM_MARAUDER_REDIRECT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3675},{"id":1691,"name":"STOP_REDIRECT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3665},{"id":1692,"name":"BEHAVIOR_GENERATECREEPON","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":1693,"name":"BEHAVIOR_GENERATECREEPOFF","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":1694,"name":"BUILD_CREEPTUMOR_QUEEN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3691,"target":{"Build":{"produces":138,"produces_name":"CREEPTUMORQUEEN"}}},{"id":1724,"name":"QUEENBUILD_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3660},{"id":1725,"name":"SPINECRAWLERUPROOT_SPINECRAWLERUPROOT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3681,"target":{"Morph":{"produces":139,"produces_name":"SPINECRAWLERUPROOTED"}}},{"id":1726,"name":"SPINECRAWLERUPROOT_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1727,"name":"SPORECRAWLERUPROOT_SPORECRAWLERUPROOT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3681,"target":{"Morph":{"produces":140,"produces_name":"SPORECRAWLERUPROOTED"}}},{"id":1728,"name":"SPORECRAWLERUPROOT_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1729,"name":"SPINECRAWLERROOT_SPINECRAWLERROOT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3680,"target":{"MorphPlace":{"produces":98,"produces_name":"SPINECRAWLER"}}},{"id":1730,"name":"CANCEL_SPINECRAWLERROOT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1731,"name":"SPORECRAWLERROOT_SPORECRAWLERROOT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3680,"target":{"MorphPlace":{"produces":99,"produces_name":"SPORECRAWLER"}}},{"id":1732,"name":"CANCEL_SPORECRAWLERROOT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1733,"name":"BUILD_CREEPTUMOR_TUMOR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3691,"target":{"Build":{"produces":87,"produces_name":"CREEPTUMOR"}}},{"id":1763,"name":"CANCEL_CREEPTUMOR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1764,"name":"BUILDAUTOTURRET_AUTOTURRET","cast_range":2.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":31,"produces_name":"AUTOTURRET"}}},{"id":1766,"name":"MORPH_ARCHON","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":141,"produces_name":"ARCHON"}}},{"id":1767,"name":"ARCHON_WARP_TARGET","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":1768,"name":"BUILD_NYDUSWORM","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":142,"produces_name":"NYDUSCANAL"}}},{"id":1769,"name":"BUILDNYDUSCANAL_SUMMONNYDUSCANALATTACKER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":491,"produces_name":"NYDUSCANALATTACKER"}}},{"id":1798,"name":"BUILDNYDUSCANAL_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3660},{"id":1799,"name":"BROODLORDHANGAR_BROODLORDHANGAR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":1819,"name":"EFFECT_CHARGE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":1820,"name":"TOWERCAPTURE_TOWERCAPTURE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":1821,"name":"HERDINTERACT_HERD","cast_range":6.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":1825,"name":"CONTAMINATE_CONTAMINATE","cast_range":3.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":1827,"name":"SHATTER_SHATTER","cast_range":0.10009765625,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":1831,"name":"CANCEL_QUEUEPASIVE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3671},{"id":1832,"name":"CANCELSLOT_QUEUEPASSIVE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3672},{"id":1833,"name":"CANCEL_QUEUEPASSIVECANCELTOSELECTION","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3671},{"id":1834,"name":"CANCELSLOT_QUEUEPASSIVECANCELTOSELECTION","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3672},{"id":1837,"name":"MORPHTOGHOSTNOVA_MOVE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":145,"produces_name":"GHOSTNOVA"}}},{"id":1839,"name":"DIGESTERCREEPSPRAY_DIGESTERCREEPSPRAY","cast_range":500.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":1841,"name":"MORPHTOCOLLAPSIBLETERRANTOWERDEBRIS_MORPHTOCOLLAPSIBLETERRANTOWERDEBRIS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":0,"produces_name":"NOTAUNIT"}}},{"id":1842,"name":"MORPHTOCOLLAPSIBLETERRANTOWERDEBRIS_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1843,"name":"MORPHTOCOLLAPSIBLETERRANTOWERDEBRISRAMPLEFT_MORPHTOCOLLAPSIBLETERRANTOWERDEBRISRAMPLEFT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":0,"produces_name":"NOTAUNIT"}}},{"id":1844,"name":"MORPHTOCOLLAPSIBLETERRANTOWERDEBRISRAMPLEFT_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1845,"name":"MORPHTOCOLLAPSIBLETERRANTOWERDEBRISRAMPRIGHT_MORPHTOCOLLAPSIBLETERRANTOWERDEBRISRAMPRIGHT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":0,"produces_name":"NOTAUNIT"}}},{"id":1846,"name":"MORPHTOCOLLAPSIBLETERRANTOWERDEBRISRAMPRIGHT_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1847,"name":"MORPH_MOTHERSHIP","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":10,"produces_name":"MOTHERSHIP"}}},{"id":1848,"name":"CANCEL_MORPHMOTHERSHIP","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1928,"name":"XELNAGAHEALINGSHRINE_XELNAGAHEALINGSHRINE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":1930,"name":"NEXUSINVULNERABILITY_NEXUSINVULNERABILITY","cast_range":10.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":1974,"name":"EFFECT_MASSRECALL_MOTHERSHIPCORE","cast_range":500.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit","remaps_to_ability_id":3686},{"id":1978,"name":"MORPH_HELLION","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":53,"produces_name":"HELLION"}}},{"id":1996,"name":"MORPHTOCOLLAPSIBLEROCKTOWERDEBRIS_MORPHTOCOLLAPSIBLEROCKTOWERDEBRIS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":0,"produces_name":"NOTAUNIT"}}},{"id":1997,"name":"MORPHTOCOLLAPSIBLEROCKTOWERDEBRIS_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":1998,"name":"MORPH_HELLBAT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":484,"produces_name":"HELLIONTANK"}}},{"id":2014,"name":"BURROWDOWN_SWARMHOST","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3661,"target":{"Morph":{"produces":493,"produces_name":"SWARMHOSTBURROWEDMP"}}},{"id":2015,"name":"MORPHTOSWARMHOSTBURROWEDMP_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":2016,"name":"BURROWUP_SWARMHOST","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3662,"target":{"Morph":{"produces":494,"produces_name":"SWARMHOSTMP"}}},{"id":2048,"name":"ATTACKPROTOSSBUILDING_ATTACKBUILDING","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit","remaps_to_ability_id":3674},{"id":2049,"name":"ATTACKPROTOSSBUILDING_ATTACKTOWARDS","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":2050,"name":"ATTACKPROTOSSBUILDING_ATTACKBARRAGE","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":2057,"name":"STOP_BUILDING","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3665},{"id":2058,"name":"STOPPROTOSSBUILDING_HOLDFIRE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2059,"name":"STOPPROTOSSBUILDING_CHEER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2060,"name":"STOPPROTOSSBUILDING_DANCE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2063,"name":"BLINDINGCLOUD_BLINDINGCLOUD","cast_range":10.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":2067,"name":"EFFECT_ABDUCT","cast_range":9.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":2073,"name":"VIPERCONSUMESTRUCTURE_VIPERCONSUME","cast_range":7.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":2079,"name":"TESTZERG_TESTZERG","cast_range":10.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":2080,"name":"TESTZERG_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":2081,"name":"BEHAVIOR_BUILDINGATTACKON","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2082,"name":"BEHAVIOR_BUILDINGATTACKOFF","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2083,"name":"PICKUPSCRAPSMALL_PICKUPSCRAPSMALL","cast_range":1.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":2085,"name":"PICKUPSCRAPMEDIUM_PICKUPSCRAPMEDIUM","cast_range":1.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":2087,"name":"PICKUPSCRAPLARGE_PICKUPSCRAPLARGE","cast_range":1.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":2089,"name":"PICKUPPALLETGAS_PICKUPPALLETGAS","cast_range":1.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":2091,"name":"PICKUPPALLETMINERALS_PICKUPPALLETMINERALS","cast_range":1.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":2093,"name":"MASSIVEKNOCKOVER_MASSIVEKNOCKOVER","cast_range":1.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":2095,"name":"BURROWDOWN_WIDOWMINE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3661,"target":{"Morph":{"produces":500,"produces_name":"WIDOWMINEBURROWED"}}},{"id":2096,"name":"WIDOWMINEBURROW_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":2097,"name":"BURROWUP_WIDOWMINE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3662,"target":{"Morph":{"produces":498,"produces_name":"WIDOWMINE"}}},{"id":2099,"name":"WIDOWMINEATTACK_WIDOWMINEATTACK","cast_range":5.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":2101,"name":"TORNADOMISSILE_TORNADOMISSILE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":2108,"name":"BURROWDOWN_LURKER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3661,"target":{"Morph":{"produces":503,"produces_name":"LURKERMPBURROWED"}}},{"id":2109,"name":"BURROWLURKERMPDOWN_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":2110,"name":"BURROWUP_LURKER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3662,"target":{"Morph":{"produces":502,"produces_name":"LURKERMP"}}},{"id":2114,"name":"HALLUCINATION_ORACLE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2116,"name":"EFFECT_MEDIVACIGNITEAFTERBURNERS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2146,"name":"ORACLEREVELATION_ORACLEREVELATION","cast_range":12.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":2152,"name":"MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPRIGHT_MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPRIGHT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":0,"produces_name":"NOTAUNIT"}}},{"id":2153,"name":"MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPRIGHT_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":2154,"name":"MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPLEFT_MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPLEFT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":0,"produces_name":"NOTAUNIT"}}},{"id":2155,"name":"MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPLEFT_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":2158,"name":"ULTRALISKWEAPONCOOLDOWN_ULTRALISKWEAPONCOOLDOWN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2162,"name":"EFFECT_PHOTONOVERCHARGE","cast_range":10.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":2172,"name":"XELNAGA_CAVERNS_DOORNEOPENED_XELNAGA_CAVERNS_DOORDEFAULTOPEN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2174,"name":"XELNAGA_CAVERNS_DOORNOPENED_XELNAGA_CAVERNS_DOORDEFAULTOPEN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2178,"name":"XELNAGA_CAVERNS_DOORNWOPENED_XELNAGA_CAVERNS_DOORDEFAULTOPEN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2184,"name":"XELNAGA_CAVERNS_DOORSEOPENED_XELNAGA_CAVERNS_DOORDEFAULTOPEN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2186,"name":"XELNAGA_CAVERNS_DOORSOPENED_XELNAGA_CAVERNS_DOORDEFAULTOPEN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2190,"name":"XELNAGA_CAVERNS_DOORSWOPENED_XELNAGA_CAVERNS_DOORDEFAULTOPEN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2194,"name":"XELNAGA_CAVERNS_DOORWOPENED_XELNAGA_CAVERNS_DOORDEFAULTOPEN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2244,"name":"EFFECT_TIMEWARP","cast_range":9.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":2262,"name":"TARSONIS_DOORN_TARSONIS_DOORN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2264,"name":"TARSONIS_DOORNLOWERED_TARSONIS_DOORNLOWERED","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2266,"name":"TARSONIS_DOORNE_TARSONIS_DOORNE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2268,"name":"TARSONIS_DOORNELOWERED_TARSONIS_DOORNELOWERED","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2270,"name":"TARSONIS_DOORE_TARSONIS_DOORE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2272,"name":"TARSONIS_DOORELOWERED_TARSONIS_DOORELOWERED","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2274,"name":"TARSONIS_DOORNW_TARSONIS_DOORNW","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2276,"name":"TARSONIS_DOORNWLOWERED_TARSONIS_DOORNWLOWERED","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2278,"name":"COMPOUNDMANSION_DOORN_COMPOUNDMANSION_DOORN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2280,"name":"COMPOUNDMANSION_DOORNLOWERED_COMPOUNDMANSION_DOORNLOWERED","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2282,"name":"COMPOUNDMANSION_DOORNE_COMPOUNDMANSION_DOORNE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2284,"name":"COMPOUNDMANSION_DOORNELOWERED_COMPOUNDMANSION_DOORNELOWERED","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2286,"name":"COMPOUNDMANSION_DOORE_COMPOUNDMANSION_DOORE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2288,"name":"COMPOUNDMANSION_DOORELOWERED_COMPOUNDMANSION_DOORELOWERED","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2290,"name":"COMPOUNDMANSION_DOORNW_COMPOUNDMANSION_DOORNW","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2292,"name":"COMPOUNDMANSION_DOORNWLOWERED_COMPOUNDMANSION_DOORNWLOWERED","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2324,"name":"CAUSTICSPRAY_CAUSTICSPRAY","cast_range":6.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":2330,"name":"MORPHTORAVAGER_RAVAGER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":688,"produces_name":"RAVAGER"}}},{"id":2331,"name":"CANCEL_MORPHRAVAGER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":2332,"name":"MORPH_LURKER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":502,"produces_name":"LURKERMP"}}},{"id":2333,"name":"CANCEL_MORPHLURKER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":2338,"name":"EFFECT_CORROSIVEBILE","cast_range":9.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":2340,"name":"BURROWDOWN_RAVAGER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3661,"target":{"Morph":{"produces":690,"produces_name":"RAVAGERBURROWED"}}},{"id":2341,"name":"BURROWRAVAGERDOWN_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":2342,"name":"BURROWUP_RAVAGER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3662,"target":{"Morph":{"produces":688,"produces_name":"RAVAGER"}}},{"id":2344,"name":"PURIFICATIONNOVA_PURIFICATIONNOVA","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2346,"name":"EFFECT_PURIFICATIONNOVA","cast_range":500.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":2350,"name":"LOCKON_LOCKON","cast_range":7.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":2354,"name":"CANCEL_LOCKON","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":2358,"name":"EFFECT_TACTICALJUMP","cast_range":500.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":2362,"name":"MORPH_THORHIGHIMPACTMODE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":691,"produces_name":"THORAP"}}},{"id":2363,"name":"THORAPMODE_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":2364,"name":"MORPH_THOREXPLOSIVEMODE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":52,"produces_name":"THOR"}}},{"id":2365,"name":"CANCEL_MORPHTHOREXPLOSIVEMODE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":2370,"name":"LOAD_NYDUSWORM","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit","remaps_to_ability_id":3668},{"id":2371,"name":"UNLOADALL_NYDUSWORM","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3664},{"id":2375,"name":"BEHAVIOR_PULSARBEAMON","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2376,"name":"BEHAVIOR_PULSARBEAMOFF","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2383,"name":"LOCUSTMPFLYINGMORPHTOGROUND_LOCUSTMPFLYINGSWOOP","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":489,"produces_name":"LOCUSTMP"}}},{"id":2385,"name":"LOCUSTMPMORPHTOAIR_LOCUSTMPFLYINGSWOOP","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":693,"produces_name":"LOCUSTMPFLYING"}}},{"id":2387,"name":"EFFECT_LOCUSTSWOOP","cast_range":6.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":2389,"name":"HALLUCINATION_DISRUPTOR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2391,"name":"HALLUCINATION_ADEPT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2393,"name":"EFFECT_VOIDRAYPRISMATICALIGNMENT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2395,"name":"SEEKERDUMMYCHANNEL_SEEKERDUMMYCHANNEL","cast_range":500.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2469,"name":"VOIDMPIMMORTALREVIVEREBUILD_IMMORTAL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2473,"name":"ARBITERMPSTASISFIELD_ARBITERMPSTASISFIELD","cast_range":9.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit"},{"id":2475,"name":"ARBITERMPRECALL_ARBITERMPRECALL","cast_range":500.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":2477,"name":"CORSAIRMPDISRUPTIONWEB_CORSAIRMPDISRUPTIONWEB","cast_range":9.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":2479,"name":"MORPHTOGUARDIANMP_MORPHTOGUARDIANMP","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":727,"produces_name":"GUARDIANMP"}}},{"id":2480,"name":"MORPHTOGUARDIANMP_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":2481,"name":"MORPHTODEVOURERMP_MORPHTODEVOURERMP","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":729,"produces_name":"DEVOURERMP"}}},{"id":2482,"name":"MORPHTODEVOURERMP_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":2483,"name":"DEFILERMPCONSUME_DEFILERMPCONSUME","cast_range":0.5,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":2485,"name":"DEFILERMPDARKSWARM_DEFILERMPDARKSWARM","cast_range":8.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":2487,"name":"DEFILERMPPLAGUE_DEFILERMPPLAGUE","cast_range":8.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":2489,"name":"DEFILERMPBURROW_BURROWDOWN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3661,"target":{"Morph":{"produces":730,"produces_name":"DEFILERMPBURROWED"}}},{"id":2490,"name":"DEFILERMPBURROW_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":2491,"name":"DEFILERMPUNBURROW_BURROWUP","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"remaps_to_ability_id":3662,"target":{"Morph":{"produces":731,"produces_name":"DEFILERMP"}}},{"id":2493,"name":"QUEENMPENSNARE_QUEENMPENSNARE","cast_range":9.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":2495,"name":"QUEENMPSPAWNBROODLINGS_QUEENMPSPAWNBROODLINGS","cast_range":9.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":2497,"name":"QUEENMPINFESTCOMMANDCENTER_QUEENMPINFESTCOMMANDCENTER","cast_range":1.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2505,"name":"BUILD_STASISTRAP","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":732,"produces_name":"ORACLESTASISTRAP"}}},{"id":2535,"name":"CANCEL_STASISTRAP","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":2536,"name":"ORACLESTASISTRAPACTIVATE_ACTIVATESTASISWARD","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":2542,"name":"PARASITICBOMB_PARASITICBOMB","cast_range":8.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":2544,"name":"ADEPTPHASESHIFT_ADEPTPHASESHIFT","cast_range":500.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":2548,"name":"PURIFICATIONNOVAMORPHBACK_PURIFICATIONNOVA","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":0,"produces_name":"Unknown"}}},{"id":2550,"name":"BEHAVIOR_HOLDFIREON_LURKER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3688},{"id":2552,"name":"BEHAVIOR_HOLDFIREOFF_LURKER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3689},{"id":2554,"name":"LIBERATORMORPHTOAG_LIBERATORAGMODE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":734,"produces_name":"LIBERATORAG"}}},{"id":2556,"name":"LIBERATORMORPHTOAA_LIBERATORAAMODE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":689,"produces_name":"LIBERATOR"}}},{"id":2558,"name":"MORPH_LIBERATORAGMODE","cast_range":5.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"MorphPlace":{"produces":734,"produces_name":"LIBERATORAG"}}},{"id":2560,"name":"MORPH_LIBERATORAAMODE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":689,"produces_name":"LIBERATOR"}}},{"id":2588,"name":"KD8CHARGE_KD8CHARGE","cast_range":5.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit"},{"id":2594,"name":"CANCEL_ADEPTPHASESHIFT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":2596,"name":"CANCEL_ADEPTSHADEPHASESHIFT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":2598,"name":"SLAYNELEMENTALGRAB_SLAYNELEMENTALGRAB","cast_range":10.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":2600,"name":"MORPHTOCOLLAPSIBLEPURIFIERTOWERDEBRIS_MORPHTOCOLLAPSIBLEPURIFIERTOWERDEBRIS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":0,"produces_name":"NOTAUNIT"}}},{"id":2601,"name":"MORPHTOCOLLAPSIBLEPURIFIERTOWERDEBRIS_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":2700,"name":"EFFECT_SHADOWSTRIDE","cast_range":500.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point","remaps_to_ability_id":3687},{"id":2704,"name":"EFFECT_SPAWNLOCUSTS","cast_range":500.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":693}}},{"id":2706,"name":"LOCUSTMPFLYINGSWOOPATTACK_LOCUSTMPFLYINGSWOOP","cast_range":6.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit"},{"id":2708,"name":"MORPH_OVERLORDTRANSPORT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":893,"produces_name":"OVERLORDTRANSPORT"}}},{"id":2709,"name":"CANCEL_MORPHOVERLORDTRANSPORT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":2714,"name":"EFFECT_GHOSTSNIPE","cast_range":10.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":2715,"name":"CHANNELSNIPE_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":2716,"name":"PURIFYMORPHPYLON_MOTHERSHIPCOREWEAPON","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":894,"produces_name":"PYLONOVERCHARGED"}}},{"id":2718,"name":"PURIFYMORPHPYLONBACK_MOTHERSHIPCOREWEAPON","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":0,"produces_name":"Unknown"}}},{"id":2720,"name":"RESEARCH_SHADOWSTRIKE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":141,"upgrade_name":"DARKTEMPLARBLINKUPGRADE"}}},{"id":3659,"name":"CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":3660,"name":"HALT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":3661,"name":"BURROWDOWN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":0,"produces_name":"Unknown"}}},{"id":3662,"name":"BURROWUP","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":0,"produces_name":"Unknown"}}},{"id":3663,"name":"LOADALL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":3664,"name":"UNLOADALL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":3665,"name":"STOP","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":3666,"name":"HARVEST_GATHER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":3667,"name":"HARVEST_RETURN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":3668,"name":"LOAD","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":3669,"name":"UNLOADALLAT","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit"},{"id":3671,"name":"CANCEL_LAST","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":3672,"name":"CANCEL_SLOT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":3673,"name":"RALLY_UNITS","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit"},{"id":3674,"name":"ATTACK","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit"},{"id":3675,"name":"EFFECT_STIM","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":3676,"name":"BEHAVIOR_CLOAKON","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":3677,"name":"BEHAVIOR_CLOAKOFF","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":3678,"name":"LAND","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"MorphPlace":{"produces":0,"produces_name":"Unknown"}}},{"id":3679,"name":"LIFT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":0,"produces_name":"Unknown"}}},{"id":3680,"name":"MORPH_ROOT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"MorphPlace":{"produces":0,"produces_name":"Unknown"}}},{"id":3681,"name":"MORPH_UPROOT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":0,"produces_name":"Unknown"}}},{"id":3682,"name":"BUILD_TECHLAB","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"BuildInstant":{"produces":5,"produces_name":"TECHLAB"}}},{"id":3683,"name":"BUILD_REACTOR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"BuildInstant":{"produces":6,"produces_name":"REACTOR"}}},{"id":3684,"name":"EFFECT_SPRAY","cast_range":1.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":3685,"name":"EFFECT_REPAIR","cast_range":6.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":3686,"name":"EFFECT_MASSRECALL","cast_range":500.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":3687,"name":"EFFECT_BLINK","cast_range":500.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":3688,"name":"BEHAVIOR_HOLDFIREON","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":3689,"name":"BEHAVIOR_HOLDFIREOFF","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":3690,"name":"RALLY_WORKERS","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit"},{"id":3691,"name":"BUILD_CREEPTUMOR","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Build":{"produces":87,"produces_name":"CREEPTUMOR"}}},{"id":3707,"name":"CANCEL_VOIDRAYPRISMATICALIGNMENT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":3709,"name":"RESEARCH_ADAPTIVETALONS","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":293,"upgrade_name":"DIGGINGCLAWS"}}},{"id":3710,"name":"LURKERDENRESEARCH_RESEARCHLURKERRANGE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Research":{"upgrade":127,"upgrade_name":"LURKERRANGE"}}},{"id":3739,"name":"MORPH_OBSERVERMODE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":82,"produces_name":"OBSERVER"}}},{"id":3741,"name":"MORPH_SURVEILLANCEMODE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":1911,"produces_name":"OBSERVERSIEGEMODE"}}},{"id":3743,"name":"MORPH_OVERSIGHTMODE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":1912,"produces_name":"OVERSEERSIEGEMODE"}}},{"id":3745,"name":"MORPH_OVERSEERMODE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":129,"produces_name":"OVERSEER"}}},{"id":3747,"name":"EFFECT_INTERFERENCEMATRIX","cast_range":9.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":3751,"name":"EFFECT_REPAIR_REPAIRDRONE","cast_range":6.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"Unit","remaps_to_ability_id":3685},{"id":3753,"name":"EFFECT_ANTIARMORMISSILE","cast_range":10.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":3755,"name":"EFFECT_CHRONOBOOSTENERGYCOST","cast_range":500.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":3757,"name":"EFFECT_MASSRECALL_NEXUS","cast_range":500.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point","remaps_to_ability_id":3686},{"id":3763,"name":"INFESTORENSNARE_INFESTORENSNARE","cast_range":8.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":3771,"name":"ATTACK_BATTLECRUISER","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit","remaps_to_ability_id":3674},{"id":3772,"name":"BATTLECRUISERATTACK_ATTACKTOWARDS","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":3773,"name":"BATTLECRUISERATTACK_ATTACKBARRAGE","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":3776,"name":"MOVE_BATTLECRUISER","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit","remaps_to_ability_id":3794},{"id":3777,"name":"PATROL_BATTLECRUISER","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit","remaps_to_ability_id":3795},{"id":3778,"name":"HOLDPOSITION_BATTLECRUISER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3793},{"id":3779,"name":"BATTLECRUISERMOVE_ACQUIREMOVE","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit"},{"id":3780,"name":"BATTLECRUISERMOVE_TURN","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit"},{"id":3783,"name":"STOP_BATTLECRUISER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3665},{"id":3784,"name":"BATTLECRUISERSTOP_HOLDFIRE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":3785,"name":"BATTLECRUISERSTOP_CHEER","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":3786,"name":"BATTLECRUISERSTOP_DANCE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":3789,"name":"VIPERPARASITICBOMBRELAY_PARASITICBOMB","cast_range":500.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":3791,"name":"PARASITICBOMBRELAYDODGE_PARASITICBOMB","cast_range":500.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":3793,"name":"HOLDPOSITION","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":3794,"name":"MOVE","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit"},{"id":3795,"name":"PATROL","cast_range":0.0,"energy_cost":0,"allow_minimap":true,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"PointOrUnit"},{"id":3796,"name":"UNLOADUNIT","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":3966,"name":"MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPLEFTGREEN_MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPLEFTGREEN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":0,"produces_name":"NOTAUNIT"}}},{"id":3967,"name":"MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPLEFTGREEN_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":3969,"name":"MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPRIGHTGREEN_MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPRIGHTGREEN","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":0,"produces_name":"NOTAUNIT"}}},{"id":3970,"name":"MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPRIGHTGREEN_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":4109,"name":"HYDRALISKFRENZY_HYDRALISKFRENZY","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":4111,"name":"AMORPHOUSARMORCLOUD_AMORPHOUSARMORCLOUD","cast_range":9.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Point"},{"id":4113,"name":"SHIELDBATTERYRECHARGEEX5_SHIELDBATTERYRECHARGE","cast_range":6.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":true,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":4114,"name":"SHIELDBATTERYRECHARGEEX5_STOP","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":4121,"name":"MORPHTOBANELING_BANELING","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":{"Morph":{"produces":0,"produces_name":"NOTAUNIT"}}},{"id":4122,"name":"MORPHTOBANELING_CANCEL","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None","remaps_to_ability_id":3659},{"id":4124,"name":"MOTHERSHIPCLOAK_ORACLECLOAKFIELD","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":4126,"name":"ENERGYRECHARGE_ENERGYRECHARGE","cast_range":500.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"},{"id":4128,"name":"SALVAGEEFFECT_SALVAGE","cast_range":0.0,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"None"},{"id":4132,"name":"WORKERSTOPIDLEABILITYVESPENE_GATHER","cast_range":0.300048828125,"energy_cost":0,"allow_minimap":false,"allow_autocast":false,"effect":[],"buff":[],"cooldown":0,"target":"Unit"}],"Unit":[{"id":4,"name":"Colossus","race":"Protoss","supply":6.0,"cargo_size":8,"max_health":250.0,"armor":1.0,"sight":10.0,"speed":2.25,"speed_creep_mul":1.0,"attributes":["Armored","Mechanical","Massive"],"size":0,"radius":1.0,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":300,"gas":200,"time":1200.0,"tech_alias":[],"unit_alias":0,"max_shield":100.0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":10.0,"damage_splash":0,"attacks":2,"range":7.0,"cooldown":1.5,"bonuses":[{"against":"Light","damage":5.0}]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1}]},{"id":5,"name":"TechLab","race":"Terran","supply":0.0,"max_health":400.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"abilities":[{"ability":730,"requirements":[{"addon_to":21}]},{"ability":731,"requirements":[{"addon_to":21}]},{"ability":732,"requirements":[{"addon_to":21}]},{"ability":761,"requirements":[{"addon_to":27}]},{"ability":764,"requirements":[{"addon_to":27}]},{"ability":793,"requirements":[{"addon_to":28}]},{"ability":790,"requirements":[{"addon_to":28}]}],"size":0,"radius":1.125,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":true,"is_worker":false,"is_townhall":false,"minerals":50,"gas":25,"time":2.0,"tech_alias":[],"unit_alias":0,"is_flying":false},{"id":6,"name":"Reactor","race":"Terran","supply":0.0,"max_health":400.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"abilities":[],"size":0,"radius":1.125,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":true,"is_worker":false,"is_townhall":false,"minerals":50,"gas":50,"time":2.0,"tech_alias":[],"unit_alias":0,"is_flying":false},{"id":7,"name":"InfestorTerran","race":"Zerg","supply":0.0,"max_health":75.0,"armor":0.0,"sight":9.0,"speed":0.9375,"speed_creep_mul":1.0,"attributes":["Light","Biological"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":78.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Air","damage_per_hit":24.0,"damage_splash":0,"attacks":1,"range":6.0,"cooldown":1.330078125,"bonuses":[]},{"target_type":"Ground","damage_per_hit":12.0,"damage_splash":0,"attacks":1,"range":5.0,"cooldown":0.86083984375,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1},{"requirements":[{"upgrade":64}],"ability":1394}]},{"id":8,"name":"BanelingCocoon","race":"Zerg","supply":0.5,"max_health":50.0,"armor":2.0,"sight":5.0,"speed":2.5,"speed_creep_mul":1.0,"weapons":[],"attributes":["Biological"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":50,"gas":25,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":195},{"ability":1}]},{"id":9,"name":"Baneling","race":"Zerg","supply":0.5,"cargo_size":2,"max_health":30.0,"armor":0.0,"sight":8.0,"speed":2.5,"speed_creep_mul":1.0,"weapons":[],"attributes":["Biological"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":50,"gas":25,"time":320.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":42},{"ability":2081},{"ability":1},{"requirements":[{"upgrade":64}],"ability":1374}]},{"id":10,"name":"Mothership","race":"Protoss","supply":8.0,"max_health":350.0,"armor":2.0,"sight":14.0,"speed":2.015625,"speed_creep_mul":1.0,"attributes":["Armored","Mechanical","Psionic","Massive","Heroic"],"size":0,"radius":1.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":400,"gas":400,"time":2000.0,"tech_alias":[],"unit_alias":0,"max_shield":350.0,"is_flying":true,"weapons":[{"target_type":"Any","damage_per_hit":6.0,"damage_splash":0,"attacks":4,"range":7.0,"cooldown":2.2099609375,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":142},{"ability":2244},{"ability":4124},{"ability":1}]},{"id":11,"name":"PointDefenseDrone","race":"Terran","supply":0.0,"max_health":50.0,"armor":0.0,"sight":7.0,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":200,"weapons":[],"attributes":["Light","Mechanical","Structure"],"abilities":[],"size":0,"radius":0.625,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":true},{"id":12,"name":"Changeling","race":"Zerg","supply":0.0,"max_health":5.0,"armor":0.0,"sight":8.0,"speed":2.25,"speed_creep_mul":1.0,"weapons":[],"attributes":["Light","Biological"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":1}]},{"id":13,"name":"ChangelingZealot","normal_mode":12,"race":"Zerg","supply":0.0,"max_health":100.0,"armor":1.0,"sight":9.0,"speed":2.25,"speed_creep_mul":1.0,"attributes":["Light","Biological"],"size":0,"radius":0.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":8.0,"tech_alias":[],"unit_alias":12,"max_shield":50.0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":8.0,"damage_splash":0,"attacks":2,"range":0.10009765625,"cooldown":1.199951171875,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":1}]},{"id":14,"name":"ChangelingMarineShield","normal_mode":12,"race":"Zerg","supply":0.0,"max_health":55.0,"armor":0.0,"sight":9.0,"speed":2.25,"speed_creep_mul":1.0,"attributes":["Light","Biological"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":8.0,"tech_alias":[],"unit_alias":12,"is_flying":false,"weapons":[{"target_type":"Any","damage_per_hit":6.0,"damage_splash":0,"attacks":1,"range":5.0,"cooldown":0.86083984375,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":1}]},{"id":15,"name":"ChangelingMarine","normal_mode":12,"race":"Zerg","supply":0.0,"max_health":45.0,"armor":0.0,"sight":9.0,"speed":2.25,"speed_creep_mul":1.0,"attributes":["Light","Biological"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":8.0,"tech_alias":[],"unit_alias":12,"is_flying":false,"weapons":[{"target_type":"Any","damage_per_hit":6.0,"damage_splash":0,"attacks":1,"range":5.0,"cooldown":0.86083984375,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":1}]},{"id":16,"name":"ChangelingZerglingWings","normal_mode":12,"race":"Zerg","supply":0.0,"max_health":35.0,"armor":0.0,"sight":8.0,"speed":2.953125,"speed_creep_mul":1.0,"attributes":["Light","Biological"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":8.0,"tech_alias":[],"unit_alias":12,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":5.0,"damage_splash":0,"attacks":1,"range":0.10009765625,"cooldown":0.696044921875,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":1}]},{"id":17,"name":"ChangelingZergling","normal_mode":12,"race":"Zerg","supply":0.0,"max_health":35.0,"armor":0.0,"sight":8.0,"speed":2.953125,"speed_creep_mul":1.0,"attributes":["Light","Biological"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":8.0,"tech_alias":[],"unit_alias":12,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":5.0,"damage_splash":0,"attacks":1,"range":0.10009765625,"cooldown":0.696044921875,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":1}]},{"id":18,"name":"CommandCenter","race":"Terran","supply":-15.0,"cargo_capacity":5,"max_health":1500.0,"armor":1.0,"sight":11.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":2.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":true,"minerals":400,"gas":0,"time":1600.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":203},{"ability":416},{"ability":417},{"ability":524},{"ability":1},{"requirements":[{"building":22}],"ability":1450},{"requirements":[{"building":21}],"ability":1516}]},{"id":19,"name":"SupplyDepot","race":"Terran","supply":-8.0,"max_health":400.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":1.25,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":0,"time":480.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":556}]},{"id":20,"name":"Refinery","race":"Terran","supply":0.0,"max_health":500.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"abilities":[],"size":0,"radius":1.6875,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":true,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":75,"gas":0,"time":480.0,"tech_alias":[],"unit_alias":0,"is_flying":false},{"id":21,"name":"Barracks","race":"Terran","supply":0.0,"max_health":1000.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":1.8125,"accepts_addon":true,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":0,"time":1040.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":195},{"ability":421},{"ability":422},{"ability":452},{"ability":560},{"ability":561},{"ability":1},{"requirements":[{"building":26,"addon":5}],"ability":562},{"requirements":[{"addon":5}],"ability":563}]},{"id":22,"name":"EngineeringBay","race":"Terran","supply":0.0,"max_health":850.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":1.8125,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":125,"gas":0,"time":560.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":650},{"ability":651},{"ability":652},{"ability":656},{"ability":653,"requirements":[{"upgrade":7},{"building":29}]},{"ability":654,"requirements":[{"upgrade":8},{"building":29}]},{"ability":657,"requirements":[{"upgrade":11},{"building":29}]},{"ability":658,"requirements":[{"upgrade":12},{"building":29}]}]},{"id":23,"name":"MissileTurret","race":"Terran","supply":0.0,"max_health":250.0,"armor":0.0,"sight":11.0,"detection_range":11.0,"speed_creep_mul":1.0,"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":1.125,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":0,"time":400.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Air","damage_per_hit":12.0,"damage_splash":0,"attacks":2,"range":7.0,"cooldown":0.86083984375,"bonuses":[]}],"abilities":[{"ability":4},{"ability":23},{"ability":1}]},{"id":24,"name":"Bunker","race":"Terran","supply":0.0,"cargo_capacity":4,"max_health":400.0,"armor":1.0,"sight":10.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":1.8125,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":0,"time":640.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":195},{"ability":407},{"ability":4128},{"ability":1}]},{"id":25,"name":"SensorTower","race":"Terran","supply":0.0,"max_health":200.0,"armor":0.0,"sight":12.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":50,"time":400.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":4128}]},{"id":26,"name":"GhostAcademy","race":"Terran","supply":0.0,"max_health":1250.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":1.8125,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":50,"time":640.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":820},{"requirements":[{"building":27}],"ability":710}]},{"id":27,"name":"Factory","race":"Terran","supply":0.0,"max_health":1250.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":1.8125,"accepts_addon":true,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":100,"time":960.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":195},{"ability":454},{"ability":455},{"ability":485},{"ability":595},{"ability":614},{"ability":1},{"requirements":[{"addon":5}],"ability":591},{"requirements":[{"addon":5},{"building":29}],"ability":594},{"requirements":[{"building":29}],"ability":596},{"requirements":[{"addon":5}],"ability":597}]},{"id":28,"name":"Starport","race":"Terran","supply":0.0,"max_health":1300.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":1.8125,"accepts_addon":true,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":100,"time":800.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":195},{"ability":487},{"ability":488},{"ability":518},{"ability":620},{"ability":624},{"ability":626},{"ability":1},{"requirements":[{"addon":5}],"ability":621},{"requirements":[{"addon":5}],"ability":622},{"requirements":[{"addon":5},{"building":30}],"ability":623}]},{"id":29,"name":"Armory","race":"Terran","supply":0.0,"max_health":750.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":1.8125,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":50,"time":1040.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":855},{"ability":861},{"ability":864},{"ability":856,"requirements":[{"upgrade":30}]},{"ability":857,"requirements":[{"upgrade":31}]},{"ability":862,"requirements":[{"upgrade":36}]},{"ability":863,"requirements":[{"upgrade":37}]},{"ability":865,"requirements":[{"upgrade":116}]},{"ability":866,"requirements":[{"upgrade":117}]}]},{"id":30,"name":"FusionCore","race":"Terran","supply":0.0,"max_health":750.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":1.8125,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":150,"time":1040.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":1532},{"ability":1533},{"ability":1535}]},{"id":31,"name":"AutoTurret","race":"Terran","supply":0.0,"max_health":100.0,"armor":0.0,"sight":7.0,"speed_creep_mul":1.0,"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":1.0,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":0,"time":16.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Any","damage_per_hit":18.0,"damage_splash":0,"attacks":1,"range":6.0,"cooldown":0.800048828125,"bonuses":[]}],"abilities":[{"ability":4},{"ability":23},{"ability":1}]},{"id":32,"name":"SiegeTankSieged","normal_mode":33,"race":"Terran","supply":3.0,"max_health":175.0,"armor":1.0,"sight":11.0,"speed_creep_mul":1.0,"attributes":["Armored","Mechanical"],"size":0,"radius":0.875,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":125,"time":68.66796875,"tech_alias":[33],"unit_alias":33,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":40.0,"damage_splash":0,"attacks":1,"range":13.0,"cooldown":3.0,"bonuses":[{"against":"Armored","damage":30.0}]}],"abilities":[{"ability":4},{"ability":23},{"ability":390},{"ability":1}]},{"id":33,"name":"SiegeTank","race":"Terran","supply":3.0,"cargo_size":4,"max_health":175.0,"armor":1.0,"sight":11.0,"speed":2.25,"speed_creep_mul":1.0,"attributes":["Armored","Mechanical"],"size":0,"radius":0.875,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":125,"time":720.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":15.0,"damage_splash":0,"attacks":1,"range":7.0,"cooldown":1.0400390625,"bonuses":[{"against":"Armored","damage":10.0}]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":388},{"ability":1}]},{"id":34,"name":"VikingAssault","normal_mode":35,"race":"Terran","supply":2.0,"cargo_size":2,"max_health":135.0,"armor":0.0,"sight":10.0,"speed":2.25,"speed_creep_mul":1.0,"attributes":["Armored","Mechanical"],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":75,"time":41.44140625,"tech_alias":[1940],"unit_alias":35,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":12.0,"damage_splash":0,"attacks":1,"range":6.0,"cooldown":1.0,"bonuses":[{"against":"Mechanical","damage":8.0}]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":405},{"ability":1}]},{"id":35,"name":"VikingFighter","race":"Terran","supply":2.0,"max_health":135.0,"armor":0.0,"sight":10.0,"speed":2.75,"speed_creep_mul":1.0,"attributes":["Armored","Mechanical"],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":75,"time":672.0,"tech_alias":[1940],"unit_alias":0,"is_flying":true,"weapons":[{"target_type":"Air","damage_per_hit":10.0,"damage_splash":0,"attacks":2,"range":9.0,"cooldown":2.0,"bonuses":[{"against":"Armored","damage":4.0}]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":403},{"ability":1}]},{"id":36,"name":"CommandCenterFlying","normal_mode":18,"race":"Terran","supply":-15.0,"cargo_capacity":5,"max_health":1500.0,"armor":1.0,"sight":11.0,"speed":0.9375,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":2.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":true,"minerals":400,"gas":0,"time":32.0,"tech_alias":[18],"unit_alias":18,"is_flying":true,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":416},{"ability":419},{"ability":1}]},{"id":37,"name":"BarracksTechLab","normal_mode":5,"race":"Terran","supply":0.0,"max_health":400.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":1.125,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":true,"is_worker":false,"is_townhall":false,"minerals":50,"gas":25,"time":400.0,"tech_alias":[5],"unit_alias":0,"is_flying":false,"abilities":[{"ability":730},{"ability":731},{"ability":732}]},{"id":38,"name":"BarracksReactor","normal_mode":6,"race":"Terran","supply":0.0,"max_health":400.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"abilities":[],"size":0,"radius":1.125,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":true,"is_worker":false,"is_townhall":false,"minerals":50,"gas":50,"time":800.0,"tech_alias":[6],"unit_alias":0,"is_flying":false},{"id":39,"name":"FactoryTechLab","normal_mode":5,"race":"Terran","supply":0.0,"max_health":400.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":1.125,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":true,"is_worker":false,"is_townhall":false,"minerals":50,"gas":25,"time":400.0,"tech_alias":[5],"unit_alias":0,"is_flying":false,"abilities":[{"ability":761},{"ability":769},{"ability":764,"requirements":[{"building":29}]},{"ability":766,"requirements":[{"building":29}]}]},{"id":40,"name":"FactoryReactor","normal_mode":6,"race":"Terran","supply":0.0,"max_health":400.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"abilities":[],"size":0,"radius":1.125,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":true,"is_worker":false,"is_townhall":false,"minerals":50,"gas":50,"time":800.0,"tech_alias":[6],"unit_alias":0,"is_flying":false},{"id":41,"name":"StarportTechLab","normal_mode":5,"race":"Terran","supply":0.0,"max_health":400.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":1.125,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":true,"is_worker":false,"is_townhall":false,"minerals":50,"gas":25,"time":400.0,"tech_alias":[5],"unit_alias":0,"is_flying":false,"abilities":[{"ability":790},{"ability":799},{"ability":807}]},{"id":42,"name":"StarportReactor","normal_mode":6,"race":"Terran","supply":0.0,"max_health":400.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"abilities":[],"size":0,"radius":1.125,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":true,"is_worker":false,"is_townhall":false,"minerals":50,"gas":50,"time":800.0,"tech_alias":[6],"unit_alias":0,"is_flying":false},{"id":43,"name":"FactoryFlying","normal_mode":27,"race":"Terran","supply":0.0,"max_health":1250.0,"armor":1.0,"sight":9.0,"speed":0.9375,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":1.625,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":100,"time":32.0,"tech_alias":[27],"unit_alias":27,"is_flying":true,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":454},{"ability":455},{"ability":520},{"ability":1}]},{"id":44,"name":"StarportFlying","normal_mode":28,"race":"Terran","supply":0.0,"max_health":1300.0,"armor":1.0,"sight":9.0,"speed":0.9375,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":1.625,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":100,"time":32.0,"tech_alias":[28],"unit_alias":28,"is_flying":true,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":487},{"ability":488},{"ability":522},{"ability":1}]},{"id":45,"name":"SCV","race":"Terran","supply":1.0,"cargo_size":1,"max_health":45.0,"armor":0.0,"sight":8.0,"speed":2.8125,"speed_creep_mul":1.0,"attributes":["Light","Biological","Mechanical"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":true,"is_townhall":false,"minerals":50,"gas":0,"time":272.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":5.0,"damage_splash":0,"attacks":1,"range":0.199951171875,"cooldown":1.5,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":26},{"ability":295},{"ability":316},{"ability":318},{"ability":319},{"ability":320},{"ability":1},{"requirements":[{"building":19}],"ability":321},{"requirements":[{"building":18}],"ability":322},{"requirements":[{"building":22}],"ability":323},{"requirements":[{"building":21}],"ability":324},{"requirements":[{"building":22}],"ability":326},{"requirements":[{"building":21}],"ability":327},{"requirements":[{"building":21}],"ability":328},{"requirements":[{"building":27}],"ability":329},{"requirements":[{"building":27}],"ability":331},{"requirements":[{"building":28}],"ability":333}]},{"id":46,"name":"BarracksFlying","normal_mode":21,"race":"Terran","supply":0.0,"max_health":1000.0,"armor":1.0,"sight":9.0,"speed":0.9375,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":1.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":0,"time":32.0,"tech_alias":[21],"unit_alias":21,"is_flying":true,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":421},{"ability":422},{"ability":554},{"ability":1}]},{"id":47,"name":"SupplyDepotLowered","normal_mode":19,"race":"Terran","supply":-8.0,"max_health":400.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":1.25,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":0,"time":20.80078125,"tech_alias":[19],"unit_alias":19,"is_flying":false,"abilities":[{"ability":558}]},{"id":48,"name":"Marine","race":"Terran","supply":1.0,"cargo_size":1,"max_health":45.0,"armor":0.0,"sight":9.0,"speed":2.25,"speed_creep_mul":1.0,"attributes":["Light","Biological"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":50,"gas":0,"time":400.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Any","damage_per_hit":6.0,"damage_splash":0,"attacks":1,"range":5.0,"cooldown":0.86083984375,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1},{"requirements":[{"upgrade":15}],"ability":380}]},{"id":49,"name":"Reaper","race":"Terran","supply":1.0,"cargo_size":1,"max_health":60.0,"armor":0.0,"sight":9.0,"speed":3.75,"speed_creep_mul":1.0,"attributes":["Light","Biological"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":50,"gas":50,"time":720.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":4.0,"damage_splash":0,"attacks":2,"range":5.0,"cooldown":1.10009765625,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":2588},{"ability":1}]},{"id":50,"name":"Ghost","race":"Terran","supply":3.0,"cargo_size":2,"max_health":100.0,"armor":0.0,"sight":11.0,"speed":2.8125,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":75,"attributes":["Biological","Psionic"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":125,"time":640.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Any","damage_per_hit":10.0,"damage_splash":0,"attacks":1,"range":6.0,"cooldown":1.5,"bonuses":[{"against":"Light","damage":10.0}]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":36},{"ability":1628},{"ability":2714},{"ability":1},{"requirements":[{"upgrade":25}],"ability":382}]},{"id":51,"name":"Marauder","race":"Terran","supply":2.0,"cargo_size":2,"max_health":125.0,"armor":1.0,"sight":10.0,"speed":2.25,"speed_creep_mul":1.0,"attributes":["Armored","Biological"],"size":0,"radius":0.5625,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":25,"time":480.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":10.0,"damage_splash":0,"attacks":1,"range":6.0,"cooldown":1.5,"bonuses":[{"against":"Armored","damage":10.0}]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1},{"requirements":[{"upgrade":15}],"ability":253}]},{"id":52,"name":"Thor","race":"Terran","supply":6.0,"cargo_size":8,"max_health":400.0,"armor":1.0,"sight":11.0,"speed":1.875,"speed_creep_mul":1.0,"attributes":["Armored","Mechanical","Massive"],"size":0,"radius":1.0,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":300,"gas":200,"time":960.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Air","damage_per_hit":6.0,"damage_splash":0,"attacks":4,"range":10.0,"cooldown":3.0,"bonuses":[{"against":"Light","damage":6.0}]},{"target_type":"Ground","damage_per_hit":30.0,"damage_splash":0,"attacks":2,"range":7.0,"cooldown":1.280029296875,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":2362},{"ability":1}]},{"id":53,"name":"Hellion","race":"Terran","supply":2.0,"cargo_size":2,"max_health":90.0,"armor":0.0,"sight":10.0,"speed":4.25,"speed_creep_mul":1.0,"attributes":["Light","Mechanical"],"size":0,"radius":0.625,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":0,"time":480.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":8.0,"damage_splash":0,"attacks":1,"range":5.0,"cooldown":2.5,"bonuses":[{"against":"Light","damage":6.0}]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1},{"requirements":[{"building":29}],"ability":1998}]},{"id":54,"name":"Medivac","race":"Terran","supply":2.0,"cargo_capacity":8,"max_health":150.0,"armor":1.0,"sight":11.0,"speed":2.5,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":50,"weapons":[],"attributes":["Armored","Mechanical"],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":100,"time":672.0,"tech_alias":[],"unit_alias":0,"is_flying":true,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":386},{"ability":394},{"ability":2116},{"ability":1}]},{"id":55,"name":"Banshee","race":"Terran","supply":3.0,"max_health":140.0,"armor":0.0,"sight":10.0,"speed":2.75,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":50,"attributes":["Light","Mechanical"],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":100,"time":960.0,"tech_alias":[],"unit_alias":0,"is_flying":true,"weapons":[{"target_type":"Ground","damage_per_hit":12.0,"damage_splash":0,"attacks":2,"range":6.0,"cooldown":1.25,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1},{"requirements":[{"upgrade":20}],"ability":392}]},{"id":56,"name":"Raven","race":"Terran","supply":2.0,"max_health":140.0,"armor":1.0,"sight":11.0,"detection_range":11.0,"speed":2.94921875,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":50,"weapons":[],"attributes":["Light","Mechanical"],"size":0,"radius":0.625,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":150,"time":768.0,"tech_alias":[],"unit_alias":0,"is_flying":true,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":1764},{"ability":3753},{"ability":1},{"requirements":[{"upgrade":299}],"ability":3747}]},{"id":57,"name":"Battlecruiser","race":"Terran","supply":6.0,"max_health":550.0,"armor":3.0,"sight":12.0,"speed":1.875,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Massive"],"size":0,"radius":1.25,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":400,"gas":300,"time":1440.0,"tech_alias":[],"unit_alias":0,"is_flying":true,"abilities":[{"ability":2358},{"ability":3771},{"ability":3776},{"ability":3777},{"ability":3778},{"ability":3783},{"ability":1},{"requirements":[{"upgrade":76}],"ability":401}]},{"id":58,"name":"Nuke","race":"Terran","supply":0.0,"max_health":100.0,"armor":0.0,"sight":0.0,"speed_creep_mul":1.0,"weapons":[],"attributes":[],"abilities":[],"size":0,"radius":0.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":100,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":true},{"id":59,"name":"Nexus","race":"Protoss","supply":-15.0,"max_health":1000.0,"armor":1.0,"sight":11.0,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":50,"weapons":[],"attributes":["Armored","Structure"],"size":0,"radius":2.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":true,"minerals":400,"gas":0,"time":1600.0,"tech_alias":[],"unit_alias":0,"max_shield":1000.0,"is_flying":false,"abilities":[{"ability":207},{"ability":1006},{"ability":3755},{"ability":3757},{"ability":4126},{"ability":1},{"requirements":[{"building":64}],"ability":110}]},{"id":60,"name":"Pylon","race":"Protoss","supply":-8.0,"max_health":200.0,"armor":1.0,"sight":10.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Structure"],"abilities":[],"size":0,"radius":1.125,"power_radius":6.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":0,"time":400.0,"tech_alias":[],"unit_alias":0,"max_shield":200.0,"is_flying":false},{"id":61,"name":"Assimilator","race":"Protoss","supply":0.0,"max_health":300.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Structure"],"abilities":[],"size":0,"radius":1.6875,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":true,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":75,"gas":0,"time":480.0,"tech_alias":[],"unit_alias":0,"max_shield":300.0,"is_flying":false},{"id":62,"name":"Gateway","race":"Protoss","supply":0.0,"max_health":500.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Structure"],"size":0,"radius":1.8125,"power_radius":6.5,"accepts_addon":false,"needs_power":true,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":0,"time":1040.0,"tech_alias":[],"unit_alias":0,"max_shield":500.0,"is_flying":false,"abilities":[{"ability":195},{"ability":916},{"ability":1},{"requirements":[{"building":72}],"ability":917},{"requirements":[{"building":68}],"ability":919},{"requirements":[{"building":69}],"ability":920},{"requirements":[{"building":72}],"ability":921},{"requirements":[{"building":72}],"ability":922},{"requirements":[{"upgrade":84}],"ability":1518}]},{"id":63,"name":"Forge","race":"Protoss","supply":0.0,"max_health":400.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Structure"],"size":0,"radius":1.8125,"power_radius":6.5,"accepts_addon":false,"needs_power":true,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":0,"time":720.0,"tech_alias":[],"unit_alias":0,"max_shield":400.0,"is_flying":false,"abilities":[{"ability":1062},{"ability":1065},{"ability":1068},{"ability":1063,"requirements":[{"upgrade":39},{"building":65}]},{"ability":1064,"requirements":[{"upgrade":40},{"building":65}]},{"ability":1066,"requirements":[{"upgrade":42},{"building":65}]},{"ability":1067,"requirements":[{"upgrade":43},{"building":65}]},{"ability":1069,"requirements":[{"upgrade":45},{"building":65}]},{"ability":1070,"requirements":[{"upgrade":46},{"building":65}]}]},{"id":64,"name":"FleetBeacon","race":"Protoss","supply":0.0,"max_health":500.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Structure"],"size":0,"radius":1.8125,"power_radius":6.5,"accepts_addon":false,"needs_power":true,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":300,"gas":200,"time":960.0,"tech_alias":[],"unit_alias":0,"max_shield":500.0,"is_flying":false,"abilities":[{"ability":46},{"ability":48},{"ability":49}]},{"id":65,"name":"TwilightCouncil","race":"Protoss","supply":0.0,"max_health":500.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Structure"],"size":0,"radius":1.8125,"power_radius":6.5,"accepts_addon":false,"needs_power":true,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":100,"time":800.0,"tech_alias":[],"unit_alias":0,"max_shield":500.0,"is_flying":false,"abilities":[{"ability":1592},{"ability":1593},{"ability":1594}]},{"id":66,"name":"PhotonCannon","race":"Protoss","supply":0.0,"max_health":150.0,"armor":1.0,"sight":11.0,"detection_range":11.0,"speed_creep_mul":1.0,"attributes":["Armored","Structure"],"size":0,"radius":1.125,"power_radius":6.5,"accepts_addon":false,"needs_power":true,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":0,"time":640.0,"tech_alias":[],"unit_alias":0,"max_shield":150.0,"is_flying":false,"weapons":[{"target_type":"Any","damage_per_hit":20.0,"damage_splash":0,"attacks":1,"range":7.0,"cooldown":1.25,"bonuses":[]}],"abilities":[{"ability":4},{"ability":23},{"ability":1}]},{"id":67,"name":"Stargate","race":"Protoss","supply":0.0,"max_health":600.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Structure"],"size":0,"radius":1.8125,"power_radius":6.5,"accepts_addon":false,"needs_power":true,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":150,"time":960.0,"tech_alias":[],"unit_alias":0,"max_shield":600.0,"is_flying":false,"abilities":[{"ability":195},{"ability":946},{"ability":950},{"ability":954},{"ability":1},{"requirements":[{"building":64}],"ability":948},{"requirements":[{"building":64}],"ability":955}]},{"id":68,"name":"TemplarArchive","race":"Protoss","supply":0.0,"max_health":500.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Structure"],"size":0,"radius":1.8125,"power_radius":6.5,"accepts_addon":false,"needs_power":true,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":200,"time":800.0,"tech_alias":[],"unit_alias":0,"max_shield":500.0,"is_flying":false,"abilities":[{"ability":1126}]},{"id":69,"name":"DarkShrine","race":"Protoss","supply":0.0,"max_health":500.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Structure"],"size":0,"radius":1.5,"power_radius":6.5,"accepts_addon":false,"needs_power":true,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":150,"time":1600.0,"tech_alias":[],"unit_alias":0,"max_shield":500.0,"is_flying":false,"abilities":[{"ability":2720}]},{"id":70,"name":"RoboticsBay","race":"Protoss","supply":0.0,"max_health":500.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Structure"],"size":0,"radius":1.8125,"power_radius":6.5,"accepts_addon":false,"needs_power":true,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":150,"time":1040.0,"tech_alias":[],"unit_alias":0,"max_shield":500.0,"is_flying":false,"abilities":[{"ability":1093},{"ability":1094},{"ability":1097}]},{"id":71,"name":"RoboticsFacility","race":"Protoss","supply":0.0,"max_health":450.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Structure"],"size":0,"radius":1.8125,"power_radius":6.5,"accepts_addon":false,"needs_power":true,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":100,"time":1040.0,"tech_alias":[],"unit_alias":0,"max_shield":450.0,"is_flying":false,"abilities":[{"ability":195},{"ability":976},{"ability":977},{"ability":979},{"ability":1},{"requirements":[{"building":70}],"ability":978},{"requirements":[{"building":70}],"ability":994}]},{"id":72,"name":"CyberneticsCore","race":"Protoss","supply":0.0,"max_health":550.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Structure"],"size":0,"radius":1.8125,"power_radius":6.5,"accepts_addon":false,"needs_power":true,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":0,"time":800.0,"tech_alias":[],"unit_alias":0,"max_shield":550.0,"is_flying":false,"abilities":[{"ability":1562},{"ability":1565},{"ability":1568},{"ability":1563,"requirements":[{"upgrade":78},{"building":64}]},{"ability":1564,"requirements":[{"upgrade":79},{"building":64}]},{"ability":1566,"requirements":[{"upgrade":81},{"building":64}]},{"ability":1567,"requirements":[{"upgrade":82},{"building":64}]}]},{"id":73,"name":"Zealot","race":"Protoss","supply":2.0,"cargo_size":2,"max_health":100.0,"armor":1.0,"sight":9.0,"speed":2.25,"speed_creep_mul":1.0,"attributes":["Light","Biological"],"size":0,"radius":0.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":0,"time":608.0,"tech_alias":[],"unit_alias":0,"max_shield":50.0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":8.0,"damage_splash":0,"attacks":2,"range":0.10009765625,"cooldown":1.199951171875,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1},{"requirements":[{"upgrade":86}],"ability":1819}]},{"id":74,"name":"Stalker","race":"Protoss","supply":2.0,"cargo_size":2,"max_health":80.0,"armor":1.0,"sight":10.0,"speed":2.953125,"speed_creep_mul":1.0,"attributes":["Armored","Mechanical"],"size":0,"radius":0.625,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":125,"gas":50,"time":608.0,"tech_alias":[],"unit_alias":0,"max_shield":80.0,"is_flying":false,"weapons":[{"target_type":"Any","damage_per_hit":13.0,"damage_splash":0,"attacks":1,"range":6.0,"cooldown":1.8701171875,"bonuses":[{"against":"Armored","damage":5.0}]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1},{"requirements":[{"upgrade":87}],"ability":1442}]},{"id":75,"name":"HighTemplar","race":"Protoss","supply":2.0,"cargo_size":2,"max_health":40.0,"armor":0.0,"sight":10.0,"speed":2.015625,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":50,"attributes":["Light","Biological","Psionic"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":50,"gas":150,"time":880.0,"tech_alias":[],"unit_alias":0,"max_shield":40.0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":4.0,"damage_splash":0,"attacks":1,"range":6.0,"cooldown":1.75390625,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":23},{"ability":140},{"ability":1},{"requirements":[{"upgrade":52}],"ability":1036},{"ability":1766}]},{"id":76,"name":"DarkTemplar","race":"Protoss","supply":2.0,"cargo_size":2,"max_health":40.0,"armor":1.0,"sight":8.0,"speed":2.8125,"speed_creep_mul":1.0,"attributes":["Light","Biological","Psionic"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":125,"gas":125,"time":880.0,"tech_alias":[],"unit_alias":0,"max_shield":80.0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":45.0,"damage_splash":0,"attacks":1,"range":0.10009765625,"cooldown":1.694091796875,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1},{"requirements":[{"upgrade":141}],"ability":2700},{"ability":1766}]},{"id":77,"name":"Sentry","race":"Protoss","supply":2.0,"cargo_size":2,"max_health":40.0,"armor":1.0,"sight":10.0,"speed":2.5,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":50,"weapons":[],"attributes":["Mechanical","Psionic"],"size":0,"radius":0.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":50,"gas":100,"time":512.0,"tech_alias":[],"unit_alias":0,"max_shield":40.0,"is_flying":false,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":76},{"ability":146},{"ability":148},{"ability":150},{"ability":152},{"ability":154},{"ability":156},{"ability":158},{"ability":160},{"ability":162},{"ability":164},{"ability":1526},{"ability":2114},{"ability":2389},{"ability":2391},{"ability":1}]},{"id":78,"name":"Phoenix","race":"Protoss","supply":2.0,"max_health":120.0,"armor":0.0,"sight":10.0,"speed":4.25,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":50,"attributes":["Light","Mechanical"],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":100,"time":560.0,"tech_alias":[],"unit_alias":0,"max_shield":60.0,"is_flying":true,"weapons":[{"target_type":"Air","damage_per_hit":5.0,"damage_splash":0,"attacks":2,"range":5.0,"cooldown":1.10009765625,"bonuses":[{"against":"Light","damage":5.0}]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":173},{"ability":1}]},{"id":79,"name":"Carrier","race":"Protoss","supply":6.0,"max_health":300.0,"armor":2.0,"sight":12.0,"speed":1.875,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Massive"],"size":0,"radius":1.25,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":350,"gas":250,"time":1440.0,"tech_alias":[],"unit_alias":0,"max_shield":150.0,"is_flying":true,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1038},{"ability":1042},{"ability":1}]},{"id":80,"name":"VoidRay","race":"Protoss","supply":4.0,"max_health":150.0,"armor":0.0,"sight":10.0,"speed":2.75,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical"],"size":0,"radius":1.0,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":250,"gas":150,"time":963.19921875,"tech_alias":[],"unit_alias":0,"max_shield":100.0,"is_flying":true,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":2393},{"ability":1}]},{"id":81,"name":"WarpPrism","race":"Protoss","supply":2.0,"cargo_capacity":8,"max_health":80.0,"armor":0.0,"sight":10.0,"speed":2.953125,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Psionic"],"size":0,"radius":0.875,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":250,"gas":0,"time":800.0,"tech_alias":[],"unit_alias":0,"max_shield":100.0,"is_flying":true,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":911},{"ability":1528},{"ability":1}]},{"id":82,"name":"Observer","race":"Protoss","supply":1.0,"max_health":40.0,"armor":0.0,"sight":11.0,"detection_range":11.0,"speed":2.015625,"speed_creep_mul":1.0,"weapons":[],"attributes":["Light","Mechanical"],"size":0,"radius":0.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":25,"gas":75,"time":400.0,"tech_alias":[],"unit_alias":0,"max_shield":30.0,"is_flying":true,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":3741},{"ability":1}]},{"id":83,"name":"Immortal","race":"Protoss","supply":4.0,"cargo_size":4,"max_health":200.0,"armor":1.0,"sight":9.0,"speed":2.25,"speed_creep_mul":1.0,"attributes":["Armored","Mechanical"],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":250,"gas":100,"time":880.0,"tech_alias":[],"unit_alias":0,"max_shield":100.0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":20.0,"damage_splash":0,"attacks":1,"range":6.0,"cooldown":1.60009765625,"bonuses":[{"against":"Armored","damage":30.0}]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1}]},{"id":84,"name":"Probe","race":"Protoss","supply":1.0,"cargo_size":1,"max_health":20.0,"armor":0.0,"sight":8.0,"speed":2.8125,"speed_creep_mul":1.0,"attributes":["Light","Mechanical"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":true,"is_townhall":false,"minerals":50,"gas":0,"time":272.0,"tech_alias":[],"unit_alias":0,"max_shield":20.0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":5.0,"damage_splash":0,"attacks":1,"range":0.199951171875,"cooldown":1.5,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":30},{"ability":298},{"ability":880},{"ability":881},{"ability":882},{"ability":1},{"requirements":[{"building":60}],"ability":883},{"requirements":[{"building":60}],"ability":884},{"requirements":[{"building":67}],"ability":885},{"requirements":[{"building":72}],"ability":886},{"requirements":[{"building":63}],"ability":887},{"requirements":[{"building":72}],"ability":889},{"requirements":[{"building":65}],"ability":890},{"requirements":[{"building":65}],"ability":891},{"requirements":[{"building":71}],"ability":892},{"requirements":[{"building":72}],"ability":893},{"requirements":[{"building":62}],"ability":894},{"requirements":[{"building":72}],"ability":895}]},{"id":85,"name":"Interceptor","race":"Protoss","supply":0.0,"max_health":40.0,"armor":0.0,"sight":7.0,"speed":7.5,"speed_creep_mul":1.0,"attributes":["Light","Mechanical"],"size":0,"radius":0.25,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":15,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"max_shield":40.0,"is_flying":true,"weapons":[{"target_type":"Any","damage_per_hit":5.0,"damage_splash":0,"attacks":2,"range":2.0,"cooldown":3.0,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1}]},{"id":86,"name":"Hatchery","race":"Zerg","supply":-6.0,"max_health":1500.0,"armor":1.0,"sight":12.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological","Structure"],"size":0,"radius":2.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":true,"minerals":325,"gas":0,"time":1600.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":211},{"ability":212},{"ability":1223},{"ability":1225},{"ability":1},{"requirements":[{"building":89}],"ability":1216},{"requirements":[{"building":89}],"ability":1632}]},{"id":87,"name":"CreepTumor","race":"Zerg","supply":0.0,"max_health":50.0,"armor":0.0,"sight":10.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Light","Biological","Structure"],"abilities":[],"size":0,"radius":1.0,"accepts_addon":false,"needs_power":false,"needs_creep":true,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":240.0,"tech_alias":[],"unit_alias":0,"is_flying":false},{"id":88,"name":"Extractor","race":"Zerg","supply":0.0,"max_health":500.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological","Structure"],"abilities":[],"size":0,"radius":1.6875,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":true,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":75,"gas":0,"time":480.0,"tech_alias":[],"unit_alias":0,"is_flying":false},{"id":89,"name":"SpawningPool","race":"Zerg","supply":0.0,"max_health":1000.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological","Structure"],"size":0,"radius":1.8125,"accepts_addon":false,"needs_power":false,"needs_creep":true,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":250,"gas":0,"time":1040.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":1253},{"ability":1252,"requirements":[{"building":101}]}]},{"id":90,"name":"EvolutionChamber","race":"Zerg","supply":0.0,"max_health":750.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological","Structure"],"size":0,"radius":1.8125,"accepts_addon":false,"needs_power":false,"needs_creep":true,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":125,"gas":0,"time":560.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":1186},{"ability":1189},{"ability":1192},{"ability":1187,"requirements":[{"upgrade":53},{"building":100}]},{"ability":1188,"requirements":[{"upgrade":54},{"building":101}]},{"ability":1190,"requirements":[{"upgrade":56},{"building":100}]},{"ability":1191,"requirements":[{"upgrade":57},{"building":101}]},{"ability":1193,"requirements":[{"upgrade":59},{"building":100}]},{"ability":1194,"requirements":[{"upgrade":60},{"building":101}]}]},{"id":91,"name":"HydraliskDen","race":"Zerg","supply":0.0,"max_health":850.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological","Structure"],"size":0,"radius":1.8125,"accepts_addon":false,"needs_power":false,"needs_creep":true,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":100,"time":640.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":1282},{"ability":1283},{"requirements":[{"building":101}],"ability":1284}]},{"id":92,"name":"Spire","race":"Zerg","supply":0.0,"max_health":850.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological","Structure"],"size":0,"radius":1.125,"accepts_addon":false,"needs_power":false,"needs_creep":true,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":250,"gas":200,"time":1600.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":1312},{"ability":1315},{"requirements":[{"building":101}],"ability":1220},{"ability":1313,"requirements":[{"upgrade":68},{"building":100}]},{"ability":1314,"requirements":[{"upgrade":69},{"building":101}]},{"ability":1316,"requirements":[{"upgrade":71},{"building":100}]},{"ability":1317,"requirements":[{"upgrade":72},{"building":101}]}]},{"id":93,"name":"UltraliskCavern","race":"Zerg","supply":0.0,"max_health":850.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological","Structure"],"size":0,"radius":1.8125,"accepts_addon":false,"needs_power":false,"needs_creep":true,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":200,"gas":200,"time":1040.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":263},{"ability":265}]},{"id":94,"name":"InfestationPit","race":"Zerg","supply":0.0,"max_health":850.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological","Structure"],"size":0,"radius":1.8125,"accepts_addon":false,"needs_power":false,"needs_creep":true,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":100,"time":800.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":1455}]},{"id":95,"name":"NydusNetwork","race":"Zerg","supply":0.0,"cargo_capacity":1020,"max_health":850.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological","Structure"],"size":0,"radius":1.8125,"accepts_addon":false,"needs_power":false,"needs_creep":true,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":200,"gas":150,"time":800.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":4},{"ability":195},{"ability":1437},{"ability":1768},{"ability":1}]},{"id":96,"name":"BanelingNest","race":"Zerg","supply":0.0,"max_health":850.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological","Structure"],"abilities":[{"ability":1482,"requirements":[{"building":100}]}],"size":0,"radius":1.8125,"accepts_addon":false,"needs_power":false,"needs_creep":true,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":50,"time":960.0,"tech_alias":[],"unit_alias":0,"is_flying":false},{"id":97,"name":"RoachWarren","race":"Zerg","supply":0.0,"max_health":850.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological","Structure"],"abilities":[{"ability":216,"requirements":[{"building":100}]},{"ability":217,"requirements":[{"building":100}]}],"size":0,"radius":1.8125,"accepts_addon":false,"needs_power":false,"needs_creep":true,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":200,"gas":0,"time":880.0,"tech_alias":[],"unit_alias":0,"is_flying":false},{"id":98,"name":"SpineCrawler","race":"Zerg","supply":0.0,"max_health":300.0,"armor":2.0,"sight":11.0,"speed_creep_mul":1.0,"attributes":["Armored","Biological","Structure"],"size":0,"radius":1.125,"accepts_addon":false,"needs_power":false,"needs_creep":true,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":0,"time":800.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":25.0,"damage_splash":0,"attacks":1,"range":7.0,"cooldown":1.85009765625,"bonuses":[{"against":"Armored","damage":5.0}]}],"abilities":[{"ability":4},{"ability":23},{"ability":1725},{"ability":1}]},{"id":99,"name":"SporeCrawler","race":"Zerg","supply":0.0,"max_health":300.0,"armor":1.0,"sight":11.0,"detection_range":11.0,"speed_creep_mul":1.0,"attributes":["Armored","Biological","Structure"],"size":0,"radius":0.875,"accepts_addon":false,"needs_power":false,"needs_creep":true,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":125,"gas":0,"time":480.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Air","damage_per_hit":20.0,"damage_splash":0,"attacks":1,"range":7.0,"cooldown":0.86083984375,"bonuses":[{"against":"Biological","damage":10.0}]}],"abilities":[{"ability":4},{"ability":23},{"ability":1727},{"ability":1}]},{"id":100,"name":"Lair","normal_mode":86,"race":"Zerg","supply":-6.0,"max_health":2000.0,"armor":1.0,"sight":12.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological","Structure"],"size":0,"radius":2.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":true,"minerals":475,"gas":100,"time":1280.0,"tech_alias":[86],"unit_alias":0,"is_flying":false,"abilities":[{"ability":211},{"ability":212},{"ability":1223},{"ability":1225},{"ability":1},{"requirements":[{"building":94}],"ability":1218},{"requirements":[{"building":89}],"ability":1632}]},{"id":101,"name":"Hive","normal_mode":86,"race":"Zerg","supply":-6.0,"max_health":2500.0,"armor":1.0,"sight":12.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological","Structure"],"size":0,"radius":2.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":true,"minerals":675,"gas":250,"time":1600.0,"tech_alias":[86,100],"unit_alias":0,"is_flying":false,"abilities":[{"ability":211},{"ability":212},{"ability":1223},{"ability":1225},{"ability":1},{"requirements":[{"building":89}],"ability":1632}]},{"id":102,"name":"GreaterSpire","normal_mode":92,"race":"Zerg","supply":0.0,"max_health":1000.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological","Structure"],"size":0,"radius":1.125,"accepts_addon":false,"needs_power":false,"needs_creep":true,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":350,"gas":350,"time":1600.0,"tech_alias":[92],"unit_alias":0,"is_flying":false,"abilities":[{"ability":1312},{"ability":1315},{"ability":1313,"requirements":[{"upgrade":68},{"building":100}]},{"ability":1314,"requirements":[{"upgrade":69},{"building":101}]},{"ability":1316,"requirements":[{"upgrade":71},{"building":100}]},{"ability":1317,"requirements":[{"upgrade":72},{"building":101}]}]},{"id":103,"name":"Egg","race":"Zerg","supply":0.0,"max_health":200.0,"armor":10.0,"sight":5.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Biological"],"size":0,"radius":0.125,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":195},{"ability":1}]},{"id":104,"name":"Drone","race":"Zerg","supply":1.0,"cargo_size":1,"max_health":40.0,"armor":0.0,"sight":8.0,"speed":2.8125,"speed_creep_mul":1.0,"attributes":["Light","Biological"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":true,"is_townhall":false,"minerals":50,"gas":0,"time":272.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":5.0,"damage_splash":0,"attacks":1,"range":0.199951171875,"cooldown":1.5,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":28},{"ability":1152},{"ability":1154},{"ability":1183},{"ability":1},{"requirements":[{"building":86}],"ability":1155},{"requirements":[{"building":86}],"ability":1156},{"requirements":[{"building":100}],"ability":1157},{"requirements":[{"building":100}],"ability":1158},{"requirements":[{"building":101}],"ability":1159},{"requirements":[{"building":100}],"ability":1160},{"requirements":[{"building":100}],"ability":1161},{"requirements":[{"building":89}],"ability":1162},{"requirements":[{"building":91}],"ability":1163},{"requirements":[{"building":89}],"ability":1165},{"requirements":[{"building":89}],"ability":1166},{"requirements":[{"building":89}],"ability":1167},{"requirements":[{"upgrade":64}],"ability":1378}]},{"id":105,"name":"Zergling","race":"Zerg","supply":0.5,"cargo_size":1,"max_health":35.0,"armor":0.0,"sight":8.0,"speed":2.953125,"speed_creep_mul":1.0,"attributes":["Light","Biological"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":25,"gas":0,"time":384.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":5.0,"damage_splash":0,"attacks":1,"range":0.10009765625,"cooldown":0.696044921875,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1},{"requirements":[{"upgrade":64}],"ability":1390},{"requirements":[{"building":96}],"ability":4121}]},{"id":106,"name":"Overlord","race":"Zerg","supply":-8.0,"max_health":200.0,"armor":0.0,"sight":11.0,"speed":0.64453125,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological"],"size":0,"radius":1.0,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":0,"time":400.0,"tech_alias":[],"unit_alias":0,"is_flying":true,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":1},{"requirements":[{"building":100}],"ability":1448},{"requirements":[{"building":100}],"ability":1692},{"requirements":[{"building":100}],"ability":2708}]},{"id":107,"name":"Hydralisk","race":"Zerg","supply":2.0,"cargo_size":2,"max_health":90.0,"armor":0.0,"sight":9.0,"speed":2.25,"speed_creep_mul":1.0,"attributes":["Light","Biological"],"size":0,"radius":0.625,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":50,"time":528.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Any","damage_per_hit":12.0,"damage_splash":0,"attacks":1,"range":5.0,"cooldown":0.824951171875,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1},{"requirements":[{"upgrade":64}],"ability":1382},{"requirements":[{"building":504}],"ability":2332},{"requirements":[{"upgrade":298}],"ability":4109}]},{"id":108,"name":"Mutalisk","race":"Zerg","supply":2.0,"max_health":120.0,"armor":0.0,"sight":11.0,"speed":4.0,"speed_creep_mul":1.0,"attributes":["Light","Biological"],"size":0,"radius":0.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":100,"time":528.0,"tech_alias":[],"unit_alias":0,"is_flying":true,"weapons":[{"target_type":"Any","damage_per_hit":9.0,"damage_splash":0,"attacks":1,"range":3.0,"cooldown":1.524658203125,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1}]},{"id":109,"name":"Ultralisk","race":"Zerg","supply":6.0,"cargo_size":8,"max_health":500.0,"armor":2.0,"sight":9.0,"speed":2.953125,"speed_creep_mul":1.0,"attributes":["Armored","Biological","Massive"],"size":0,"radius":1.0,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":275,"gas":200,"time":880.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":35.0,"damage_splash":0,"attacks":1,"range":1.0,"cooldown":0.860107421875,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1},{"requirements":[{"upgrade":64}],"ability":1512}]},{"id":110,"name":"Roach","race":"Zerg","supply":2.0,"cargo_size":2,"max_health":145.0,"armor":1.0,"sight":9.0,"speed":2.25,"speed_creep_mul":1.0,"attributes":["Armored","Biological"],"size":0,"radius":0.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":75,"gas":25,"time":432.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":16.0,"damage_splash":0,"attacks":1,"range":4.0,"cooldown":2.0,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1},{"requirements":[{"upgrade":64}],"ability":1386},{"requirements":[{"building":86}],"ability":2330}]},{"id":111,"name":"Infestor","race":"Zerg","supply":2.0,"cargo_size":2,"max_health":90.0,"armor":0.0,"sight":10.0,"speed":2.25,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":75,"weapons":[],"attributes":["Armored","Biological","Psionic"],"size":0,"radius":0.625,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":150,"time":800.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":74},{"ability":4111},{"ability":1},{"requirements":[{"upgrade":101}],"ability":249},{"requirements":[{"upgrade":64}],"ability":1394},{"requirements":[{"upgrade":64}],"ability":1444}]},{"id":112,"name":"Corruptor","race":"Zerg","supply":2.0,"max_health":200.0,"armor":2.0,"sight":10.0,"speed":3.375,"speed_creep_mul":1.0,"attributes":["Armored","Biological"],"size":0,"radius":0.625,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":100,"time":640.0,"tech_alias":[],"unit_alias":0,"is_flying":true,"weapons":[{"target_type":"Air","damage_per_hit":14.0,"damage_splash":0,"attacks":1,"range":6.0,"cooldown":1.89990234375,"bonuses":[{"against":"Massive","damage":6.0}]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":2324},{"ability":1},{"requirements":[{"building":102}],"ability":1372}]},{"id":113,"name":"BroodLordCocoon","race":"Zerg","supply":2.0,"max_health":200.0,"armor":2.0,"sight":5.0,"speed":1.40625,"speed_creep_mul":1.0,"weapons":[],"attributes":["Biological","Massive"],"abilities":[],"size":0,"radius":0.625,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":300,"gas":250,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":true},{"id":114,"name":"BroodLord","race":"Zerg","supply":4.0,"max_health":225.0,"armor":1.0,"sight":12.0,"speed":1.875,"speed_creep_mul":1.0,"attributes":["Armored","Biological","Massive"],"size":0,"radius":1.0,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":300,"gas":250,"time":541.34765625,"tech_alias":[],"unit_alias":0,"is_flying":true,"weapons":[{"target_type":"Ground","damage_per_hit":20.0,"damage_splash":0,"attacks":1,"range":10.0,"cooldown":2.5,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1}]},{"id":115,"name":"BanelingBurrowed","normal_mode":9,"race":"Zerg","supply":0.5,"max_health":30.0,"armor":0.0,"sight":8.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Biological"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":50,"gas":25,"time":18.962890625,"tech_alias":[],"unit_alias":9,"is_flying":false,"abilities":[{"ability":42},{"ability":1376}]},{"id":116,"name":"DroneBurrowed","normal_mode":104,"race":"Zerg","supply":1.0,"max_health":40.0,"armor":0.0,"sight":4.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Light","Biological"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":50,"gas":0,"time":23.328125,"tech_alias":[],"unit_alias":104,"is_flying":false,"abilities":[{"ability":1380}]},{"id":117,"name":"HydraliskBurrowed","normal_mode":107,"race":"Zerg","supply":2.0,"max_health":90.0,"armor":0.0,"sight":5.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Light","Biological"],"size":0,"radius":0.625,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":50,"time":24.291015625,"tech_alias":[],"unit_alias":107,"is_flying":false,"abilities":[{"ability":1384}]},{"id":118,"name":"RoachBurrowed","normal_mode":110,"race":"Zerg","supply":2.0,"max_health":145.0,"armor":1.0,"sight":5.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological"],"size":0,"radius":0.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":75,"gas":25,"time":9.69140625,"tech_alias":[],"unit_alias":110,"is_flying":false,"abilities":[{"ability":4},{"ability":1388},{"requirements":[{"upgrade":3}],"ability":16},{"requirements":[{"upgrade":3}],"ability":17},{"requirements":[{"upgrade":3}],"ability":18},{"requirements":[{"upgrade":3}],"ability":19},{"requirements":[{"upgrade":3}],"ability":1}]},{"id":119,"name":"ZerglingBurrowed","normal_mode":105,"race":"Zerg","supply":0.5,"max_health":35.0,"armor":0.0,"sight":4.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Light","Biological"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":25,"gas":0,"time":24.291015625,"tech_alias":[],"unit_alias":105,"is_flying":false,"abilities":[{"ability":1392}]},{"id":120,"name":"InfestorTerranBurrowed","normal_mode":7,"race":"Zerg","supply":0.0,"max_health":75.0,"armor":0.0,"sight":4.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Light","Biological"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":24.291015625,"tech_alias":[],"unit_alias":7,"is_flying":false,"abilities":[{"ability":1396}]},{"id":125,"name":"QueenBurrowed","normal_mode":126,"race":"Zerg","supply":2.0,"max_health":175.0,"armor":1.0,"sight":5.0,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":60,"weapons":[],"attributes":["Biological","Psionic"],"size":0,"radius":0.875,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":175,"gas":0,"time":15.33203125,"tech_alias":[126],"unit_alias":126,"is_flying":false,"abilities":[{"ability":1435}]},{"id":126,"name":"Queen","race":"Zerg","supply":2.0,"cargo_size":2,"max_health":175.0,"armor":1.0,"sight":9.0,"speed":0.9375,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":25,"attributes":["Biological","Psionic"],"size":0,"radius":0.875,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":175,"gas":0,"time":800.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Air","damage_per_hit":9.0,"damage_splash":0,"attacks":1,"range":7.0,"cooldown":1.0,"bonuses":[]},{"target_type":"Ground","damage_per_hit":4.0,"damage_splash":0,"attacks":2,"range":5.0,"cooldown":1.0,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":251},{"ability":1664},{"ability":1694},{"ability":1},{"requirements":[{"upgrade":64}],"ability":1433},{"ability":3691}]},{"id":127,"name":"InfestorBurrowed","normal_mode":111,"race":"Zerg","supply":2.0,"cargo_size":2,"max_health":90.0,"armor":0.0,"sight":8.0,"speed":2.0,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":75,"weapons":[],"attributes":["Armored","Biological","Psionic"],"size":0,"radius":0.625,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":150,"time":10.962890625,"tech_alias":[],"unit_alias":111,"is_flying":false,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":1396},{"ability":1446},{"ability":1},{"requirements":[{"upgrade":101}],"ability":249}]},{"id":128,"name":"OverlordCocoon","race":"Zerg","supply":-8.0,"max_health":200.0,"armor":2.0,"sight":5.0,"speed":1.875,"speed_creep_mul":1.0,"weapons":[],"attributes":["Biological"],"abilities":[],"size":0,"radius":0.625,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":100,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":true},{"id":129,"name":"Overseer","race":"Zerg","supply":-8.0,"max_health":200.0,"armor":1.0,"sight":11.0,"detection_range":11.0,"speed":1.875,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":50,"weapons":[],"attributes":["Armored","Biological"],"size":0,"radius":1.0,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":50,"time":266.6796875,"tech_alias":[106],"unit_alias":0,"is_flying":true,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":181},{"ability":1825},{"ability":3743},{"ability":1}]},{"id":130,"name":"PlanetaryFortress","normal_mode":18,"race":"Terran","supply":-15.0,"cargo_capacity":5,"max_health":1500.0,"armor":2.0,"sight":11.0,"speed_creep_mul":1.0,"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":2.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":true,"minerals":550,"gas":150,"time":800.0,"tech_alias":[18],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":40.0,"damage_splash":0,"attacks":1,"range":6.0,"cooldown":2.0,"bonuses":[]}],"abilities":[{"ability":4},{"ability":23},{"ability":203},{"ability":416},{"ability":524},{"ability":1}]},{"id":131,"name":"UltraliskBurrowed","normal_mode":109,"race":"Zerg","supply":6.0,"max_health":500.0,"armor":2.0,"sight":5.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological","Massive"],"size":0,"radius":1.0,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":275,"gas":200,"time":22.0,"tech_alias":[],"unit_alias":109,"is_flying":false,"abilities":[{"ability":1514}]},{"id":132,"name":"OrbitalCommand","normal_mode":18,"race":"Terran","supply":-15.0,"max_health":1500.0,"armor":1.0,"sight":11.0,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":50,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":2.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":true,"minerals":550,"gas":0,"time":560.0,"tech_alias":[18],"unit_alias":0,"is_flying":false,"abilities":[{"ability":171},{"ability":203},{"ability":255},{"ability":399},{"ability":524},{"ability":1522},{"ability":1}]},{"id":133,"name":"WarpGate","normal_mode":62,"race":"Protoss","supply":0.0,"max_health":500.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Structure"],"size":0,"radius":1.8125,"power_radius":6.5,"accepts_addon":false,"needs_power":true,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":0,"time":160.0,"tech_alias":[62],"unit_alias":0,"max_shield":500.0,"is_flying":false,"abilities":[{"ability":1413},{"ability":1520},{"ability":1},{"requirements":[{"building":72}],"ability":1414},{"requirements":[{"building":68}],"ability":1416},{"requirements":[{"building":69}],"ability":1417},{"requirements":[{"building":72}],"ability":1418},{"requirements":[{"building":72}],"ability":1419}]},{"id":134,"name":"OrbitalCommandFlying","normal_mode":132,"race":"Terran","supply":-15.0,"max_health":1500.0,"armor":1.0,"sight":11.0,"speed":0.9375,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":50,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":2.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":true,"minerals":550,"gas":0,"time":32.0,"tech_alias":[18],"unit_alias":132,"is_flying":true,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":1524},{"ability":1}]},{"id":136,"name":"WarpPrismPhasing","normal_mode":81,"race":"Protoss","supply":2.0,"cargo_capacity":8,"max_health":80.0,"armor":0.0,"sight":11.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Psionic"],"size":0,"radius":0.875,"power_radius":3.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":250,"gas":0,"time":24.0,"tech_alias":[81],"unit_alias":81,"max_shield":100.0,"is_flying":true,"abilities":[{"ability":4},{"ability":911},{"ability":1530},{"ability":1}]},{"id":137,"name":"CreepTumorBurrowed","normal_mode":87,"race":"Zerg","supply":0.0,"max_health":50.0,"armor":0.0,"sight":10.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Light","Biological","Structure"],"size":0,"radius":1.0,"accepts_addon":false,"needs_power":false,"needs_creep":true,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":18.9609375,"tech_alias":[87],"unit_alias":87,"is_flying":false,"abilities":[{"ability":1733},{"ability":1},{"ability":3691}]},{"id":138,"name":"CreepTumorQueen","normal_mode":87,"race":"Zerg","supply":0.0,"max_health":50.0,"armor":0.0,"sight":10.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Light","Biological","Structure"],"abilities":[],"size":0,"radius":1.0,"accepts_addon":false,"needs_power":false,"needs_creep":true,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":240.0,"tech_alias":[87],"unit_alias":87,"is_flying":false},{"id":139,"name":"SpineCrawlerUprooted","normal_mode":98,"race":"Zerg","supply":0.0,"max_health":300.0,"armor":2.0,"sight":11.0,"speed":1.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological","Structure"],"size":0,"radius":0.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":0,"time":16.0,"tech_alias":[],"unit_alias":98,"is_flying":false,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":1},{"ability":1729}]},{"id":140,"name":"SporeCrawlerUprooted","normal_mode":99,"race":"Zerg","supply":0.0,"max_health":300.0,"armor":1.0,"sight":11.0,"speed":1.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological","Structure"],"size":0,"radius":0.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":125,"gas":0,"time":16.0,"tech_alias":[],"unit_alias":99,"is_flying":false,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":1},{"ability":1731}]},{"id":141,"name":"Archon","race":"Protoss","supply":4.0,"cargo_size":4,"max_health":10.0,"armor":0.0,"sight":9.0,"speed":2.8125,"speed_creep_mul":1.0,"attributes":["Psionic","Massive"],"size":0,"radius":1.0,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":175,"gas":275,"time":0.0,"tech_alias":[],"unit_alias":0,"max_shield":350.0,"is_flying":false,"weapons":[{"target_type":"Any","damage_per_hit":25.0,"damage_splash":0,"attacks":1,"range":3.0,"cooldown":1.75390625,"bonuses":[{"against":"Biological","damage":10.0}]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1}]},{"id":142,"name":"NydusCanal","race":"Zerg","supply":0.0,"cargo_capacity":1020,"max_health":300.0,"armor":1.0,"sight":10.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological","Structure"],"size":0,"radius":1.125,"accepts_addon":false,"needs_power":false,"needs_creep":true,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":75,"gas":75,"time":320.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":4},{"ability":195},{"ability":2370},{"ability":1}]},{"id":145,"name":"GhostNova","normal_mode":50,"race":"Terran","supply":3.0,"cargo_size":2,"max_health":100.0,"armor":0.0,"sight":11.0,"speed":2.8125,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":75,"attributes":["Biological","Psionic"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":125,"time":0.0,"tech_alias":[],"unit_alias":50,"is_flying":false,"weapons":[{"target_type":"Any","damage_per_hit":10.0,"damage_splash":0,"attacks":1,"range":6.0,"cooldown":1.5,"bonuses":[{"against":"Light","damage":10.0}]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":36},{"ability":1628},{"ability":2714},{"ability":1},{"requirements":[],"ability":382}]},{"id":150,"name":"InfestedTerransEgg","race":"Zerg","supply":0.0,"max_health":75.0,"armor":2.0,"sight":0.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Biological"],"size":0,"radius":0.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[]},{"id":151,"name":"Larva","race":"Zerg","supply":0.0,"max_health":25.0,"armor":10.0,"sight":5.0,"speed":0.5625,"speed_creep_mul":1.0,"weapons":[],"attributes":["Light","Biological"],"size":0,"radius":0.125,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":1342},{"ability":1344},{"requirements":[{"building":89}],"ability":1343},{"requirements":[{"building":91}],"ability":1345},{"requirements":[{"building":92}],"ability":1346},{"requirements":[{"building":93}],"ability":1348},{"requirements":[{"building":97}],"ability":1351},{"requirements":[{"building":94}],"ability":1352},{"requirements":[{"building":92}],"ability":1353},{"requirements":[{"building":101}],"ability":1354},{"requirements":[{"building":94}],"ability":1356}]},{"id":268,"name":"MULE","race":"Terran","supply":0.0,"max_health":60.0,"armor":0.0,"sight":8.0,"speed":2.8125,"speed_creep_mul":1.0,"weapons":[],"attributes":["Light","Mechanical"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":50,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":78},{"ability":166},{"ability":1}]},{"id":289,"name":"Broodling","race":"Zerg","supply":0.0,"max_health":20.0,"armor":0.0,"sight":7.0,"speed":2.953125,"speed_creep_mul":1.0,"attributes":["Light","Biological"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":4.0,"damage_splash":0,"attacks":1,"range":0.10009765625,"cooldown":0.800048828125,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1}]},{"id":311,"name":"Adept","race":"Protoss","supply":2.0,"cargo_size":2,"max_health":70.0,"armor":1.0,"sight":9.0,"speed":2.5,"speed_creep_mul":1.0,"attributes":["Light","Biological"],"size":0,"radius":0.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":25,"time":672.0,"tech_alias":[],"unit_alias":0,"max_shield":70.0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":10.0,"damage_splash":0,"attacks":1,"range":4.0,"cooldown":2.25,"bonuses":[{"against":"Light","damage":12.0}]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":2544},{"ability":1}]},{"id":339,"name":"InfestedTerransEggPlacement","race":"Zerg","supply":0.0,"max_health":1.0,"armor":0.0,"sight":0.0,"speed_creep_mul":1.0,"weapons":[],"attributes":[],"abilities":[],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":false},{"id":484,"name":"HellionTank","race":"Terran","supply":2.0,"cargo_size":4,"max_health":135.0,"armor":0.0,"sight":10.0,"speed":2.25,"speed_creep_mul":1.0,"attributes":["Light","Biological","Mechanical"],"size":0,"radius":0.625,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":0,"time":480.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":18.0,"damage_splash":0,"attacks":1,"range":2.0,"cooldown":2.0,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1},{"requirements":[{"building":29}],"ability":1978}]},{"id":488,"name":"MothershipCore","race":"Protoss","supply":2.0,"max_health":130.0,"armor":1.0,"sight":9.0,"speed":1.875,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":50,"attributes":["Armored","Mechanical","Psionic"],"size":0,"radius":1.0,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":100,"time":480.0,"tech_alias":[],"unit_alias":0,"max_shield":60.0,"is_flying":true,"weapons":[{"target_type":"Ground","damage_per_hit":8.0,"damage_splash":0,"attacks":1,"range":5.0,"cooldown":0.85009765625,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1974},{"ability":2162},{"ability":2244},{"ability":1},{"requirements":[],"ability":1847}]},{"id":489,"name":"LocustMP","race":"Zerg","supply":0.0,"max_health":50.0,"armor":0.0,"sight":6.0,"speed":1.875,"speed_creep_mul":1.0,"attributes":["Light","Biological"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":10.0,"damage_splash":0,"attacks":1,"range":3.0,"cooldown":0.60009765625,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1}]},{"id":491,"name":"NydusCanalAttacker","race":"Zerg","supply":2.0,"max_health":200.0,"armor":1.0,"sight":10.0,"speed_creep_mul":1.0,"attributes":["Armored","Biological","Structure"],"size":0,"radius":1.125,"accepts_addon":false,"needs_power":false,"needs_creep":true,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":200,"gas":0,"time":320.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Any","damage_per_hit":10.0,"damage_splash":0,"attacks":1,"range":7.0,"cooldown":2.0,"bonuses":[]}],"abilities":[{"ability":4},{"ability":23},{"ability":1}]},{"id":492,"name":"NydusCanalCreeper","race":"Zerg","supply":0.0,"max_health":200.0,"armor":1.0,"sight":10.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological","Structure"],"size":0,"radius":1.125,"accepts_addon":false,"needs_power":false,"needs_creep":true,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":75,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":4},{"ability":23},{"ability":1839},{"ability":1}]},{"id":493,"name":"SwarmHostBurrowedMP","normal_mode":494,"race":"Zerg","supply":3.0,"max_health":160.0,"armor":1.0,"sight":10.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological"],"size":0,"radius":0.8125,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":75,"time":42.0,"tech_alias":[],"unit_alias":494,"is_flying":false,"abilities":[{"ability":2704},{"ability":1}]},{"id":494,"name":"SwarmHostMP","race":"Zerg","supply":3.0,"cargo_size":4,"max_health":160.0,"armor":1.0,"sight":10.0,"speed":2.25,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological"],"size":0,"radius":0.8125,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":75,"time":640.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":2704},{"ability":1},{"requirements":[{"upgrade":64}],"ability":2014}]},{"id":495,"name":"Oracle","race":"Protoss","supply":3.0,"max_health":100.0,"armor":0.0,"sight":10.0,"speed":4.0,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":50,"weapons":[],"attributes":["Armored","Mechanical","Psionic"],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":150,"time":832.0,"tech_alias":[],"unit_alias":0,"max_shield":60.0,"is_flying":true,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":23},{"ability":2146},{"ability":2375},{"ability":2505},{"ability":1}]},{"id":496,"name":"Tempest","race":"Protoss","supply":4.0,"max_health":200.0,"armor":2.0,"sight":12.0,"speed":2.25,"speed_creep_mul":1.0,"attributes":["Armored","Mechanical","Massive"],"size":0,"radius":1.125,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":250,"gas":175,"time":960.0,"tech_alias":[],"unit_alias":0,"max_shield":100.0,"is_flying":true,"weapons":[{"target_type":"Air","damage_per_hit":30.0,"damage_splash":0,"attacks":1,"range":13.0,"cooldown":3.300048828125,"bonuses":[{"against":"Massive","damage":22.0}]},{"target_type":"Ground","damage_per_hit":40.0,"damage_splash":0,"attacks":1,"range":10.0,"cooldown":3.300048828125,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1}]},{"id":497,"name":"WarHound","race":"Terran","supply":3.0,"cargo_size":4,"max_health":220.0,"armor":1.0,"sight":11.0,"speed":2.8125,"speed_creep_mul":1.0,"attributes":["Armored","Mechanical"],"size":0,"radius":0.8125,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":75,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":23.0,"damage_splash":0,"attacks":1,"range":7.0,"cooldown":1.300048828125,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":2101},{"ability":1}]},{"id":498,"name":"WidowMine","race":"Terran","supply":2.0,"cargo_size":2,"max_health":90.0,"armor":0.0,"sight":7.0,"speed":2.8125,"speed_creep_mul":1.0,"weapons":[],"attributes":["Light","Mechanical"],"size":0,"radius":0.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":75,"gas":25,"time":480.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":2095},{"ability":1}]},{"id":499,"name":"Viper","race":"Zerg","supply":3.0,"max_health":150.0,"armor":1.0,"sight":11.0,"speed":2.953125,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":50,"weapons":[],"attributes":["Armored","Biological","Psionic"],"size":0,"radius":1.0,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":200,"time":640.0,"tech_alias":[],"unit_alias":0,"is_flying":true,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":2063},{"ability":2067},{"ability":2073},{"ability":2542},{"ability":1}]},{"id":500,"name":"WidowMineBurrowed","normal_mode":498,"race":"Terran","supply":2.0,"max_health":90.0,"armor":0.0,"sight":7.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Light","Mechanical"],"size":0,"radius":0.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":75,"gas":25,"time":52.0,"tech_alias":[498],"unit_alias":498,"is_flying":false,"abilities":[{"ability":2097},{"ability":2099},{"ability":1}]},{"id":501,"name":"LurkerMPEgg","race":"Zerg","supply":2.0,"max_health":100.0,"armor":1.0,"sight":5.0,"speed":3.375,"speed_creep_mul":1.0,"weapons":[],"attributes":["Biological"],"size":0,"radius":0.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":195},{"ability":1}]},{"id":502,"name":"LurkerMP","race":"Zerg","supply":3.0,"cargo_size":4,"max_health":190.0,"armor":1.0,"sight":11.0,"speed":2.953125,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological"],"size":0,"radius":0.9375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":150,"time":553.328125,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":23},{"ability":2108},{"ability":1}]},{"id":503,"name":"LurkerMPBurrowed","normal_mode":502,"race":"Zerg","supply":3.0,"max_health":190.0,"armor":1.0,"sight":11.0,"speed_creep_mul":1.0,"attributes":["Armored","Biological"],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":150,"time":42.0,"tech_alias":[],"unit_alias":502,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":20.0,"damage_splash":0,"attacks":1,"range":8.0,"cooldown":2.0,"bonuses":[{"against":"Armored","damage":10.0}]}],"abilities":[{"ability":4},{"ability":23},{"ability":2110},{"ability":2550},{"ability":1}]},{"id":504,"name":"LurkerDenMP","race":"Zerg","supply":0.0,"max_health":850.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological","Structure"],"abilities":[{"ability":3709,"requirements":[{"building":101}]},{"ability":3710,"requirements":[{"building":101}]}],"size":0,"radius":1.8125,"accepts_addon":false,"needs_power":false,"needs_creep":true,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":150,"time":1280.0,"tech_alias":[],"unit_alias":0,"is_flying":false},{"id":569,"name":"ResourceBlocker","race":"Protoss","supply":0.0,"max_health":130.0,"armor":0.0,"sight":2.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Structure"],"abilities":[],"size":0,"radius":1.0,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":false},{"id":593,"name":"IceProtossCrates","race":"Protoss","supply":0.0,"max_health":10.0,"armor":0.0,"sight":0.0,"speed_creep_mul":1.0,"weapons":[],"attributes":[],"abilities":[],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":false},{"id":594,"name":"ProtossCrates","race":"Protoss","supply":0.0,"max_health":10.0,"armor":0.0,"sight":0.0,"speed_creep_mul":1.0,"weapons":[],"attributes":[],"abilities":[],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":false},{"id":595,"name":"TowerMine","race":"Terran","supply":4.0,"max_health":100.0,"armor":0.0,"sight":0.0,"speed_creep_mul":1.0,"weapons":[],"attributes":[],"abilities":[],"size":0,"radius":0.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":50,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":true},{"id":687,"name":"RavagerCocoon","race":"Zerg","supply":2.0,"max_health":100.0,"armor":5.0,"sight":5.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Biological"],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":195},{"ability":1}]},{"id":688,"name":"Ravager","race":"Zerg","supply":3.0,"cargo_size":4,"max_health":120.0,"armor":1.0,"sight":9.0,"speed":2.75,"speed_creep_mul":1.0,"attributes":["Biological"],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":100,"time":272.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":16.0,"damage_splash":0,"attacks":1,"range":6.0,"cooldown":1.60009765625,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":2338},{"ability":1},{"requirements":[{"upgrade":64}],"ability":2340}]},{"id":689,"name":"Liberator","race":"Terran","supply":3.0,"max_health":180.0,"armor":0.0,"sight":10.0,"speed":3.375,"speed_creep_mul":1.0,"attributes":["Armored","Mechanical"],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":125,"time":960.0,"tech_alias":[],"unit_alias":0,"is_flying":true,"weapons":[{"target_type":"Air","damage_per_hit":5.0,"damage_splash":0,"attacks":2,"range":5.0,"cooldown":1.800048828125,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":2558},{"ability":1}]},{"id":690,"name":"RavagerBurrowed","normal_mode":688,"race":"Zerg","supply":3.0,"max_health":120.0,"armor":1.0,"sight":5.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Biological"],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":100,"time":9.69140625,"tech_alias":[],"unit_alias":688,"is_flying":false,"abilities":[{"ability":2342}]},{"id":691,"name":"ThorAP","normal_mode":52,"race":"Terran","supply":6.0,"cargo_size":8,"max_health":400.0,"armor":1.0,"sight":11.0,"speed":1.875,"speed_creep_mul":1.0,"attributes":["Armored","Mechanical","Massive"],"size":0,"radius":1.0,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":300,"gas":200,"time":42.0,"tech_alias":[52],"unit_alias":52,"is_flying":false,"weapons":[{"target_type":"Air","damage_per_hit":25.0,"damage_splash":0,"attacks":1,"range":11.0,"cooldown":1.280029296875,"bonuses":[{"against":"Massive","damage":10.0}]},{"target_type":"Ground","damage_per_hit":30.0,"damage_splash":0,"attacks":2,"range":7.0,"cooldown":1.280029296875,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":2364},{"ability":1}]},{"id":692,"name":"Cyclone","race":"Terran","supply":3.0,"cargo_size":4,"max_health":120.0,"armor":1.0,"sight":11.0,"speed":3.375,"speed_creep_mul":1.0,"attributes":["Armored","Mechanical"],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":100,"time":720.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Any","damage_per_hit":18.0,"damage_splash":0,"attacks":1,"range":5.0,"cooldown":1.0,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":2350},{"ability":1}]},{"id":693,"name":"LocustMPFlying","normal_mode":489,"race":"Zerg","supply":0.0,"max_health":50.0,"armor":0.0,"sight":6.0,"speed":1.875,"speed_creep_mul":1.0,"weapons":[],"attributes":["Light","Biological"],"size":0,"radius":0.375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":8.0,"tech_alias":[],"unit_alias":489,"is_flying":true,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":2387},{"ability":1}]},{"id":694,"name":"Disruptor","race":"Protoss","supply":4.0,"cargo_size":4,"max_health":100.0,"armor":1.0,"sight":9.0,"speed":2.25,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical"],"size":0,"radius":0.625,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":150,"time":800.0,"tech_alias":[],"unit_alias":0,"max_shield":100.0,"is_flying":false,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":2346},{"ability":1}]},{"id":725,"name":"VoidMPImmortalReviveCorpse","race":"Protoss","supply":4.0,"cargo_size":4,"max_health":200.0,"armor":1.0,"sight":0.0,"speed":2.25,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical"],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":250,"gas":100,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":195},{"ability":2469},{"ability":1}]},{"id":726,"name":"GuardianCocoonMP","race":"Zerg","supply":2.0,"max_health":200.0,"armor":2.0,"sight":5.0,"speed":1.40625,"speed_creep_mul":1.0,"weapons":[],"attributes":["Biological","Massive"],"abilities":[],"size":0,"radius":0.625,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":200,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":true},{"id":727,"name":"GuardianMP","race":"Zerg","supply":2.0,"max_health":150.0,"armor":2.0,"sight":10.0,"speed":1.5,"speed_creep_mul":1.0,"attributes":["Armored","Biological","Massive"],"size":0,"radius":1.0,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":200,"time":640.015625,"tech_alias":[],"unit_alias":0,"is_flying":true,"weapons":[{"target_type":"Ground","damage_per_hit":20.0,"damage_splash":0,"attacks":1,"range":9.0,"cooldown":1.300048828125,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1}]},{"id":728,"name":"DevourerCocoonMP","race":"Zerg","supply":2.0,"max_health":200.0,"armor":2.0,"sight":5.0,"speed":1.40625,"speed_creep_mul":1.0,"weapons":[],"attributes":["Biological","Massive"],"abilities":[],"size":0,"radius":0.625,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":200,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":true},{"id":729,"name":"DevourerMP","race":"Zerg","supply":2.0,"max_health":250.0,"armor":2.0,"sight":9.0,"speed":2.25,"speed_creep_mul":1.0,"attributes":["Armored","Biological","Massive"],"size":0,"radius":0.875,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":250,"gas":150,"time":640.015625,"tech_alias":[],"unit_alias":0,"is_flying":true,"weapons":[{"target_type":"Air","damage_per_hit":25.0,"damage_splash":0,"attacks":1,"range":5.0,"cooldown":3.0,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1}]},{"id":730,"name":"DefilerMPBurrowed","race":"Zerg","supply":2.0,"cargo_size":2,"max_health":80.0,"armor":1.0,"sight":5.0,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":50,"weapons":[],"attributes":["Biological","Psionic"],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":50,"gas":150,"time":24.291015625,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":2491}]},{"id":731,"name":"DefilerMP","race":"Zerg","supply":2.0,"cargo_size":2,"max_health":80.0,"armor":1.0,"sight":10.0,"speed":2.25,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":50,"weapons":[],"attributes":["Biological","Psionic"],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":50,"gas":150,"time":8.80078125,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":2483},{"ability":2485},{"ability":2487},{"ability":1},{"requirements":[{"upgrade":64}],"ability":2489}]},{"id":732,"name":"OracleStasisTrap","race":"Protoss","supply":0.0,"max_health":30.0,"armor":0.0,"sight":7.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Light","Structure"],"abilities":[],"size":0,"radius":0.4375,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":80.0,"tech_alias":[],"unit_alias":0,"max_shield":30.0,"is_flying":false},{"id":733,"name":"DisruptorPhased","race":"Protoss","supply":3.0,"cargo_size":4,"max_health":100.0,"armor":1.0,"sight":9.0,"speed":4.25,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical"],"size":0,"radius":0.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"max_shield":100.0,"is_flying":false,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":1}]},{"id":734,"name":"LiberatorAG","normal_mode":689,"race":"Terran","supply":3.0,"max_health":180.0,"armor":0.0,"sight":10.0,"speed_creep_mul":1.0,"attributes":["Armored","Mechanical"],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":125,"time":64.66796875,"tech_alias":[689],"unit_alias":689,"is_flying":true,"weapons":[{"target_type":"Ground","damage_per_hit":75.0,"damage_splash":0,"attacks":1,"range":10.0,"cooldown":1.60009765625,"bonuses":[]}],"abilities":[{"ability":4},{"ability":23},{"ability":2560},{"ability":1}]},{"id":800,"name":"ReleaseInterceptorsBeacon","race":"Protoss","supply":0.0,"max_health":1.0,"armor":0.0,"sight":0.0,"speed_creep_mul":1.0,"weapons":[],"attributes":[],"abilities":[],"size":0,"radius":1.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":true},{"id":801,"name":"AdeptPhaseShift","normal_mode":311,"race":"Protoss","supply":2.0,"cargo_size":2,"max_health":90.0,"armor":1.0,"sight":4.0,"speed":4.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Light","Biological"],"size":0,"radius":0.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":311,"max_shield":50.0,"is_flying":false,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":23},{"ability":2596},{"ability":1}]},{"id":807,"name":"ThorAALance","race":"Terran","supply":0.0,"max_health":10.0,"armor":0.0,"sight":0.0,"speed_creep_mul":1.0,"weapons":[],"attributes":[],"abilities":[],"size":0,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":false},{"id":820,"name":"HERCPlacement","normal_mode":838,"race":"Terran","supply":3.0,"cargo_size":2,"max_health":80.0,"armor":1.0,"sight":9.0,"speed":2.25,"speed_creep_mul":1.0,"attributes":["Armored","Biological"],"size":0,"radius":0.6875,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":200,"gas":100,"time":0.0,"tech_alias":[],"unit_alias":838,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":20.0,"damage_splash":0,"attacks":1,"range":6.0,"cooldown":1.5,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1}]},{"id":838,"name":"HERC","race":"Terran","supply":3.0,"cargo_size":2,"max_health":80.0,"armor":1.0,"sight":9.0,"speed":2.25,"speed_creep_mul":1.0,"attributes":["Armored","Biological"],"size":0,"radius":0.6875,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":200,"gas":100,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"weapons":[{"target_type":"Ground","damage_per_hit":20.0,"damage_splash":0,"attacks":1,"range":6.0,"cooldown":1.5,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1}]},{"id":840,"name":"Replicant","race":"Protoss","supply":4.0,"cargo_size":4,"max_health":100.0,"armor":1.0,"sight":9.0,"speed":2.25,"speed_creep_mul":1.0,"weapons":[],"attributes":["Light","Biological"],"size":0,"radius":0.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":300,"time":0.0,"tech_alias":[],"unit_alias":0,"max_shield":150.0,"is_flying":false,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1}]},{"id":855,"name":"CorsairMP","race":"Protoss","supply":2.0,"max_health":120.0,"armor":1.0,"sight":9.0,"speed":2.8125,"speed_creep_mul":1.0,"attributes":["Light","Mechanical"],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":100,"time":0.0,"tech_alias":[],"unit_alias":0,"max_shield":60.0,"is_flying":true,"weapons":[{"target_type":"Air","damage_per_hit":5.0,"damage_splash":0,"attacks":1,"range":5.0,"cooldown":0.472412109375,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":2477},{"ability":1}]},{"id":856,"name":"ScoutMP","race":"Protoss","supply":3.0,"max_health":150.0,"armor":0.0,"sight":9.0,"speed":2.8125,"speed_creep_mul":1.0,"attributes":["Armored","Mechanical"],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":275,"gas":125,"time":0.0,"tech_alias":[],"unit_alias":0,"max_shield":100.0,"is_flying":true,"weapons":[{"target_type":"Ground","damage_per_hit":8.0,"damage_splash":0,"attacks":1,"range":4.0,"cooldown":1.694091796875,"bonuses":[]},{"target_type":"Air","damage_per_hit":7.0,"damage_splash":0,"attacks":2,"range":4.0,"cooldown":1.25,"bonuses":[{"against":"Armored","damage":7.0}]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1}]},{"id":857,"name":"ArbiterMP","race":"Protoss","supply":4.0,"max_health":200.0,"armor":0.0,"sight":9.0,"speed":2.25,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":50,"attributes":["Armored","Mechanical"],"size":0,"radius":1.0,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":350,"time":0.0,"tech_alias":[],"unit_alias":0,"max_shield":150.0,"is_flying":true,"weapons":[{"target_type":"Any","damage_per_hit":10.0,"damage_splash":0,"attacks":1,"range":5.0,"cooldown":1.5,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":2473},{"ability":2475},{"ability":1}]},{"id":858,"name":"ScourgeMP","race":"Zerg","supply":0.5,"max_health":25.0,"armor":0.0,"sight":5.0,"speed":3.5,"speed_creep_mul":1.0,"attributes":["Light","Biological"],"size":0,"radius":0.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":12,"gas":37,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":true,"weapons":[{"target_type":"Air","damage_per_hit":110.0,"damage_splash":0,"attacks":1,"range":0.0,"cooldown":0.833251953125,"bonuses":[]}],"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":1}]},{"id":860,"name":"QueenMP","race":"Zerg","supply":-2.0,"max_health":150.0,"armor":0.0,"sight":11.0,"speed":3.25,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":50,"weapons":[],"attributes":["Biological"],"size":0,"radius":0.75,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":true,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":23},{"ability":2493},{"ability":2495},{"ability":2497},{"ability":1}]},{"id":891,"name":"Elsecaro_Colonist_Hut","race":"Terran","supply":0.0,"max_health":200.0,"armor":1.0,"sight":10.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"size":0,"radius":2.125,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":false,"abilities":[{"ability":195},{"ability":1}]},{"id":892,"name":"TransportOverlordCocoon","race":"Zerg","supply":-8.0,"max_health":200.0,"armor":2.0,"sight":5.0,"speed":1.875,"speed_creep_mul":1.0,"weapons":[],"attributes":["Biological"],"abilities":[],"size":0,"radius":0.625,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":100,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":true},{"id":893,"name":"OverlordTransport","race":"Zerg","supply":-8.0,"cargo_capacity":8,"max_health":200.0,"armor":0.0,"sight":11.0,"speed":0.9140625,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological"],"size":0,"radius":1.0,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":0,"time":336.015625,"tech_alias":[106],"unit_alias":0,"is_flying":true,"abilities":[{"ability":4},{"ability":16},{"ability":17},{"ability":18},{"ability":19},{"ability":1406},{"ability":1},{"requirements":[{"building":100}],"ability":1448},{"requirements":[{"building":100}],"ability":1692}]},{"id":894,"name":"PylonOvercharged","normal_mode":60,"race":"Protoss","supply":-8.0,"max_health":200.0,"armor":1.0,"sight":10.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Structure"],"abilities":[],"size":0,"radius":1.125,"power_radius":6.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":0,"time":0.0,"tech_alias":[60,60],"unit_alias":60,"max_shield":200.0,"is_flying":false},{"id":895,"name":"BypassArmorDrone","race":"Terran","supply":0.0,"max_health":80.0,"armor":0.0,"sight":7.0,"speed":5.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Light","Mechanical","Structure"],"size":0,"radius":0.625,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":true,"abilities":[{"ability":4},{"ability":16},{"ability":23},{"ability":1}]},{"id":1910,"name":"ShieldBattery","race":"Protoss","supply":0.0,"max_health":200.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"max_energy":100.0,"start_energy":78,"weapons":[],"attributes":["Armored","Structure"],"size":0,"radius":1.125,"power_radius":6.5,"accepts_addon":false,"needs_power":true,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":0,"time":640.0,"tech_alias":[],"unit_alias":0,"max_shield":200.0,"is_flying":false,"abilities":[{"ability":4113},{"ability":1}]},{"id":1911,"name":"ObserverSiegeMode","normal_mode":82,"race":"Protoss","supply":1.0,"max_health":40.0,"armor":0.0,"sight":13.75,"detection_range":13.75,"speed_creep_mul":1.0,"weapons":[],"attributes":["Light","Mechanical"],"size":0,"radius":0.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":25,"gas":75,"time":12.0,"tech_alias":[],"unit_alias":82,"max_shield":30.0,"is_flying":true,"abilities":[{"ability":4},{"ability":3739}]},{"id":1912,"name":"OverseerSiegeMode","normal_mode":129,"race":"Zerg","supply":-8.0,"max_health":200.0,"armor":1.0,"sight":13.75,"detection_range":13.75,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":50,"weapons":[],"attributes":["Armored","Biological"],"size":0,"radius":1.0,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":150,"gas":50,"time":12.0,"tech_alias":[106],"unit_alias":129,"is_flying":true,"abilities":[{"ability":4},{"ability":181},{"ability":1825},{"ability":3745},{"ability":1}]},{"id":1913,"name":"RavenRepairDrone","race":"Terran","supply":0.0,"max_health":50.0,"armor":0.0,"sight":7.0,"speed_creep_mul":1.0,"max_energy":200.0,"start_energy":200,"weapons":[],"attributes":["Light","Mechanical","Structure","Summoned"],"size":0,"radius":0.625,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":100,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":true,"abilities":[{"ability":4},{"ability":3751},{"ability":1}]},{"id":1940,"name":"Viking","race":"Terran","supply":0.0,"max_health":1.0,"armor":0.0,"sight":0.0,"speed_creep_mul":1.0,"weapons":[],"attributes":[],"abilities":[],"size":0,"radius":0.5,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":false,"is_structure":false,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":0,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":false},{"id":1943,"name":"RefineryRich","race":"Terran","supply":0.0,"max_health":500.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Mechanical","Structure"],"abilities":[],"size":0,"radius":1.6875,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":true,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":75,"gas":0,"time":480.0,"tech_alias":[],"unit_alias":0,"is_flying":false},{"id":1994,"name":"AssimilatorRich","race":"Protoss","supply":0.0,"max_health":300.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Structure"],"abilities":[],"size":0,"radius":1.6875,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":true,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":75,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"max_shield":300.0,"is_flying":false},{"id":1995,"name":"ExtractorRich","race":"Zerg","supply":0.0,"max_health":500.0,"armor":1.0,"sight":9.0,"speed_creep_mul":1.0,"weapons":[],"attributes":["Armored","Biological","Structure"],"abilities":[],"size":0,"radius":1.6875,"accepts_addon":false,"needs_power":false,"needs_creep":false,"needs_geyser":true,"is_structure":true,"is_addon":false,"is_worker":false,"is_townhall":false,"minerals":75,"gas":0,"time":0.0,"tech_alias":[],"unit_alias":0,"is_flying":false}],"Upgrade":[{"id":1,"name":"CarrierLaunchSpeedUpgrade","cost":{"minerals":150,"gas":150,"time":1280.0}},{"id":2,"name":"GlialReconstitution","cost":{"minerals":100,"gas":100,"time":1760.0}},{"id":3,"name":"TunnelingClaws","cost":{"minerals":100,"gas":100,"time":1760.0}},{"id":4,"name":"ChitinousPlating","cost":{"minerals":150,"gas":150,"time":1760.0}},{"id":5,"name":"HiSecAutoTracking","cost":{"minerals":100,"gas":100,"time":1280.0}},{"id":6,"name":"TerranBuildingArmor","cost":{"minerals":150,"gas":150,"time":2240.0}},{"id":7,"name":"TerranInfantryWeaponsLevel1","cost":{"minerals":100,"gas":100,"time":2560.0}},{"id":8,"name":"TerranInfantryWeaponsLevel2","cost":{"minerals":150,"gas":150,"time":3040.0}},{"id":9,"name":"TerranInfantryWeaponsLevel3","cost":{"minerals":200,"gas":200,"time":3520.0}},{"id":10,"name":"NeosteelFrame","cost":{"minerals":100,"gas":100,"time":1760.0}},{"id":11,"name":"TerranInfantryArmorsLevel1","cost":{"minerals":100,"gas":100,"time":2560.0}},{"id":12,"name":"TerranInfantryArmorsLevel2","cost":{"minerals":150,"gas":150,"time":3040.0}},{"id":13,"name":"TerranInfantryArmorsLevel3","cost":{"minerals":200,"gas":200,"time":3520.0}},{"id":14,"name":"ReaperSpeed","cost":{"minerals":50,"gas":50,"time":1600.0}},{"id":15,"name":"Stimpack","cost":{"minerals":100,"gas":100,"time":2240.0}},{"id":16,"name":"ShieldWall","cost":{"minerals":100,"gas":100,"time":1760.0}},{"id":17,"name":"PunisherGrenades","cost":{"minerals":50,"gas":50,"time":960.0}},{"id":19,"name":"HighCapacityBarrels","cost":{"minerals":100,"gas":100,"time":1760.0}},{"id":20,"name":"BansheeCloak","cost":{"minerals":100,"gas":100,"time":1760.0}},{"id":21,"name":"MedivacCaduceusReactor","cost":{"minerals":100,"gas":100,"time":1120.0}},{"id":22,"name":"RavenCorvidReactor","cost":{"minerals":150,"gas":150,"time":1760.0}},{"id":23,"name":"HunterSeeker","cost":{"minerals":150,"gas":150,"time":1760.0}},{"id":24,"name":"DurableMaterials","cost":{"minerals":150,"gas":150,"time":1760.0}},{"id":25,"name":"PersonalCloaking","cost":{"minerals":150,"gas":150,"time":1920.0}},{"id":27,"name":"TerranVehicleArmorsLevel1","cost":{"minerals":100,"gas":100,"time":2560.0}},{"id":28,"name":"TerranVehicleArmorsLevel2","cost":{"minerals":175,"gas":175,"time":3040.0}},{"id":29,"name":"TerranVehicleArmorsLevel3","cost":{"minerals":250,"gas":250,"time":3520.0}},{"id":30,"name":"TerranVehicleWeaponsLevel1","cost":{"minerals":100,"gas":100,"time":2560.0}},{"id":31,"name":"TerranVehicleWeaponsLevel2","cost":{"minerals":175,"gas":175,"time":3040.0}},{"id":32,"name":"TerranVehicleWeaponsLevel3","cost":{"minerals":250,"gas":250,"time":3520.0}},{"id":33,"name":"TerranShipArmorsLevel1","cost":{"minerals":100,"gas":100,"time":2560.0}},{"id":34,"name":"TerranShipArmorsLevel2","cost":{"minerals":175,"gas":175,"time":3040.0}},{"id":35,"name":"TerranShipArmorsLevel3","cost":{"minerals":250,"gas":250,"time":3520.0}},{"id":36,"name":"TerranShipWeaponsLevel1","cost":{"minerals":100,"gas":100,"time":2560.0}},{"id":37,"name":"TerranShipWeaponsLevel2","cost":{"minerals":175,"gas":175,"time":3040.0}},{"id":38,"name":"TerranShipWeaponsLevel3","cost":{"minerals":250,"gas":250,"time":3520.0}},{"id":39,"name":"ProtossGroundWeaponsLevel1","cost":{"minerals":100,"gas":100,"time":2720.0}},{"id":40,"name":"ProtossGroundWeaponsLevel2","cost":{"minerals":150,"gas":150,"time":3240.0}},{"id":41,"name":"ProtossGroundWeaponsLevel3","cost":{"minerals":200,"gas":200,"time":3760.0}},{"id":42,"name":"ProtossGroundArmorsLevel1","cost":{"minerals":100,"gas":100,"time":2720.0}},{"id":43,"name":"ProtossGroundArmorsLevel2","cost":{"minerals":150,"gas":150,"time":3240.0}},{"id":44,"name":"ProtossGroundArmorsLevel3","cost":{"minerals":200,"gas":200,"time":3760.0}},{"id":45,"name":"ProtossShieldsLevel1","cost":{"minerals":150,"gas":150,"time":2720.0}},{"id":46,"name":"ProtossShieldsLevel2","cost":{"minerals":200,"gas":200,"time":3240.0}},{"id":47,"name":"ProtossShieldsLevel3","cost":{"minerals":250,"gas":250,"time":3760.0}},{"id":48,"name":"ObserverGraviticBooster","cost":{"minerals":100,"gas":100,"time":1280.0}},{"id":49,"name":"GraviticDrive","cost":{"minerals":100,"gas":100,"time":1280.0}},{"id":50,"name":"ExtendedThermalLance","cost":{"minerals":150,"gas":150,"time":2240.0}},{"id":52,"name":"PsiStormTech","cost":{"minerals":200,"gas":200,"time":1760.0}},{"id":53,"name":"ZergMeleeWeaponsLevel1","cost":{"minerals":100,"gas":100,"time":2560.0}},{"id":54,"name":"ZergMeleeWeaponsLevel2","cost":{"minerals":150,"gas":150,"time":3040.0}},{"id":55,"name":"ZergMeleeWeaponsLevel3","cost":{"minerals":200,"gas":200,"time":3520.0}},{"id":56,"name":"ZergGroundArmorsLevel1","cost":{"minerals":150,"gas":150,"time":2560.0}},{"id":57,"name":"ZergGroundArmorsLevel2","cost":{"minerals":200,"gas":200,"time":3040.0}},{"id":58,"name":"ZergGroundArmorsLevel3","cost":{"minerals":250,"gas":250,"time":3520.0}},{"id":59,"name":"ZergMissileWeaponsLevel1","cost":{"minerals":100,"gas":100,"time":2560.0}},{"id":60,"name":"ZergMissileWeaponsLevel2","cost":{"minerals":150,"gas":150,"time":3040.0}},{"id":61,"name":"ZergMissileWeaponsLevel3","cost":{"minerals":200,"gas":200,"time":3520.0}},{"id":62,"name":"overlordspeed","cost":{"minerals":100,"gas":100,"time":960.0}},{"id":63,"name":"overlordtransport","cost":{"minerals":200,"gas":200,"time":2080.0}},{"id":64,"name":"Burrow","cost":{"minerals":100,"gas":100,"time":1600.0}},{"id":65,"name":"zerglingattackspeed","cost":{"minerals":200,"gas":200,"time":2080.0}},{"id":66,"name":"zerglingmovementspeed","cost":{"minerals":100,"gas":100,"time":1760.0}},{"id":68,"name":"ZergFlyerWeaponsLevel1","cost":{"minerals":100,"gas":100,"time":2560.0}},{"id":69,"name":"ZergFlyerWeaponsLevel2","cost":{"minerals":175,"gas":175,"time":3040.0}},{"id":70,"name":"ZergFlyerWeaponsLevel3","cost":{"minerals":250,"gas":250,"time":3520.0}},{"id":71,"name":"ZergFlyerArmorsLevel1","cost":{"minerals":100,"gas":100,"time":2560.0}},{"id":72,"name":"ZergFlyerArmorsLevel2","cost":{"minerals":175,"gas":175,"time":3040.0}},{"id":73,"name":"ZergFlyerArmorsLevel3","cost":{"minerals":250,"gas":250,"time":3520.0}},{"id":75,"name":"CentrificalHooks","cost":{"minerals":100,"gas":100,"time":1600.0}},{"id":76,"name":"BattlecruiserEnableSpecializations","cost":{"minerals":150,"gas":150,"time":2240.0}},{"id":78,"name":"ProtossAirWeaponsLevel1","cost":{"minerals":100,"gas":100,"time":2880.0}},{"id":79,"name":"ProtossAirWeaponsLevel2","cost":{"minerals":175,"gas":175,"time":3440.0}},{"id":80,"name":"ProtossAirWeaponsLevel3","cost":{"minerals":250,"gas":250,"time":4000.0}},{"id":81,"name":"ProtossAirArmorsLevel1","cost":{"minerals":100,"gas":100,"time":2880.0}},{"id":82,"name":"ProtossAirArmorsLevel2","cost":{"minerals":175,"gas":175,"time":3440.0}},{"id":83,"name":"ProtossAirArmorsLevel3","cost":{"minerals":250,"gas":250,"time":4000.0}},{"id":84,"name":"WarpGateResearch","cost":{"minerals":50,"gas":50,"time":2240.0}},{"id":85,"name":"haltech","cost":{"minerals":100,"gas":100,"time":1280.0}},{"id":86,"name":"Charge","cost":{"minerals":100,"gas":100,"time":2240.0}},{"id":87,"name":"BlinkTech","cost":{"minerals":150,"gas":150,"time":2720.0}},{"id":88,"name":"AnabolicSynthesis","cost":{"minerals":150,"gas":150,"time":960.0}},{"id":98,"name":"TransformationServos","cost":{"minerals":150,"gas":150,"time":1760.0}},{"id":99,"name":"PhoenixRangeUpgrade","cost":{"minerals":150,"gas":150,"time":1440.0}},{"id":100,"name":"TempestRangeUpgrade","cost":{"minerals":200,"gas":200,"time":1760.0}},{"id":101,"name":"NeuralParasite","cost":{"minerals":150,"gas":150,"time":1760.0}},{"id":102,"name":"LocustLifetimeIncrease","cost":{"minerals":200,"gas":200,"time":1920.0}},{"id":113,"name":"TerranVehicleAndShipWeaponsLevel1","cost":{"minerals":100,"gas":100,"time":2560.0}},{"id":114,"name":"TerranVehicleAndShipWeaponsLevel2","cost":{"minerals":175,"gas":175,"time":3040.0}},{"id":115,"name":"TerranVehicleAndShipWeaponsLevel3","cost":{"minerals":250,"gas":250,"time":3520.0}},{"id":116,"name":"TerranVehicleAndShipArmorsLevel1","cost":{"minerals":100,"gas":100,"time":2560.0}},{"id":117,"name":"TerranVehicleAndShipArmorsLevel2","cost":{"minerals":175,"gas":175,"time":3040.0}},{"id":118,"name":"TerranVehicleAndShipArmorsLevel3","cost":{"minerals":250,"gas":250,"time":3520.0}},{"id":120,"name":"RoachSupply","cost":{"minerals":200,"gas":200,"time":2080.0}},{"id":121,"name":"ImmortalRevive","cost":{"minerals":100,"gas":100,"time":1280.0}},{"id":122,"name":"DrillClaws","cost":{"minerals":75,"gas":75,"time":1760.0}},{"id":123,"name":"CycloneLockOnRangeUpgrade","cost":{"minerals":150,"gas":150,"time":1760.0}},{"id":125,"name":"LiberatorMorph","cost":{"minerals":150,"gas":150,"time":1760.0}},{"id":127,"name":"LurkerRange","cost":{"minerals":150,"gas":150,"time":1280.0}},{"id":130,"name":"AdeptPiercingAttack","cost":{"minerals":100,"gas":100,"time":2240.0}},{"id":134,"name":"EvolveGroovedSpines","cost":{"minerals":75,"gas":75,"time":1120.0}},{"id":135,"name":"EvolveMuscularAugments","cost":{"minerals":100,"gas":100,"time":1440.0}},{"id":136,"name":"BansheeSpeed","cost":{"minerals":125,"gas":125,"time":2240.0}},{"id":137,"name":"MedivacRapidDeployment","cost":{"minerals":150,"gas":150,"time":1920.0}},{"id":138,"name":"RavenRecalibratedExplosives","cost":{"minerals":150,"gas":150,"time":1760.0}},{"id":139,"name":"MedivacIncreaseSpeedBoost","cost":{"minerals":100,"gas":100,"time":1280.0}},{"id":140,"name":"LiberatorAGRangeUpgrade","cost":{"minerals":150,"gas":150,"time":1760.0}},{"id":141,"name":"DarkTemplarBlinkUpgrade","cost":{"minerals":100,"gas":100,"time":2240.0}},{"id":144,"name":"CycloneLockOnDamageUpgrade","cost":{"minerals":100,"gas":100,"time":2240.0}},{"id":288,"name":"VoidRaySpeedUpgrade","cost":{"minerals":100,"gas":100,"time":1280.0}},{"id":289,"name":"SmartServos","cost":{"minerals":100,"gas":100,"time":1760.0}},{"id":290,"name":"ArmorPiercingRockets","cost":{"minerals":150,"gas":150,"time":1760.0}},{"id":291,"name":"CycloneRapidFireLaunchers","cost":{"minerals":75,"gas":75,"time":1760.0}},{"id":292,"name":"RavenEnhancedMunitions","cost":{"minerals":150,"gas":150,"time":1760.0}},{"id":293,"name":"DiggingClaws","cost":{"minerals":100,"gas":100,"time":1280.0}},{"id":296,"name":"HurricaneThrusters","cost":{"minerals":100,"gas":100,"time":2240.0}},{"id":297,"name":"TempestGroundAttackUpgrade","cost":{"minerals":150,"gas":150,"time":2240.0}},{"id":298,"name":"Frenzy","cost":{"minerals":100,"gas":100,"time":1440.0}},{"id":299,"name":"MicrobialShroud","cost":{"minerals":150,"gas":150,"time":1760.0}},{"id":300,"name":"InterferenceMatrix","cost":{"minerals":50,"gas":50,"time":1280.0}},{"id":301,"name":"SunderingImpact","cost":{"minerals":100,"gas":100,"time":2240.0}},{"id":302,"name":"AmplifiedShielding","cost":{"minerals":100,"gas":100,"time":2240.0}},{"id":303,"name":"PsionicAmplifiers","cost":{"minerals":100,"gas":100,"time":2240.0}},{"id":304,"name":"SecretedCoating","cost":{"minerals":100,"gas":100,"time":1280.0}}]} ================================================ FILE: dockerfiles/Dockerfile ================================================ # Set up StarCraft II Test Environment for python-sc2 bots (not pysc2 bots!) ARG PYTHON_VERSION=3.10 # https://docs.astral.sh/uv/guides/integration/docker/#available-images FROM ghcr.io/astral-sh/uv:python$PYTHON_VERSION-bookworm-slim AS base ARG SC2_VERSION=4.10 # Debugging purposes RUN echo $PYTHON_VERSION RUN echo $SC2_VERSION USER root # Update system RUN apt-get update \ && apt-get upgrade --assume-yes --quiet=2 # Update and install packages for SC2 development environment # gcc to compile packages # libc6-dev required by gcc: /usr/local/include/python3.12/Python.h:23:12: fatal error: stdlib.h: No such file or directory # git, unzip and wget for download and extraction # rename to rename maps # tree for debugging RUN apt-get install --assume-yes --no-install-recommends --no-show-upgraded \ gcc \ libc6-dev \ git \ unzip \ curl \ rename \ tree # Set working directory to root, this uncompresses StarCraftII below to folder /root/StarCraftII WORKDIR /root/ # Download and uncompress StarCraftII from https://github.com/Blizzard/s2client-proto#linux-packages and remove zip file # If file is locally available, use this instead: #COPY SC2.4.10.zip /root/ RUN curl --retry 5 --retry-delay 5 -C - http://blzdistsc2-a.akamaihd.net/Linux/SC2.$SC2_VERSION.zip -o "SC2.$SC2_VERSION.zip" \ && unzip -q -P iagreetotheeula "SC2.$SC2_VERSION.zip" \ && rm SC2.$SC2_VERSION.zip # Remove Battle.net folder RUN rm -rf /root/StarCraftII/Battle.net/* \ # Remove Shaders folder && rm -rf /root/StarCraftII/Versions/Shaders* # Create a symlink for the maps directory RUN ln -s /root/StarCraftII/Maps /root/StarCraftII/maps \ # Remove the Maps that come with the SC2 client && rm -rf /root/StarCraftII/maps/* # See download_maps.sh COPY dockerfiles/maps/* /root/StarCraftII/maps/ # Squash image with trick https://stackoverflow.com/a/56118557/10882657 FROM scratch COPY --from=base / / WORKDIR /root/ ENTRYPOINT [ "/bin/bash" ] # To run a python-sc2 bot: # Install python-sc2 and requirements via pip: # pip install --upgrade https://github.com/BurnySc2/python-sc2/archive/develop.zip # To run an example bot, copy one to your container and then run it with python: # python /your-bot.py ================================================ FILE: dockerfiles/README.md ================================================ # Dockerfile This dockerfile is meant to be as small as possible, contain only the sc2 binary and maps. It is used to run python-sc2 tests. # Source repo https://github.com/BurnySc2/python-sc2/tree/develop/dockerfiles Originally but deprecated: https://github.com/BurnySc2/python-sc2-docker This repository is related to the docker image on https://hub.docker.com/r/burnysc2/python-sc2-docker/ # Build locally See build shell script `test_docker_image.sh` ================================================ FILE: dockerfiles/download_maps.sh ================================================ #!/bin/bash # Run via # sh dockerfiles/download_maps.sh set -e # Create maps directory if it doesn't exist mkdir -p dockerfiles/maps cd dockerfiles/maps # Download function with retries download_with_retry() { local url=$1 local output=$2 curl --retry 5 --retry-delay 5 -C - -L "$url" -o "$output" } # Download and process sc2ai.net ladder maps download_with_retry "https://sc2ai.net/wiki/184/plugin/attachments/download/9/" "1.zip" download_with_retry "https://sc2ai.net/wiki/184/plugin/attachments/download/14/" "2.zip" download_with_retry "https://sc2ai.net/wiki/184/plugin/attachments/download/21/" "3.zip" download_with_retry "https://sc2ai.net/wiki/184/plugin/attachments/download/35/" "4.zip" download_with_retry "https://sc2ai.net/wiki/184/plugin/attachments/download/36/" "5.zip" download_with_retry "https://sc2ai.net/wiki/184/plugin/attachments/download/38/" "6.zip" download_with_retry "https://sc2ai.net/wiki/184/plugin/attachments/download/39/" "7.zip" unzip -q -o '*.zip' rm *.zip # Download and process official blizzard maps download_with_retry "http://blzdistsc2-a.akamaihd.net/MapPacks/Ladder2019Season3.zip" "Ladder2019Season3.zip" unzip -q -P iagreetotheeula -o "Ladder2019Season3.zip" mv Ladder2019Season3/* . rm "Ladder2019Season3.zip" rmdir Ladder2019Season3 # Download and process v5.0.6 maps download_with_retry "https://github.com/shostyn/sc2patch/raw/4987d4915b47c801adbc05e297abaa9ca2988838/Maps/506.zip" "506.zip" unzip -q -o "506.zip" rm "506.zip" # Download and process flat/empty maps download_with_retry "http://blzdistsc2-a.akamaihd.net/MapPacks/Melee.zip" "Melee.zip" unzip -q -P iagreetotheeula -o "Melee.zip" mv Melee/* . rm "Melee.zip" rmdir Melee # Remove LE suffix from file names for f in *LE.SC2Map; do mv -- "$f" "${f%LE.SC2Map}.SC2Map"; done ================================================ FILE: dockerfiles/test_docker_image.sh ================================================ # This script is meant for development, which produces fresh images and then runs tests # Run via # sh dockerfiles/test_docker_image.sh # Stop on error https://stackoverflow.com/a/2871034/10882657 set -e # Set which versions to use export VERSION_NUMBER=${VERSION_NUMBER:-0.9.9} export PYTHON_VERSION=${PYTHON_VERSION:-3.13} export SC2_VERSION=${SC2_VERSION:-4.10} # For better readability, set local variables IMAGE_NAME=burnysc2/python-sc2-docker:py_$PYTHON_VERSION-sc2_$SC2_VERSION-v$VERSION_NUMBER BUILD_ARGS="--build-arg PYTHON_VERSION=$PYTHON_VERSION --build-arg SC2_VERSION=$SC2_VERSION" # Build image without context # https://stackoverflow.com/a/54666214/10882657 docker build -f dockerfiles/Dockerfile -t $IMAGE_NAME $BUILD_ARGS . # Delete previous container if it exists docker rm -f test_container # Start container, override the default entrypoint docker run -i -d \ --name test_container \ --env 'PYTHONPATH=/root/python-sc2' \ --entrypoint /bin/bash \ $IMAGE_NAME # Install requirements docker exec -i test_container mkdir -p /root/python-sc2 docker cp pyproject.toml test_container:/root/python-sc2/ docker cp uv.lock test_container:/root/python-sc2/ docker exec -i test_container bash -c "pip install uv && cd python-sc2 && uv sync --no-cache --no-install-project" docker cp sc2 test_container:/root/python-sc2/sc2 docker cp test test_container:/root/python-sc2/test # Run various test bots docker exec -i test_container bash -c "cd python-sc2 && uv run python test/travis_test_script.py test/autotest_bot.py" docker exec -i test_container bash -c "cd python-sc2 && uv run python test/travis_test_script.py test/queries_test_bot.py" docker exec -i test_container bash -c "cd python-sc2 && uv run python test/travis_test_script.py test/damagetest_bot.py" docker cp examples test_container:/root/python-sc2/examples docker exec -i test_container bash -c "cd python-sc2 && uv run python test/run_example_bots_vs_computer.py" # Command for entering the container to debug if something went wrong: # docker exec -it test_container bash ================================================ FILE: dockerfiles/test_new_python_candidate.sh ================================================ # This script is meant for development, which produces fresh images and then runs tests # If it succeeds, a new python version can be added to CI # Run via # sh dockerfiles/test_new_python_candidate.sh # Stop on error https://stackoverflow.com/a/2871034/10882657 set -e # Set which versions to use export VERSION_NUMBER=${VERSION_NUMBER:-0.9.9} export PYTHON_VERSION=${PYTHON_VERSION:-3.14} export SC2_VERSION=${SC2_VERSION:-4.10} # For better readability, set local variables IMAGE_NAME=burnysc2/python-sc2-docker:py_$PYTHON_VERSION-sc2_$SC2_VERSION-v$VERSION_NUMBER BUILD_ARGS="--build-arg PYTHON_VERSION=$PYTHON_VERSION --build-arg SC2_VERSION=$SC2_VERSION" # Build image docker build -f dockerfiles/Dockerfile -t $IMAGE_NAME $BUILD_ARGS . # Delete previous container if it exists docker rm -f test_container # Start container # https://docs.docker.com/storage/bind-mounts/#use-a-read-only-bind-mount docker run -i -d \ --name test_container \ --env 'PYTHONPATH=/root/python-sc2' \ --entrypoint /bin/bash \ $IMAGE_NAME # Install requirements docker exec -i test_container mkdir -p /root/python-sc2 docker cp pyproject.toml test_container:/root/python-sc2/ docker cp uv.lock test_container:/root/python-sc2/ docker exec -i test_container bash -c "pip install uv && cd python-sc2 && uv sync --no-cache --no-install-project" docker cp sc2 test_container:/root/python-sc2/sc2 docker cp test test_container:/root/python-sc2/test # Run various test bots docker exec -i test_container bash -c "cd python-sc2 && uv run python test/travis_test_script.py test/autotest_bot.py" docker cp examples test_container:/root/python-sc2/examples docker exec -i test_container bash -c "cd python-sc2 && uv run python test/run_example_bots_vs_computer.py" ================================================ FILE: docs_generate/Makefile ================================================ # Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ================================================ FILE: docs_generate/bot_ai/index.rst ================================================ .. toctree:: :maxdepth: 2 **************************** bot_ai.py **************************** This basic bot class contains a few helper functions, basic properties and variables to get a simple bot started. How to use this information is shown in the bot examples (with comments). .. autoclass:: sc2.bot_ai.BotAI :members: .. autoclass:: sc2.bot_ai_internal.BotAIInternal :members: ================================================ FILE: docs_generate/client/index.rst ================================================ .. toctree:: :maxdepth: 2 **************************** client.py **************************** .. autoclass:: sc2.client.Client :members: ================================================ FILE: docs_generate/conf.py ================================================ # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys sys.path.insert(0, os.path.abspath("..")) # noqa: PTH100 # -- Project information ----------------------------------------------------- project = "python-sc2" copyright = "2019, tweakimp BurnySc2" author = "tweakimp, BurnySc2" # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx_autodoc_typehints", # https://www.sphinx-doc.org/en/master/usage/extensions/viewcode.html "sphinx.ext.viewcode", ] # autodoc_typehints options https://github.com/agronholm/sphinx-autodoc-typehints#options always_document_param_types = True typehints_use_signature = True typehints_use_signature_return = True # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # # Themes to choose from: # http://www.sphinx-doc.org/en/stable/theming.html # https://www.writethedocs.org/guide/tools/sphinx-themes/ # https://sphinx-themes.org/ html_theme = "sphinx_book_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] ================================================ FILE: docs_generate/game_data/index.rst ================================================ .. toctree:: :maxdepth: 2 ******************************************************** game_data.py ******************************************************** .. autoclass:: sc2.game_data.GameData :members: .. autoclass:: sc2.game_data.AbilityData :members: .. autoclass:: sc2.game_data.UnitTypeData :members: .. autoclass:: sc2.game_data.UpgradeData :members: .. autoclass:: sc2.game_data.Cost :members: ================================================ FILE: docs_generate/game_info/index.rst ================================================ .. toctree:: :maxdepth: 2 **************************** game_info.py **************************** .. autoclass:: sc2.game_info.Ramp :members: .. autoclass:: sc2.game_info.GameInfo :members: ================================================ FILE: docs_generate/game_state/index.rst ================================================ .. toctree:: :maxdepth: 2 ******************************************************** game_state.py ******************************************************** .. autoclass:: sc2.game_state.GameState :members: .. autoclass:: sc2.game_state.Blip :members: .. autoclass:: sc2.game_state.Common :members: .. autoclass:: sc2.game_state.EffectData :members: ================================================ FILE: docs_generate/ids/index.rst ================================================ .. toctree:: :maxdepth: 2 **************************** ids **************************** .. autoclass:: sc2.ids.ability_id.AbilityId :members: .. autoclass:: sc2.ids.buff_id.BuffId :members: .. autoclass:: sc2.ids.effect_id.EffectId :members: .. autoclass:: sc2.ids.unit_typeid.UnitTypeId :members: .. autoclass:: sc2.ids.upgrade_id.UpgradeId :members: ================================================ FILE: docs_generate/index.rst ================================================ .. python-sc2 documentation master file, created by sphinx-quickstart on Sat Jun 15 05:52:03 2019. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to python-sc2's documentation! ====================================== .. toctree:: :maxdepth: 2 text_files/introduction.rst text_files/docker.rst bot_ai/index.rst unit/index.rst units/index.rst game_data/index.rst game_info/index.rst game_state/index.rst client/index.rst position/index.rst pixel_map/index.rst distances/index.rst protocol/index.rst score/index.rst unit_command/index.rst ids/index.rst Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ================================================ FILE: docs_generate/make.bat ================================================ @ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd ================================================ FILE: docs_generate/pixel_map/index.rst ================================================ .. toctree:: :maxdepth: 2 **************************** pixel_map.py **************************** .. autoclass:: sc2.pixel_map.PixelMap :members: ================================================ FILE: docs_generate/position/index.rst ================================================ .. toctree:: :maxdepth: 2 **************************** position.py **************************** .. autoclass:: sc2.position.Pointlike :members: .. autoclass:: sc2.position.Point2 :members: .. autoclass:: sc2.position.Point3 :members: .. autoclass:: sc2.position.Size :members: .. autoclass:: sc2.position.Rect :members: ================================================ FILE: docs_generate/protocol/index.rst ================================================ .. toctree:: :maxdepth: 2 **************************** protocol.py **************************** .. autoclass:: sc2.protocol.Protocol :members: ================================================ FILE: docs_generate/score/index.rst ================================================ .. toctree:: :maxdepth: 2 **************************** score.py **************************** .. autoclass:: sc2.score.ScoreDetails :members: ================================================ FILE: docs_generate/text_files/docker.rst ================================================ ***************************** Using docker to run SC2 bots ***************************** This is a small overview on how to use docker to run 2 bots (here: python-sc2 bots) against each other using the linux binary SC2 client. For a quick summary of commands, scroll to the bottom. Requirements ------------ - Docker installed and running - Internet - Doesn't require a GPU Pulling the Docker image ------------------------ The SC2 AI community has decided to stay on Python3.10 for a while. I'll try to update the docker image as soon as a new linux binary is released, or create a pull request at https://github.com/BurnySc2/python-sc2-docker :: docker pull burnysc2/python-sc2-docker:release-python_3.10-sc2_4.10_arenaclient_burny Deleting previous containers ----------------------------- To remove the previously used ``app`` container:: docker run -it -d --name app test_image Launching a new container -------------------------- The following command launches a new container in interactive mode, which means it will not shut down once it is done running:: docker run -it -d --name app burnysc2/python-sc2-docker:release-python_3.10-sc2_4.10_arenaclient_burny Install bot requirements ------------------------- The command ``docker exec -i app uv add "burnysc2>=0.12.12"`` installs the ``burnysc2`` dependencies in the docker container. Add more libraries as needed. You can also create your custom docker image so you do not have to re-install the dependencies every time you create a new container. Since the linux SC2 binary is usually outdated (last update as of this writing was summer of 2019), you will likely have to replace your IDs with older IDs, which can be found here: https://github.com/BurnySc2/python-sc2/tree/linux-4.10/sc2/ids If you want your bots to play against a compiled bot (``.exe``), you will have to install ``wine``. I have not included wine in the docker image to keep the image as small as possible. Copying Bots to the container ------------------------------ The bots in the container need to be located under ``app:/root/StarCraftII/Bots/`` A copy command could be ``docker cp examples/competetive/. app:/root/StarCraftII/Bots/my_bot`` if you are in the main ``python-sc2` directory, which copies the competetive example bot to the container. The bots will be launched via ``run.py``. The ``ladderbots.json`` might not be needed. Don't forget to copy the python-sc2/sc2 folder, or else an older version might be used (on import) and your bot might not work correctly. For more info about competitive bots setup, see: https://github.com/BurnySc2/python-sc2/tree/develop/examples/competitive or https://eschamp.discourse.group/t/simple-starcraft-2-bot-template-to-get-started/155 Copying the runner to the container ------------------------------------ You will have to configure the ``custom_run_local.py`` file (ctrl+f for ``def main()``). It can be found here: https://github.com/BurnySc2/python-sc2/blob/fa4933a1bf89540a052482b1a394c8d6206d7491/bat_files/docker/custom_run_local.py You may also customize the arenaclient ``settings.json`` (e.g. max game time) which is located under ``/root/aiarena-client/arenaclient/proxy/settings.json`` Click here to check which settings are available: https://github.com/BurnySc2/aiarena-client/blob/a1cd2e9314e7fd2accd0e69aa77d89a9978e619c/arenaclient/proxy/server.py#L164-L170 After you are done customizing which matches should be played, run the following to finalize your setup:: docker cp bat_files/docker/custom_run_local.py app:/root/aiarena-client/arenaclient/run_local.py Running the match(es) --------------------- Now you are ready to let docker run your matches (headless):: docker exec -i app uv run python /root/aiarena-client/arenaclient/run_local.py Copying the replay from container to host machine -------------------------------------------------------------- To copy the ``results.json`` to the host machine to analyse the results, use:: mkdir -p temp docker cp app:/root/aiarena-client/arenaclient/proxy/results.json temp/results.json To copy all newly generated replays from the container, use:: mkdir -p temp/replays docker cp app:/root/StarCraftII/Replays/. temp/replays Summary using a shell script ----------------------------- For a full runner script, see: https://github.com/BurnySc2/python-sc2/blob/fa4933a1bf89540a052482b1a394c8d6206d7491/bat_files/docker/docker_run_bots.sh Docker cleanup --------------- See also: https://docs.docker.com/config/pruning/ Force removing all containers (including running):: docker rm -f $(docker ps -aq) Removing all images:: docker rmi $(docker images -q) Prune everything docker related:: docker system prune --volumes ================================================ FILE: docs_generate/text_files/introduction.rst ================================================ ************* Introduction ************* This is an overview to the BurnySc2/python-sc2 library which can be found here: https://github.com/BurnySc2/python-sc2 Requirements ------------- - Python 3.9 or newer - StarCraft 2 Client installation in the **default installation path** which should be ``C:\Program Files (x86)\StarCraft II`` Installation ------------- Install through pip using ``pip install burnysc2`` if Python is in your environment path, or go into your python installation folder and run through console ``pip install burnysc2``. Alternatively (of if the command above doesn't work) you can install a specific branch directly from github, here the develop branch:: pip install --upgrade --force-reinstall https://github.com/BurnySc2/python-sc2/archive/develop.zip Creating a bot --------------- A basic bot can be made by creating a new file `my_bot.py` and filling it with the following contents:: import sc2 from sc2.bot_ai import BotAI from sc2.player import Bot, Computer class MyBot(BotAI): async def on_step(self, iteration: int): print(f"This is my bot in iteration {iteration}!") sc2.run_game( sc2.maps.get("AcropolisLE"), [Bot(sc2.Race.Zerg, MyBot()), Computer(sc2.Race.Zerg, sc2.Difficulty.Hard)], realtime=False, ) You can now run the file using command ``python my_bot.py`` or double clicking the file. A SC2 window should open and your bot should print the text several times per second to the console. Your bot will not do anything else because no orders are issued to your units. Available information in the game ------------------------------------ Information about your bot:: # Resources and supply self.minerals: int self.vespene: int self.supply_army: int # 0 at game start self.supply_workers: int # 12 at game start self.supply_cap: int # 14 for zerg, 15 for T and P at game start self.supply_used: int # 12 at game start self.supply_left: int # 2 for zerg, 3 for T and P at game start # Units self.warp_gate_count: Units # Your warp gate count (only protoss) self.idle_worker_count: int # Workers that are doing nothing self.army_count: int # Amount of army units self.workers: Units # Your workers self.larva: Units # Your larva (only zerg) self.townhalls: Units # Your townhalls (nexus, hatchery, lair, hive, command center, orbital command, planetary fortress self.gas_buildings: Units # Your gas structures (refinery, extractor, assimilator self.units: Units # Your units (includes larva and workers) self.structures: Units # Your structures (includes townhalls and gas buildings) # Other information about your bot self.race: Race # The race your bot plays. If you chose random, your bot gets assigned a race and the assigned race will be in here (not random) self.player_id: int # Your bot id (can be 1 or 2 in a 2 player game) # Your spawn location (your first townhall location) self.start_location: Point2 # Location of your main base ramp, and has some information on how to wall the main base as terran bot (see GameInfo) self.main_base_ramp: Ramp Information about the enemy player:: # The following contains enemy units and structures inside your units' vision range (including invisible units, but not burrowed units) self.enemy_units: Units self.enemy_structures: Units # Enemy spawn locations as a list of Point2 points self.enemy_start_locations: list[Point2] # Enemy units that are inside your sensor tower range self.blips: set[Blip] # The enemy race. If the enemy chose random, this will stay at random forever self.enemy_race: Race Other information:: # Neutral units and structures self.mineral_field: Units # All mineral fields on the map self.vespene_geyser: Units # All vespene fields, even those that have a gas building on them self.resources: Units # Both of the above combined self.destructables: Units # All destructable rocks (except the platforms below the main base ramp) self.watchtowers: Units # All watch towers on the map (some maps don't have watch towers) self.all_units: Units # All units combined: yours, enemy's and neutral # Locations of possible expansions self.expansion_locations: dict[Point2, Units] # Game data about units, abilities and upgrades (see game_data.py) self.game_data: GameData # Information about the map: pathing grid, building placement, terrain height, vision and creep are found here (see game_info.py) self.game_info: GameInfo # Other information that gets updated every step (see game_state.py) self.state: GameState # Extra information self.realtime: bool # Displays if the game was started in realtime or not. In realtime, your bot only has limited time to execute on_step() self.time: float # The current game time in seconds self.time_formatted: str # The current game time properly formatted in 'min:sec' Possible bot actions --------------------- The game has started and now you want to build stuff with your mined resources. I assume you played at least one game of SC2 and know the basics, for example where you build drones (from larva) and SCVs and probes (from command center and nexus respectively). Training a unit ^^^^^^^^^^^^^^^^ Assuming you picked zerg for your bot and want to build a drone. Your larva is available in ``self.larva``. Your bot starts with 3 larva. To choose which of the larva you want to issue the command to train a drone, you need to pick one. The simplest you can do is ``my_larva = self.larva.random``. Now you have to issue a command to the larva: morph to drone. You can issue commands using the __call__ function: ``unit(ability)``. You have to import ability ids before you can use them. ``from sc2.ids.ability_id import AbilityId``. Here, the action can be ``my_larva(AbilityId.LARVATRAIN_DRONE)``. In total, this results in:: from sc2.ids.ability_id import AbilityId my_larva = self.larva.random my_larva(AbilityId.LARVATRAIN_DRONE) Important: The action will be issued after the ``on_step`` function is completed and the bot communicated with the SC2 Client over the API. This can result in unexpected behavior. Your larva count is still at three (``self.larva.amount == 3``), your minerals are still at 50 (``self.minerals == 50``) and your supply did not go up (``self.supply_used == 12``), but expected behavior might be that the larva amount drops to 2, self.minerals should be 0 and self.supply_used should be 13 since the pending drone uses up supply. The last two issues can be fixed by calling it differently, specifically:: self.larva.random(AbilityId.LARVATRAIN_DRONE, subtract_cost=True, subtract_supply=True) The keyword arguments are optional because many actions are move or attack commands, instead of train or build commands, thus making the bot slightly faster if only specific actions are checked if they have a cost associated. There are two more ways to do the same:: from sc2.ids.unit_typeid import UnitTypeId self.larva.random.train(UnitTypeId.DRONE) This converts the UnitTypeId to the AbilityId that is required to train the unit. Another way is to use the train function from the api:: self.train(UnitTypeId.DRONE, amount=1) This tries to figure out where to build the target unit from, and automatically subtracts the cost and supply after the train command was issued. If performance is important to you, you should try to give structures the train command directly from which you know they are idle and that you have enough resources to afford it. So a more performant way to train as many drones as possible is:: for loop_larva in self.larva: if self.can_afford(UnitTypeId.DRONE): loop_larva.train(UnitTypeId.DRONE) # Add break statement here if you only want to train one else: # Can't afford drones anymore break ``self.can_afford`` checks if you have enough resources and enough free supply to train the unit. ``self.do`` then automatically increases supply count and subtracts resource cost. Warning: You need to prevent issuing multiple commands to the same larva in the same frame (or iteration). The ``self.do`` function automatically adds the unit's tag to ``self.unit_tags_received_action``. This is a set with integers and it will be emptied every frame. So the final proper way to do it is:: for loop_larva in self.larva: if loop_larva.tag in self.unit_tags_received_action: continue if self.can_afford(UnitTypeId.DRONE): loop_larva.train(UnitTypeId.DRONE) # Add break statement here if you only want to train one else: # Can't afford drones anymore break Building a structure ^^^^^^^^^^^^^^^^^^^^^ Nearly the same procedure is when you want to build a structure. All that is needed is - Which building type should be built - Can you afford building it - Which worker should be used - Where should the building be placed The building type could be ``UnitTypeId.SPAWNINGPOOL``. To check if you can afford it you do ``if self.can_afford(UnitTypeId.SPAWNINGPOOL):``. Figuring out which worker to use is a bit more difficult. It could be a random worker (``my_worker = self.workers.random``) or a worker closest to the target building placement position (``my_worker = self.workers.closest_to(placement_position)``), but both of these have the issue that they could use a worker that is already busy (scouting, already on the way to build something, defending the base from worker rush). Usually worker that are mining or idle could be chosen to build something (``my_worker = self.workers.filter(lambda worker: worker.is_collecting or worker.is_idle).random``). There is an issue here that if the Units object is empty after filtering, ``.random`` will result in an assertion error. Lastly, figuring out where to place the spawning pool. This can be as easy as:: map_center = self.game_info.map_center placement_position = self.start_location.towards(map_center, distance=5) But then the question is, can you actually place it there? Is there creep, is it not blocked by a structure or enemy units? Building placement can be very difficult, if you don't want to place your buildings in your mineral line or want to leave enough space so that addons fit on the right of the structure (terran problems), or that you always leave 2x2 space between your structures so that your archons won't get stuck (protoss and terran problems). A function that can test which position is valid for a spawning pool is ``self.find_placement``, which finds a position near the given position. This function can be slow:: map_center = self.game_info.map_center position_towards_map_center = self.start_location.towards(map_center, distance=5) placement_position = await self.find_placement(UnitTypeId.SPAWNINGPOOL, near=position_towards_map_center, placement_step=1) # Can return None if no position was found if placement_position: One thing that was not mentioned yet is that you don't want to build more than 1 spawning pool. To prevent this, you can check that the number of pending and completed structures is zero:: if self.already_pending(UnitTypeId.SPAWNINGPOOL) + self.structures.filter(lambda structure: structure.type_id == UnitTypeId.SPAWNINGPOOL and structure.is_ready).amount == 0: # Build spawning pool So in total: To build a spawning pool in direction of the map center, it is recommended to use:: if self.can_afford(UnitTypeId.SPAWNINGPOOL) and self.already_pending(UnitTypeId.SPAWNINGPOOL) + self.structures.filter(lambda structure: structure.type_id == UnitTypeId.SPAWNINGPOOL and structure.is_ready).amount == 0: worker_candidates = self.workers.filter(lambda worker: (worker.is_collecting or worker.is_idle) and worker.tag not in self.unit_tags_received_action) # Worker_candidates can be empty if worker_candidates: map_center = self.game_info.map_center position_towards_map_center = self.start_location.towards(map_center, distance=5) placement_position = await self.find_placement(UnitTypeId.SPAWNINGPOOL, near=position_towards_map_center, placement_step=1) # Placement_position can be None if placement_position: build_worker = worker_candidates.closest_to(placement_position) build_worker.build(UnitTypeId.SPAWNINGPOOL, placement_position) The same can be achieved with the convenience function ``self.build`` which automatically picks a worker and internally uses ``self.find_placement``:: if self.can_afford(UnitTypeId.SPAWNINGPOOL) and self.already_pending(UnitTypeId.SPAWNINGPOOL) + self.structures.filter(lambda structure: structure.type_id == UnitTypeId.SPAWNINGPOOL and structure.is_ready).amount == 0: map_center = self.game_info.map_center position_towards_map_center = self.start_location.towards(map_center, distance=5) await self.build(UnitTypeId.SPAWNINGPOOL, near=position_towards_map_center, placement_step=1) ================================================ FILE: docs_generate/unit/index.rst ================================================ .. toctree:: :maxdepth: 2 **************************** unit.py **************************** .. autoclass:: sc2.unit.Unit :members: ================================================ FILE: docs_generate/unit_command/index.rst ================================================ .. toctree:: :maxdepth: 2 **************************** unit_command.py **************************** .. autoclass:: sc2.unit_command.UnitCommand :members: ================================================ FILE: docs_generate/units/index.rst ================================================ .. toctree:: :maxdepth: 2 **************************** units.py **************************** .. autoclass:: sc2.units.Units :members: ================================================ FILE: examples/__init__.py ================================================ from pathlib import Path __all__ = [p.stem for p in Path().iterdir() if p.is_file() and p.suffix == ".py" and p.stem != "__init__"] ================================================ FILE: examples/arcade_bot.py ================================================ """ To play an arcade map, you need to download the map first. Open the StarCraft2 Map Editor through the Battle.net launcher, in the top left go to File -> Open -> (Tab) Blizzard -> Log in -> with "Source: Map/Mod Name" search for your desired map, in this example "Marine Split Challenge-LOTV" map created by printf Hit "Ok" and confirm the download. Now that the map is opened, go to "File -> Save as" to store it on your hard drive. Now load the arcade map by entering your map name below in sc2.maps.get("YOURMAPNAME") without the .SC2Map extension Map info: You start with 30 marines, level N has 15+N speed banelings on creep Type in game "sling" to activate zergling+baneling combo Type in game "stim" to activate stimpack Improvements that could be made: - Make marines constantly run if they have a ling/bane very close to them - Split marines before engaging """ from __future__ import annotations from loguru import logger from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Race from sc2.ids.ability_id import AbilityId from sc2.ids.buff_id import BuffId from sc2.ids.unit_typeid import UnitTypeId from sc2.ids.upgrade_id import UpgradeId from sc2.main import run_game from sc2.player import Bot from sc2.position import Point2, Point3 from sc2.unit import Unit class MarineSplitChallenge(BotAI): async def on_start(self): await self.chat_send("Edit this message for automatic chat commands.") self.client.game_step = 2 async def on_step(self, iteration: int): # do marine micro vs zerglings for unit in self.units(UnitTypeId.MARINE): if self.enemy_units: # attack (or move towards) zerglings / banelings if unit.weapon_cooldown <= self.client.game_step / 2: enemies_in_range = self.enemy_units.filter(unit.target_in_range) # attack lowest hp enemy if any enemy is in range if enemies_in_range: # Use stimpack if ( self.already_pending_upgrade(UpgradeId.STIMPACK) == 1 and not unit.has_buff(BuffId.STIMPACK) and unit.health > 10 ): unit(AbilityId.EFFECT_STIM) # attack baneling first filtered_enemies_in_range = enemies_in_range.of_type(UnitTypeId.BANELING) if not filtered_enemies_in_range: filtered_enemies_in_range = enemies_in_range.of_type(UnitTypeId.ZERGLING) # attack lowest hp unit lowest_hp_enemy_in_range = min(filtered_enemies_in_range, key=lambda u: u.health) unit.attack(lowest_hp_enemy_in_range) # no enemy is in attack-range, so give attack command to closest instead else: closest_enemy = self.enemy_units.closest_to(unit) unit.attack(closest_enemy) # move away from zergling / banelings else: stutter_step_positions = self.position_around_unit(unit, distance=4) # filter in pathing grid stutter_step_positions = {p for p in stutter_step_positions if self.in_pathing_grid(p)} # find position furthest away from enemies and closest to unit enemies_in_range = self.enemy_units.filter(lambda u: unit.target_in_range(u, -0.5)) if stutter_step_positions and enemies_in_range: retreat_position = max( stutter_step_positions, key=lambda x: x.distance_to(enemies_in_range.center) - x.distance_to(unit), ) unit.move(retreat_position) else: logger.info(f"No retreat positions detected for unit {unit} at {unit.position.rounded}.") def position_around_unit( self, pos: Unit | Point2 | Point3, distance: int = 1, step_size: int = 1, exclude_out_of_bounds: bool = True, ): pos = pos.position.rounded positions = { pos.offset(Point2((x, y))) for x in range(-distance, distance + 1, step_size) for y in range(-distance, distance + 1, step_size) if (x, y) != (0, 0) } # filter positions outside map size if exclude_out_of_bounds: positions = { p for p in positions if 0 <= p[0] < self.game_info.pathing_grid.width and 0 <= p[1] < self.game_info.pathing_grid.height } return positions def main(): run_game( maps.get("Marine Split Challenge"), [Bot(Race.Terran, MarineSplitChallenge())], realtime=False, save_replay_as="Example.SC2Replay", ) if __name__ == "__main__": main() ================================================ FILE: examples/bot_vs_bot.py ================================================ """ This script shows how to let two custom bots play against each other. """ from __future__ import annotations from loguru import logger from examples.protoss.warpgate_push import WarpGateBot from examples.zerg.zerg_rush import ZergRushBot from sc2 import maps from sc2.data import Race, Result from sc2.main import GameMatch, run_game, run_multiple_games from sc2.player import Bot def main_old(): result: Result | list[Result | None] = run_game( maps.get("AcropolisLE"), [ Bot(Race.Protoss, WarpGateBot()), Bot(Race.Zerg, ZergRushBot()), ], realtime=False, game_time_limit=2, save_replay_as="Example.SC2Replay", ) logger.info(f"Result: {result}") def main(): result = run_multiple_games( [ GameMatch( map_sc2=maps.get("AcropolisLE"), players=[ Bot(Race.Protoss, WarpGateBot()), Bot(Race.Zerg, ZergRushBot()), ], realtime=False, game_time_limit=2, ) ] ) logger.info(f"Result: {result}") if __name__ == "__main__": main_old() # TODO Why does "run_multiple_games" get stuck? # main() ================================================ FILE: examples/competitive/README.md ================================================ # Example competitive bot This is a small example bot that should work with [AI-Arena](https://aiarena.net/), [Sc2AI](https://sc2ai.net/) and [Probots](https://eschamp.com/shows/probots/). Copy the "python-sc2/sc2" folder inside this folder before distributing your bot for competition. This prevents the default python-sc2 installation to be loaded/imported, which could be vastly outdated. Change the bot race in the [run.py](run.py) (line 8) and in the [ladderbots.json](ladderbots.json) file (line 4). Zip the entire folder to a .zip file. Make sure that the files are in the root folder of the zip. https://aiarena.net/wiki/getting-started/#wiki-toc-bot-zip ## AI Arena To compete on AI Arena... Make sure to notify AI-Arena if you need additional requirements (python packages) for your bot to run. A "requirements.txt" is not going to be read. Make an account on https://aiarena.net/ and upload the zip file as a new bot. Make sure to select the right race and bot type (python). ## Sc2AI & Probots The [ladderbots.json](ladderbots.json) file contains parameters to support play for Sc2AI and Probots. Don't forget to update them! Both Sc2AI and Probots will pip install your "requirements.txt" file for you. ================================================ FILE: examples/competitive/__init__.py ================================================ import argparse import asyncio import aiohttp from loguru import logger from sc2.client import Client from sc2.main import _play_game from sc2.portconfig import Portconfig from sc2.protocol import ConnectionAlreadyClosedError # Run ladder game # This lets python-sc2 connect to a LadderManager game: https://github.com/Cryptyc/Sc2LadderServer # Based on: https://github.com/Dentosal/python-sc2/blob/master/examples/run_external.py def run_ladder_game(bot): # Load command line arguments parser = argparse.ArgumentParser() parser.add_argument("--GamePort", type=int, nargs="?", help="Game port") parser.add_argument("--StartPort", type=int, nargs="?", help="Start port") parser.add_argument("--LadderServer", type=str, nargs="?", help="Ladder server") parser.add_argument("--ComputerOpponent", type=str, nargs="?", help="Computer opponent") parser.add_argument("--ComputerRace", type=str, nargs="?", help="Computer race") parser.add_argument("--ComputerDifficulty", type=str, nargs="?", help="Computer difficulty") parser.add_argument("--OpponentId", type=str, nargs="?", help="Opponent ID") parser.add_argument("--RealTime", action="store_true", help="Real time flag") args, _unknown = parser.parse_known_args() host = "127.0.0.1" if args.LadderServer is None else args.LadderServer host_port = args.GamePort lan_port = args.StartPort # Add opponent_id to the bot class (accessed through self.opponent_id) bot.ai.opponent_id = args.OpponentId realtime = args.RealTime # Port config if lan_port is None: portconfig = None else: ports = [lan_port + p for p in range(1, 6)] portconfig = Portconfig() portconfig.server = [ports[1], ports[2]] portconfig.players = [[ports[3], ports[4]]] # Join ladder game g = join_ladder_game(host=host, port=host_port, players=[bot], realtime=realtime, portconfig=portconfig) # Run it result = asyncio.get_event_loop().run_until_complete(g) return result, args.OpponentId # Modified version of sc2.main._join_game to allow custom host and port, and to not spawn an additional sc2process (thanks to alkurbatov for fix) async def join_ladder_game(host, port, players, realtime, portconfig, save_replay_as=None, game_time_limit=None): ws_url = f"ws://{host}:{port}/sc2api" # pyrefly: ignore ws_connection = await aiohttp.ClientSession().ws_connect(ws_url, timeout=120) client = Client(ws_connection) try: result = await _play_game(players[0], client, realtime, portconfig, game_time_limit) if save_replay_as is not None: await client.save_replay(save_replay_as) # await client.leave() # await client.quit() except ConnectionAlreadyClosedError: logger.error("Connection was closed before the game ended") return None finally: await ws_connection.close() return result ================================================ FILE: examples/competitive/bot.py ================================================ from sc2.bot_ai import BotAI from sc2.data import Result class CompetitiveBot(BotAI): async def on_start(self): print("Game started") # Do things here before the game starts async def on_step(self, iteration: int): # Populate this function with whatever your bot should do! pass async def on_end(self, game_result: Result): print("Game ended.") # Do things here after the game ends ================================================ FILE: examples/competitive/ladderbots.json ================================================ { "Bots": { "YOUR_BOTS_NAME_HERE": { "Race": "Put Terran Zerg or Protoss here", "Type": "Python", "RootPath": "./", "FileName": "run.py", "Args": "-O", "Debug": true, "SurrenderPhrase": "(pineapple)" } } } ================================================ FILE: examples/competitive/run.py ================================================ import sys from examples.competitive.__init__ import run_ladder_game # Load bot from examples.competitive.bot import CompetitiveBot from sc2 import maps from sc2.data import Difficulty, Race from sc2.main import run_game from sc2.player import Bot, Computer bot = Bot(Race.Terran, CompetitiveBot()) # Start game if __name__ == "__main__": if "--LadderServer" in sys.argv: # Ladder game started by LadderManager print("Starting ladder game...") result, opponentid = run_ladder_game(bot) print(result, " against opponent ", opponentid) else: # Local game print("Starting local game...") run_game(maps.get("Abyssal Reef LE"), [bot, Computer(Race.Protoss, Difficulty.VeryHard)], realtime=True) ================================================ FILE: examples/distributed_workers.py ================================================ from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race from sc2.ids.unit_typeid import UnitTypeId from sc2.main import run_game from sc2.player import Bot, Computer class TerranBot(BotAI): async def on_step(self, iteration: int): await self.distribute_workers() await self.build_supply() await self.build_workers() await self.expand() async def build_workers(self): for cc in self.townhalls(UnitTypeId.COMMANDCENTER).ready.idle: if self.can_afford(UnitTypeId.SCV): cc.train(UnitTypeId.SCV) async def expand(self): if self.townhalls(UnitTypeId.COMMANDCENTER).amount < 3 and self.can_afford(UnitTypeId.COMMANDCENTER): await self.expand_now() async def build_supply(self): ccs = self.townhalls(UnitTypeId.COMMANDCENTER).ready if ccs.exists: cc = ccs.first if self.supply_left < 4 and not self.already_pending(UnitTypeId.SUPPLYDEPOT): if self.can_afford(UnitTypeId.SUPPLYDEPOT): await self.build(UnitTypeId.SUPPLYDEPOT, near=cc.position.towards(self.game_info.map_center, 5)) if __name__ == "__main__": run_game( maps.get("Abyssal Reef LE"), [Bot(Race.Terran, TerranBot()), Computer(Race.Protoss, Difficulty.Medium)], realtime=False, ) ================================================ FILE: examples/external_bot.py ================================================ from pathlib import Path from sc2 import maps from sc2.data import Race from sc2.main import GameMatch, run_multiple_games from sc2.player import BotProcess, Computer def main(): run_multiple_games( [ GameMatch( maps.get("AcropolisLE"), [ # Enable up to 2 of the 4 following bots to test this file # Assuming you launch external_bot.py from the root directory of 'python-sc2' BotProcess( Path.cwd(), ["python", "examples/competitive/run.py"], Race.Terran, "CompetiveBot", stdout="temp.txt", ), # Bot(Race.Zerg, ZergRushBot()), # Bot(Race.Zerg, ZergRushBot()), Computer(Race.Zerg), ], realtime=True, ), ] ) if __name__ == "__main__": main() ================================================ FILE: examples/fastreload.py ================================================ from importlib import reload from examples.zerg import zerg_rush from sc2 import maps from sc2.data import Difficulty, Race from sc2.main import _host_game_iter from sc2.player import AbstractPlayer, Bot, Computer def main(): player_config: list[AbstractPlayer] = [ Bot(Race.Zerg, zerg_rush.ZergRushBot()), Computer(Race.Terran, Difficulty.Medium), ] gen = _host_game_iter(maps.get("Abyssal Reef LE"), player_config, realtime=False) _r = next(gen) while True: input("Press enter to reload ") reload(zerg_rush) if isinstance(player_config[0], Bot): player_config[0].ai = zerg_rush.ZergRushBot() gen.send(player_config) if __name__ == "__main__": main() ================================================ FILE: examples/host_external_norestart.py ================================================ from examples.zerg.zerg_rush import ZergRushBot from sc2 import maps from sc2.data import Race from sc2.main import _host_game_iter from sc2.player import Bot from sc2.portconfig import Portconfig def main(): portconfig: Portconfig = Portconfig() print(portconfig.as_json) # pyrefly: ignore player_config = [Bot(Race.Zerg, ZergRushBot()), Bot(Race.Zerg, None)] for g in _host_game_iter(maps.get("Abyssal Reef LE"), player_config, realtime=False, portconfig=portconfig): print(g) if __name__ == "__main__": main() ================================================ FILE: examples/observer_easy_vs_easy.py ================================================ from examples.protoss.cannon_rush import CannonRushBot from sc2 import maps from sc2.data import Difficulty, Race from sc2.main import run_game from sc2.player import Bot, Computer def main(): run_game( maps.get("Abyssal Reef LE"), [Bot(Race.Protoss, CannonRushBot()), Computer(Race.Protoss, Difficulty.Medium)], realtime=True, ) if __name__ == "__main__": main() ================================================ FILE: examples/play_tvz.py ================================================ """ This script let's you play as human against a simple zerg rush bot. """ from examples.zerg.zerg_rush import ZergRushBot from sc2 import maps from sc2.data import Race from sc2.main import run_game from sc2.player import Bot, Human def main(): run_game(maps.get("Abyssal Reef LE"), [Human(Race.Terran), Bot(Race.Zerg, ZergRushBot())], realtime=True) if __name__ == "__main__": main() ================================================ FILE: examples/protoss/__init__.py ================================================ from pathlib import Path __all__ = [p.stem for p in Path().iterdir() if p.is_file() and p.suffix == ".py" and p.stem != "__init__"] ================================================ FILE: examples/protoss/cannon_rush.py ================================================ import random from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race from sc2.ids.unit_typeid import UnitTypeId from sc2.main import run_game from sc2.player import Bot, Computer class CannonRushBot(BotAI): async def on_step(self, iteration: int): if iteration == 0: await self.chat_send("(probe)(pylon)(cannon)(cannon)(gg)") if not self.townhalls: # Attack with all workers if we don't have any nexuses left, attack-move on enemy spawn (doesn't work on 4 player map) so that probes auto attack on the way for worker in self.workers: worker.attack(self.enemy_start_locations[0]) return nexus = self.townhalls.random # Make probes until we have 16 total if self.supply_workers < 16 and nexus.is_idle: if self.can_afford(UnitTypeId.PROBE): nexus.train(UnitTypeId.PROBE) # If we have no pylon, build one near starting nexus elif not self.structures(UnitTypeId.PYLON) and self.already_pending(UnitTypeId.PYLON) == 0: if self.can_afford(UnitTypeId.PYLON): await self.build(UnitTypeId.PYLON, near=nexus) # If we have no forge, build one near the pylon that is closest to our starting nexus elif not self.structures(UnitTypeId.FORGE): pylon_ready = self.structures(UnitTypeId.PYLON).ready if pylon_ready: if self.can_afford(UnitTypeId.FORGE): await self.build(UnitTypeId.FORGE, near=pylon_ready.closest_to(nexus)) # If we have less than 2 pylons, build one at the enemy base elif self.structures(UnitTypeId.PYLON).amount < 2: if self.can_afford(UnitTypeId.PYLON): pos = self.enemy_start_locations[0].towards(self.game_info.map_center, random.randrange(8, 15)) await self.build(UnitTypeId.PYLON, near=pos) # If we have no cannons but at least 2 completed pylons, automatically find a placement location and build them near enemy start location elif not self.structures(UnitTypeId.PHOTONCANNON): if self.structures(UnitTypeId.PYLON).ready.amount >= 2 and self.can_afford(UnitTypeId.PHOTONCANNON): pylon = self.structures(UnitTypeId.PYLON).closer_than(20, self.enemy_start_locations[0]).random await self.build(UnitTypeId.PHOTONCANNON, near=pylon) # Decide if we should make pylon or cannons, then build them at random location near enemy spawn elif self.can_afford(UnitTypeId.PYLON) and self.can_afford(UnitTypeId.PHOTONCANNON): # Ensure "fair" decision for _ in range(20): pos = self.enemy_start_locations[0].random_on_distance(random.randrange(5, 12)) building = UnitTypeId.PHOTONCANNON if self.state.psionic_matrix.covers(pos) else UnitTypeId.PYLON await self.build(building, near=pos) def main(): run_game( maps.get("(2)CatalystLE"), [Bot(Race.Protoss, CannonRushBot(), name="CheeseCannon"), Computer(Race.Protoss, Difficulty.Medium)], realtime=False, ) if __name__ == "__main__": main() ================================================ FILE: examples/protoss/find_adept_shades.py ================================================ import math from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId from sc2.main import run_game from sc2.player import Bot, Computer from sc2.position import Point2 class FindAdeptShadesBot(BotAI): def __init__(self): self.shaded = False self.shades_mapping: dict[int, int] = {} async def on_start(self): self.client.game_step = 2 await self.client.debug_create_unit( [(UnitTypeId.ADEPT, 10, self.townhalls[0].position.towards(self.game_info.map_center, 5), 1)] ) async def on_step(self, iteration: int): adepts = self.units(UnitTypeId.ADEPT) if adepts and not self.shaded: # Wait for adepts to spawn and then cast ability for adept in adepts: adept(AbilityId.ADEPTPHASESHIFT_ADEPTPHASESHIFT, self.game_info.map_center) self.shaded = True elif self.shades_mapping: # Debug log and draw a line between the two units for adept_tag, shade_tag in self.shades_mapping.items(): adept = self.units.find_by_tag(adept_tag) shade = self.units.find_by_tag(shade_tag) if shade: # logger.info(f"Remaining shade time: {shade.buff_duration_remain} / {shade.buff_duration_max}") pass if adept and shade: self.client.debug_line_out(adept, shade, (0, 255, 0)) # logger.info(self.shades_mapping) elif self.shaded: # Find shades shades = self.units(UnitTypeId.ADEPTPHASESHIFT) for shade in shades: remaining_adepts = adepts.tags_not_in(self.shades_mapping) # Figure out where the shade should have been "self.client.game_step"-frames ago forward_position = Point2( (shade.position.x + math.cos(shade.facing), shade.position.y + math.sin(shade.facing)) ) previous_shade_location = shade.position.towards( forward_position, -(self.client.game_step / 16) * shade.movement_speed ) # See docstring of movement_speed attribute closest_adept = remaining_adepts.closest_to(previous_shade_location) self.shades_mapping[closest_adept.tag] = shade.tag def main(): run_game( maps.get("(2)CatalystLE"), [Bot(Race.Protoss, FindAdeptShadesBot()), Computer(Race.Protoss, Difficulty.Medium)], realtime=False, ) if __name__ == "__main__": main() ================================================ FILE: examples/protoss/threebase_voidray.py ================================================ from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race from sc2.ids.ability_id import AbilityId from sc2.ids.buff_id import BuffId from sc2.ids.unit_typeid import UnitTypeId from sc2.main import run_game from sc2.player import Bot, Computer class ThreebaseVoidrayBot(BotAI): async def on_step(self, iteration: int): target_base_count = 3 target_stargate_count = 3 if iteration == 0: await self.chat_send("(glhf)") if not self.townhalls.ready: # Attack with all workers if we don't have any nexuses left, attack-move on enemy spawn (doesn't work on 4 player map) so that probes auto attack on the way for worker in self.workers: worker.attack(self.enemy_start_locations[0]) return nexus = self.townhalls.ready.random # If this random nexus is not idle and has not chrono buff, chrono it with one of the nexuses we have if not nexus.is_idle and not nexus.has_buff(BuffId.CHRONOBOOSTENERGYCOST): nexuses = self.structures(UnitTypeId.NEXUS) abilities = await self.get_available_abilities(nexuses) for loop_nexus, abilities_nexus in zip(nexuses, abilities): if AbilityId.EFFECT_CHRONOBOOSTENERGYCOST in abilities_nexus: loop_nexus(AbilityId.EFFECT_CHRONOBOOSTENERGYCOST, nexus) break # If we have at least 5 void rays, attack closes enemy unit/building, or if none is visible: attack move towards enemy spawn if self.units(UnitTypeId.VOIDRAY).amount > 5: for vr in self.units(UnitTypeId.VOIDRAY): # Activate charge ability if the void ray just attacked if vr.weapon_cooldown > 0: vr(AbilityId.EFFECT_VOIDRAYPRISMATICALIGNMENT) # Choose target and attack, filter out invisible targets targets = (self.enemy_units | self.enemy_structures).filter(lambda unit: unit.can_be_attacked) if targets: target = targets.closest_to(vr) vr.attack(target) else: vr.attack(self.enemy_start_locations[0]) # Distribute workers in gas and across bases await self.distribute_workers() # If we are low on supply, build pylon if ( self.supply_left < 2 and self.already_pending(UnitTypeId.PYLON) == 0 or self.supply_used > 15 and self.supply_left < 4 and self.already_pending(UnitTypeId.PYLON) < 2 ): # Always check if you can afford something before you build it if self.can_afford(UnitTypeId.PYLON): await self.build(UnitTypeId.PYLON, near=nexus) # Train probe on nexuses that are undersaturated (avoiding distribute workers functions) # if nexus.assigned_harvesters < nexus.ideal_harvesters and nexus.is_idle: if self.supply_workers + self.already_pending(UnitTypeId.PROBE) < self.townhalls.amount * 22 and nexus.is_idle: if self.can_afford(UnitTypeId.PROBE): nexus.train(UnitTypeId.PROBE) # If we have less than 3 nexuses and none pending yet, expand if self.townhalls.ready.amount + self.already_pending(UnitTypeId.NEXUS) < 3: if self.can_afford(UnitTypeId.NEXUS): await self.expand_now() # Once we have a pylon completed if self.structures(UnitTypeId.PYLON).ready: pylon = self.structures(UnitTypeId.PYLON).ready.random if self.structures(UnitTypeId.GATEWAY).ready: # If we have gateway completed, build cyber core if not self.structures(UnitTypeId.CYBERNETICSCORE): if ( self.can_afford(UnitTypeId.CYBERNETICSCORE) and self.already_pending(UnitTypeId.CYBERNETICSCORE) == 0 ): await self.build(UnitTypeId.CYBERNETICSCORE, near=pylon) else: # If we have no gateway, build gateway if self.can_afford(UnitTypeId.GATEWAY) and self.already_pending(UnitTypeId.GATEWAY) == 0: await self.build(UnitTypeId.GATEWAY, near=pylon) # Build gas near completed nexuses once we have a cybercore (does not need to be completed if self.structures(UnitTypeId.CYBERNETICSCORE): for nexus in self.townhalls.ready: vgs = self.vespene_geyser.closer_than(15, nexus) for vg in vgs: if self.can_afford(UnitTypeId.ASSIMILATOR): worker = self.select_build_worker(vg.position) if worker is not None: if not self.gas_buildings or not self.gas_buildings.closer_than(1, vg): worker.build_gas(vg) worker.stop(queue=True) # If we have less than 3 but at least 3 nexuses, build stargate if self.structures(UnitTypeId.PYLON).ready and self.structures(UnitTypeId.CYBERNETICSCORE).ready: pylon = self.structures(UnitTypeId.PYLON).ready.random if ( self.townhalls.ready.amount + self.already_pending(UnitTypeId.NEXUS) >= target_base_count and self.structures(UnitTypeId.STARGATE).ready.amount + self.already_pending(UnitTypeId.STARGATE) < target_stargate_count ): if self.can_afford(UnitTypeId.STARGATE): await self.build(UnitTypeId.STARGATE, near=pylon) # Save up for expansions, loop over idle completed stargates and queue void ray if we can afford if self.townhalls.amount >= 3: for sg in self.structures(UnitTypeId.STARGATE).ready.idle: if self.can_afford(UnitTypeId.VOIDRAY): sg.train(UnitTypeId.VOIDRAY) def main(): run_game( maps.get("(2)CatalystLE"), [Bot(Race.Protoss, ThreebaseVoidrayBot()), Computer(Race.Protoss, Difficulty.Easy)], realtime=False, ) if __name__ == "__main__": main() ================================================ FILE: examples/protoss/warpgate_push.py ================================================ from loguru import logger from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race from sc2.ids.ability_id import AbilityId from sc2.ids.buff_id import BuffId from sc2.ids.unit_typeid import UnitTypeId from sc2.ids.upgrade_id import UpgradeId from sc2.main import run_game from sc2.player import Bot, Computer from sc2.unit import Unit class WarpGateBot(BotAI): def __init__(self): # Initialize inherited class self.proxy_built = False async def warp_new_units(self, proxy: Unit): for warpgate in self.structures(UnitTypeId.WARPGATE).ready: abilities = await self.get_available_abilities([warpgate]) # all the units have the same cooldown anyway so let's just look at ZEALOT if AbilityId.WARPGATETRAIN_STALKER in abilities[0]: pos = proxy.position.to2.random_on_distance(4) placement = await self.find_placement(AbilityId.WARPGATETRAIN_STALKER, pos, placement_step=1) if placement is None: # return ActionResult.CantFindPlacementLocation logger.info("can't place") return warpgate.warp_in(UnitTypeId.STALKER, placement) async def on_step(self, iteration: int): await self.distribute_workers() if not self.townhalls.ready: # Attack with all workers if we don't have any nexuses left, attack-move on enemy spawn (doesn't work on 4 player map) so that probes auto attack on the way for worker in self.workers: worker.attack(self.enemy_start_locations[0]) return nexus = self.townhalls.ready.random # Build pylon when on low supply if self.supply_left < 2 and self.already_pending(UnitTypeId.PYLON) == 0: # Always check if you can afford something before you build it if self.can_afford(UnitTypeId.PYLON): await self.build(UnitTypeId.PYLON, near=nexus) return if self.workers.amount < self.townhalls.amount * 22 and nexus.is_idle: if self.can_afford(UnitTypeId.PROBE): nexus.train(UnitTypeId.PROBE) elif self.structures(UnitTypeId.PYLON).amount < 5 and self.already_pending(UnitTypeId.PYLON) == 0: if self.can_afford(UnitTypeId.PYLON): await self.build(UnitTypeId.PYLON, near=nexus.position.towards(self.game_info.map_center, 5)) proxy = None if self.structures(UnitTypeId.PYLON).ready: proxy = self.structures(UnitTypeId.PYLON).closest_to(self.enemy_start_locations[0]) pylon = self.structures(UnitTypeId.PYLON).ready.random if self.structures(UnitTypeId.GATEWAY).ready: # If we have no cyber core, build one if not self.structures(UnitTypeId.CYBERNETICSCORE): if ( self.can_afford(UnitTypeId.CYBERNETICSCORE) and self.already_pending(UnitTypeId.CYBERNETICSCORE) == 0 ): await self.build(UnitTypeId.CYBERNETICSCORE, near=pylon) # Build up to 4 gates if ( self.can_afford(UnitTypeId.GATEWAY) and self.structures(UnitTypeId.WARPGATE).amount + self.structures(UnitTypeId.GATEWAY).amount < 4 ): await self.build(UnitTypeId.GATEWAY, near=pylon) # Build gas for nexus in self.townhalls.ready: vgs = self.vespene_geyser.closer_than(15, nexus) for vg in vgs: if self.can_afford(UnitTypeId.ASSIMILATOR): worker = self.select_build_worker(vg.position) if worker is not None: if not self.gas_buildings or not self.gas_buildings.closer_than(1, vg): worker.build_gas(vg) worker.stop(queue=True) # Research warp gate if cybercore is completed if ( self.structures(UnitTypeId.CYBERNETICSCORE).ready and self.can_afford(AbilityId.RESEARCH_WARPGATE) and self.already_pending_upgrade(UpgradeId.WARPGATERESEARCH) == 0 ): ccore = self.structures(UnitTypeId.CYBERNETICSCORE).ready.first ccore.research(UpgradeId.WARPGATERESEARCH) # Morph to warp gate when research is complete for gateway in self.structures(UnitTypeId.GATEWAY).ready.idle: if self.already_pending_upgrade(UpgradeId.WARPGATERESEARCH) == 1: gateway(AbilityId.MORPH_WARPGATE) if self.proxy_built and proxy: await self.warp_new_units(proxy) # Make stalkers attack either closest enemy unit or enemy spawn location if self.units(UnitTypeId.STALKER).amount > 3: for stalker in self.units(UnitTypeId.STALKER).ready.idle: targets = (self.enemy_units | self.enemy_structures).filter(lambda unit: unit.can_be_attacked) if targets: target = targets.closest_to(stalker) stalker.attack(target) else: stalker.attack(self.enemy_start_locations[0]) # Build proxy pylon if ( self.structures(UnitTypeId.CYBERNETICSCORE).amount >= 1 and not self.proxy_built and self.can_afford(UnitTypeId.PYLON) ): p = self.game_info.map_center.towards(self.enemy_start_locations[0], 20) await self.build(UnitTypeId.PYLON, near=p) self.proxy_built = True # Chrono nexus if cybercore is not ready, else chrono cybercore if not self.structures(UnitTypeId.CYBERNETICSCORE).ready: if not nexus.has_buff(BuffId.CHRONOBOOSTENERGYCOST) and not nexus.is_idle: if nexus.energy >= 50: nexus(AbilityId.EFFECT_CHRONOBOOSTENERGYCOST, nexus) else: ccore = self.structures(UnitTypeId.CYBERNETICSCORE).ready.first if not ccore.has_buff(BuffId.CHRONOBOOSTENERGYCOST) and not ccore.is_idle: if nexus.energy >= 50: nexus(AbilityId.EFFECT_CHRONOBOOSTENERGYCOST, ccore) def main(): run_game( maps.get("(2)CatalystLE"), [Bot(Race.Protoss, WarpGateBot()), Computer(Race.Protoss, Difficulty.Easy)], realtime=False, ) if __name__ == "__main__": main() ================================================ FILE: examples/simulate_fight_scenario.py ================================================ from loguru import logger from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race from sc2.ids.unit_typeid import UnitTypeId from sc2.main import run_game from sc2.player import Bot, Computer from sc2.position import Point2 MY_PLAYER_ID = 1 OPPONENT_PLAYER_ID = 2 class FightBot(BotAI): def __init__(self): super().__init__() self.enemy_location: Point2 | None = None self.fight_started = False async def on_start(self): # Retrieve control by enabling enemy control and showing whole map await self.client.debug_show_map() await self.client.debug_control_enemy() async def on_step(self, iteration: int): # Wait till control retrieved, destroy all starting units, recreate the world if iteration > 0 and self.enemy_units and not self.enemy_location: await self.reset_arena() if (self.units or self.structures) and (self.enemy_units or self.enemy_structures): self.enemy_location = (self.enemy_units + self.enemy_structures).center self.fight_started = True await self.manage_enemy_units() await self.manage_own_units() # In case of no units left - do not wait for game to finish if self.fight_started and (not self.units or not self.enemy_units): logger.info("LOSE" if not self.units else "WIN") await self.client.quit() # or reset level return async def reset_arena(self): if self.enemy_location is None: return await self.client.debug_kill_unit(self.all_units) await self.client.debug_create_unit( [ (UnitTypeId.SUPPLYDEPOT, 1, self.enemy_location, OPPONENT_PLAYER_ID), (UnitTypeId.MARINE, 4, self.enemy_location.towards(self.start_location, 8), OPPONENT_PLAYER_ID), ] ) await self.client.debug_create_unit( [ (UnitTypeId.SUPPLYDEPOT, 1, self.start_location, MY_PLAYER_ID), (UnitTypeId.MARINE, 4, self.start_location.towards(self.enemy_location, 8), MY_PLAYER_ID), ] ) async def manage_enemy_units(self): for unit in self.enemy_units: unit.attack(self.start_location) async def manage_own_units(self): if self.enemy_location is None: return for unit in self.units(UnitTypeId.MARINE): unit.attack(self.enemy_location) # TODO: implement your fight logic here # if unit.weapon_cooldown != 0: # unit.move(u.position.towards(self.start_location)) # else: # unit.attack(self.enemy_location) # pass def main(): run_game( maps.get("Flat64"), # NOTE: you can have two bots fighting with each other here [Bot(Race.Terran, FightBot()), Computer(Race.Terran, Difficulty.Medium)], realtime=True, ) if __name__ == "__main__": main() ================================================ FILE: examples/terran/__init__.py ================================================ from pathlib import Path __all__ = [p.stem for p in Path().iterdir() if p.is_file() and p.suffix == ".py" and p.stem != "__init__"] ================================================ FILE: examples/terran/cyclone_push.py ================================================ from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race from sc2.ids.unit_typeid import UnitTypeId from sc2.main import run_game from sc2.player import Bot, Computer from sc2.position import Point2 from sc2.unit import Unit from sc2.units import Units class CyclonePush(BotAI): def select_target(self) -> Point2: # Pick a random enemy structure's position targets = self.enemy_structures if targets: return targets.random.position # Pick a random enemy unit's position targets = self.enemy_units if targets: return targets.random.position # Pick enemy start location if it has no friendly units nearby if min(unit.distance_to(self.enemy_start_locations[0]) for unit in self.units) > 5: return self.enemy_start_locations[0] # Pick a random mineral field on the map return self.mineral_field.random.position async def on_step(self, iteration: int): CCs: Units = self.townhalls(UnitTypeId.COMMANDCENTER) # If no command center exists, attack-move with all workers and cyclones if not CCs: target = self.structures.random_or(self.enemy_start_locations[0]).position for unit in self.workers | self.units(UnitTypeId.CYCLONE): unit.attack(target) return # Otherwise, grab the first command center from the list of command centers cc: Unit = CCs.first # Every 50 iterations (here: every 50*8 = 400 frames) if iteration % 50 == 0 and self.units(UnitTypeId.CYCLONE).amount > 2: target: Point2 = self.select_target() forces: Units = self.units(UnitTypeId.CYCLONE) # Every 4000 frames: send all forces to attack-move the target position if iteration % 500 == 0: for unit in forces: unit.attack(target) # Every 400 frames: only send idle forces to attack the target position else: for unit in forces.idle: unit.attack(target) # While we have less than 22 workers: build more # Check if we can afford them (by minerals and by supply) if ( self.can_afford(UnitTypeId.SCV) and self.supply_workers + self.already_pending(UnitTypeId.SCV) < 22 and cc.is_idle ): cc.train(UnitTypeId.SCV) # Build supply depots if we are low on supply, do not construct more than 2 at a time elif self.supply_left < 3: if self.can_afford(UnitTypeId.SUPPLYDEPOT) and self.already_pending(UnitTypeId.SUPPLYDEPOT) < 2: # This picks a near-random worker to build a depot at location # 'from command center towards game center, distance 8' await self.build(UnitTypeId.SUPPLYDEPOT, near=cc.position.towards(self.game_info.map_center, 8)) # If we have supply depots (careful, lowered supply depots have a different UnitTypeId: UnitTypeId.SUPPLYDEPOTLOWERED) if self.structures(UnitTypeId.SUPPLYDEPOT): # If we have no barracks if not self.structures(UnitTypeId.BARRACKS): # If we can afford barracks if self.can_afford(UnitTypeId.BARRACKS): # Near same command as above with the depot await self.build(UnitTypeId.BARRACKS, near=cc.position.towards(self.game_info.map_center, 8)) # If we have a barracks (complete or under construction) and less than 2 gas structures (here: refineries) if self.structures(UnitTypeId.BARRACKS) and self.gas_buildings.amount < 2: if self.can_afford(UnitTypeId.REFINERY): # All the vespene geysirs nearby, including ones with a refinery on top of it vgs = self.vespene_geyser.closer_than(10, cc) for vg in vgs: has_refinery = self.gas_buildings.filter(lambda unit: unit.distance_to(vg) < 1) if has_refinery: continue # Select a worker closest to the vespene geysir worker: Unit | None = self.select_build_worker(vg) # Worker can be none in cases where all workers are dead # or 'select_build_worker' function only selects from workers which carry no minerals if worker is not None: # Issue the build command to the worker, important: vg has to be a Unit, not a position worker.build_gas(vg) # If we have at least one barracks that is completed, build factory if self.structures(UnitTypeId.BARRACKS).ready: if self.structures(UnitTypeId.FACTORY).amount < 3 and not self.already_pending(UnitTypeId.FACTORY): if self.can_afford(UnitTypeId.FACTORY): position: Point2 = cc.position.towards_with_random_angle(self.game_info.map_center, 16) await self.build(UnitTypeId.FACTORY, near=position) for factory in self.structures(UnitTypeId.FACTORY).ready.idle: # Reactor allows us to build two at a time if self.can_afford(UnitTypeId.CYCLONE): factory.train(UnitTypeId.CYCLONE) # Saturate gas for refinery in self.gas_buildings: if refinery.assigned_harvesters < refinery.ideal_harvesters: workers: Units = self.workers.closer_than(10, refinery) if workers: workers.random.gather(refinery) for scv in self.workers.idle: scv.gather(self.mineral_field.closest_to(cc)) def main(): run_game( maps.get("(2)CatalystLE"), [ # Human(Race.Terran), Bot(Race.Terran, CyclonePush()), Computer(Race.Zerg, Difficulty.Easy), ], realtime=False, sc2_version="4.7", ) if __name__ == "__main__": main() ================================================ FILE: examples/terran/mass_reaper.py ================================================ """ Bot that stays on 1base, goes 4 rax mass reaper This bot is one of the first examples that are micro intensive Bot has a chance to win against elite (=Difficulty.VeryHard) zerg AI Bot made by Burny """ from __future__ import annotations import random from typing import Literal from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId from sc2.main import run_game from sc2.player import Bot, Computer from sc2.position import Point2 from sc2.unit import Unit from sc2.units import Units class MassReaperBot(BotAI): def __init__(self): # Select distance calculation method 0, which is the pure python distance calculation without caching or indexing, using math.hypot(), for more info see bot_ai_internal.py _distances_override_functions() function self.distance_calculation_method = 3 async def on_step(self, iteration: int): # Benchmark and print duration time of the on_step method based on "self.distance_calculation_method" value # logger.info(self.time_formatted, self.supply_used, self.step_time[1]) """ - build depots when low on remaining supply - townhalls contains commandcenter and orbitalcommand - self.units(TYPE).not_ready.amount selects all units of that type, filters incomplete units, and then counts the amount - self.already_pending(TYPE) counts how many units are queued """ if ( self.supply_left < 5 and self.townhalls and self.supply_used >= 14 and self.can_afford(UnitTypeId.SUPPLYDEPOT) and self.already_pending(UnitTypeId.SUPPLYDEPOT) < 1 ): workers: Units = self.workers.gathering # If workers were found if workers: worker: Unit = workers.furthest_to(workers.center) location: Point2 | None = await self.find_placement( UnitTypeId.SUPPLYDEPOT, worker.position, placement_step=3 ) # If a placement location was found if location: # Order worker to build exactly on that location worker.build(UnitTypeId.SUPPLYDEPOT, location) # Lower all depots when finished for depot in self.structures(UnitTypeId.SUPPLYDEPOT).ready: depot(AbilityId.MORPH_SUPPLYDEPOT_LOWER) # Morph commandcenter to orbitalcommand # Check if tech requirement for orbital is complete (e.g. you need a barracks to be able to morph an orbital) orbital_tech_requirement: float = self.tech_requirement_progress(UnitTypeId.ORBITALCOMMAND) if orbital_tech_requirement == 1: # Loop over all idle command centers (CCs that are not building SCVs or morphing to orbital) for cc in self.townhalls(UnitTypeId.COMMANDCENTER).idle: # Check if we have 150 minerals; this used to be an issue when the API returned 550 (value) of the orbital, but we only wanted the 150 minerals morph cost if self.can_afford(UnitTypeId.ORBITALCOMMAND): cc(AbilityId.UPGRADETOORBITAL_ORBITALCOMMAND) # Expand if we can afford (400 minerals) and have less than 2 bases if ( 1 <= self.townhalls.amount < 2 and self.already_pending(UnitTypeId.COMMANDCENTER) == 0 and self.can_afford(UnitTypeId.COMMANDCENTER) ): # get_next_expansion returns the position of the next possible expansion location where you can place a command center location: Point2 | None = await self.get_next_expansion() if location: # Now we "select" (or choose) the nearest worker to that found location worker2: Unit | None = self.select_build_worker(location) if worker2 and self.can_afford(UnitTypeId.COMMANDCENTER): # The worker will be commanded to build the command center worker2.build(UnitTypeId.COMMANDCENTER, location) # Build up to 4 barracks if we can afford them # Check if we have a supply depot (tech requirement) before trying to make barracks barracks_tech_requirement: float = self.tech_requirement_progress(UnitTypeId.BARRACKS) if ( barracks_tech_requirement == 1 # self.structures.of_type( # [UnitTypeId.SUPPLYDEPOT, UnitTypeId.SUPPLYDEPOTLOWERED, UnitTypeId.SUPPLYDEPOTDROP] # ).ready and self.structures(UnitTypeId.BARRACKS).ready.amount + self.already_pending(UnitTypeId.BARRACKS) < 4 and self.can_afford(UnitTypeId.BARRACKS) ): workers: Units = self.workers.gathering if ( workers and self.townhalls ): # need to check if townhalls.amount > 0 because placement is based on townhall location worker: Unit = workers.furthest_to(workers.center) # I chose placement_step 4 here so there will be gaps between barracks hopefully location: Point2 | None = await self.find_placement( UnitTypeId.BARRACKS, self.townhalls.random.position, placement_step=4 ) if location: worker.build(UnitTypeId.BARRACKS, location) # Build refineries (on nearby vespene) when at least one barracks is in construction if ( self.structures(UnitTypeId.BARRACKS).ready.amount + self.already_pending(UnitTypeId.BARRACKS) > 0 and self.already_pending(UnitTypeId.REFINERY) < 1 ): # Loop over all townhalls that are 100% complete for th in self.townhalls.ready: # Find all vespene geysers that are closer than range 10 to this townhall vgs: Units = self.vespene_geyser.closer_than(10, th) for vg in vgs: if await self.can_place_single(UnitTypeId.REFINERY, vg.position) and self.can_afford( UnitTypeId.REFINERY ): workers: Units = self.workers.gathering if workers: # same condition as above worker: Unit = workers.closest_to(vg) # Caution: the target for the refinery has to be the vespene geyser, not its position! worker.build_gas(vg) # Make scvs until 22, usually you only need 1:1 mineral:gas ratio for reapers, but if you don't lose any then you will need additional depots (mule income should take care of that) # Stop scv production when barracks is complete but we still have a command center (priotize morphing to orbital command) if ( self.can_afford(UnitTypeId.SCV) and self.supply_left > 0 and self.supply_workers < 22 and ( self.structures(UnitTypeId.BARRACKS).ready.amount < 1 and self.townhalls(UnitTypeId.COMMANDCENTER).idle or self.townhalls(UnitTypeId.ORBITALCOMMAND).idle ) ): for th in self.townhalls.idle: th.train(UnitTypeId.SCV) # Make reapers if we can afford them and we have supply remaining if self.supply_left > 0: # Loop through all idle barracks for rax in self.structures(UnitTypeId.BARRACKS).idle: if self.can_afford(UnitTypeId.REAPER): rax.train(UnitTypeId.REAPER) # Send workers to mine from gas if iteration % 25 == 0: await self.my_distribute_workers() # Reaper micro enemies: Units = self.enemy_units | self.enemy_structures enemies_can_attack: Units = enemies.filter(lambda unit: unit.can_attack_ground) for reaper_unit in self.units(UnitTypeId.REAPER): # Move to range 15 of closest unit if reaper is below 20 hp and not regenerating enemy_threats_close: Units = enemies_can_attack.filter( lambda unit: unit.distance_to(reaper_unit) < 15 ) # Threats that can attack the reaper if reaper_unit.health_percentage < 2 / 5 and enemy_threats_close: retreat_points: set[Point2] = self.neighbors8(reaper_unit.position, distance=2) | self.neighbors8( reaper_unit.position, distance=4 ) # Filter points that are pathable retreat_points: set[Point2] = {x for x in retreat_points if self.in_pathing_grid(x)} if retreat_points: closest_enemy: Unit = enemy_threats_close.closest_to(reaper_unit) retreat_point: Point2 = closest_enemy.position.furthest(retreat_points) reaper_unit.move(retreat_point) continue # Continue for loop, dont execute any of the following # Reaper is ready to attack, shoot nearest ground unit enemy_ground_units: Units = enemies.filter( lambda unit: unit.distance_to(reaper_unit) < 5 and not unit.is_flying ) # Hardcoded attackrange of 5 if reaper_unit.weapon_cooldown == 0 and enemy_ground_units: enemy_ground_units: Units = enemy_ground_units.sorted(lambda x: x.distance_to(reaper_unit)) closest_enemy: Unit = enemy_ground_units[0] reaper_unit.attack(closest_enemy) continue # Continue for loop, dont execute any of the following # Attack is on cooldown, check if grenade is on cooldown, if not then throw it to furthest enemy in range 5 reaper_grenade_range: float = self.game_data.abilities[ AbilityId.KD8CHARGE_KD8CHARGE.value ]._proto.cast_range enemy_ground_units_in_grenade_range: Units = enemies_can_attack.filter( lambda unit: not unit.is_structure and not unit.is_flying and unit.type_id not in {UnitTypeId.LARVA, UnitTypeId.EGG} and unit.distance_to(reaper_unit) < reaper_grenade_range ) if enemy_ground_units_in_grenade_range and (reaper_unit.is_attacking or reaper_unit.is_moving): # If AbilityId.KD8CHARGE_KD8CHARGE in abilities, we check that to see if the reaper grenade is off cooldown abilities: list[AbilityId] = await self.get_available_abilities(reaper_unit) # pyrefly: ignore enemy_ground_units_in_grenade_range = enemy_ground_units_in_grenade_range.sorted( lambda x: x.distance_to(reaper_unit), reverse=True ) furthest_enemy: Unit | None = None for enemy in enemy_ground_units_in_grenade_range: if await self.can_cast( reaper_unit, AbilityId.KD8CHARGE_KD8CHARGE, enemy, cached_abilities_of_unit=abilities ): furthest_enemy = enemy break if furthest_enemy is not None: reaper_unit(AbilityId.KD8CHARGE_KD8CHARGE, furthest_enemy) continue # Continue for loop, don't execute any of the following # Move to max unit range if enemy is closer than 4 enemy_threats_very_close: Units = enemies.filter( lambda unit: unit.can_attack_ground and unit.distance_to(reaper_unit) < 4.5 ) # Hardcoded attackrange minus 0.5 # Threats that can attack the reaper if reaper_unit.weapon_cooldown != 0 and enemy_threats_very_close: retreat_points: set[Point2] = self.neighbors8(reaper_unit.position, distance=2) | self.neighbors8( reaper_unit.position, distance=4 ) # Filter points that are pathable by a reaper retreat_points: set[Point2] = {x for x in retreat_points if self.in_pathing_grid(x)} if retreat_points: closest_enemy: Unit = enemy_threats_very_close.closest_to(reaper_unit) retreat_point: Point2 = max( retreat_points, key=lambda x: x.distance_to(closest_enemy) - x.distance_to(reaper_unit) ) reaper_unit.move(retreat_point) continue # Continue for loop, don't execute any of the following # Move to nearest enemy ground unit/building because no enemy unit is closer than 5 all_enemy_ground_units: Units = self.enemy_units.not_flying if all_enemy_ground_units: closest_enemy: Unit = all_enemy_ground_units.closest_to(reaper_unit) reaper_unit.move(closest_enemy) continue # Continue for loop, don't execute any of the following # Move to random enemy start location if no enemy buildings have been seen reaper_unit.move(random.choice(self.enemy_start_locations)) # Manage idle scvs, would be taken care by distribute workers aswell if self.townhalls: for w in self.workers.idle: th: Unit = self.townhalls.closest_to(w) mfs: Units = self.mineral_field.closer_than(10, th) if mfs: mf: Unit = mfs.closest_to(w) w.gather(mf) # Manage orbital energy and drop mules for oc in self.townhalls(UnitTypeId.ORBITALCOMMAND).filter(lambda x: x.energy >= 50): mfs: Units = self.mineral_field.closer_than(10, oc) if mfs: mf: Unit = max(mfs, key=lambda x: x.mineral_contents) oc(AbilityId.CALLDOWNMULE_CALLDOWNMULE, mf) # When running out of mineral fields near command center, fly to next base with minerals # Helper functions # Stolen and modified from position.py @staticmethod def neighbors4(position: Point2, distance: float = 1) -> set[Point2]: p = position d = distance return {Point2((p.x - d, p.y)), Point2((p.x + d, p.y)), Point2((p.x, p.y - d)), Point2((p.x, p.y + d))} # Stolen and modified from position.py def neighbors8(self, position: Point2, distance: float = 1) -> set[Point2]: p = position d = distance return self.neighbors4(position, distance) | { Point2((p.x - d, p.y - d)), Point2((p.x - d, p.y + d)), Point2((p.x + d, p.y - d)), Point2((p.x + d, p.y + d)), } # Distribute workers function rewritten, the default distribute_workers() function did not saturate gas quickly enough async def my_distribute_workers(self, performance_heavy=True, only_saturate_gas=False): mineral_tags = [x.tag for x in self.mineral_field] gas_building_tags = [x.tag for x in self.gas_buildings] worker_pool = Units([], self) worker_pool_tags = set() # Find all gas_buildings that have surplus or deficit deficit_gas_buildings = {} surplus_gas_buildings = {} for g in self.gas_buildings.filter(lambda x: x.vespene_contents > 0): # Only loop over gas_buildings that have still gas in them deficit = g.ideal_harvesters - g.assigned_harvesters if deficit > 0: deficit_gas_buildings[g.tag] = {"unit": g, "deficit": deficit} elif deficit < 0: surplus_workers = self.workers.closer_than(10, g).filter( lambda w: w not in worker_pool_tags and len(w.orders) == 1 and w.orders[0].ability.id in [AbilityId.HARVEST_GATHER] and w.orders[0].target in gas_building_tags ) for _ in range(-deficit): if surplus_workers.amount > 0: w = surplus_workers.pop() worker_pool.append(w) worker_pool_tags.add(w.tag) surplus_gas_buildings[g.tag] = {"unit": g, "deficit": deficit} # Find all townhalls that have surplus or deficit deficit_townhalls: dict[int, dict[Literal["unit", "deficit"], Unit | int]] = {} surplus_townhalls: dict[int, dict[Literal["unit", "deficit"], Unit | int]] = {} if not only_saturate_gas: for th in self.townhalls: deficit = th.ideal_harvesters - th.assigned_harvesters if deficit > 0: deficit_townhalls[th.tag] = {"unit": th, "deficit": deficit} elif deficit < 0: surplus_workers = self.workers.closer_than(10, th).filter( lambda w: w.tag not in worker_pool_tags and len(w.orders) == 1 and w.orders[0].ability.id in [AbilityId.HARVEST_GATHER] and w.orders[0].target in mineral_tags ) # worker_pool.extend(surplus_workers) for _ in range(-deficit): if surplus_workers.amount > 0: w = surplus_workers.pop() worker_pool.append(w) worker_pool_tags.add(w.tag) surplus_townhalls[th.tag] = {"unit": th, "deficit": deficit} if all( [ len(deficit_gas_buildings) == 0, len(surplus_gas_buildings) == 0, len(surplus_townhalls) == 0 or deficit_townhalls == 0, ] ): # Cancel early if there is nothing to balance return # Check if deficit in gas less or equal than what we have in surplus, else grab some more workers from surplus bases # pyrefly: ignore deficit_gas_count = sum( # pyrefly: ignore gas_info["deficit"] for _gas_tag, gas_info in deficit_gas_buildings.items() # pyrefly: ignore if gas_info["deficit"] > 0 ) surplus_count = sum( # pyrefly: ignore -gas_info["deficit"] for _gas_tag, gas_info in surplus_gas_buildings.items() # pyrefly: ignore if gas_info["deficit"] < 0 ) surplus_count += sum( # pyrefly: ignore -townhall_info["deficit"] for _townhall_tag, townhall_info in surplus_townhalls.items() # pyrefly: ignore if townhall_info["deficit"] < 0 ) if deficit_gas_count - surplus_count > 0: # Grab workers near the gas who are mining minerals for _gas_tag, gas_info in deficit_gas_buildings.items(): if worker_pool.amount >= deficit_gas_count: break # pyrefly: ignore workers_near_gas = self.workers.closer_than(10, gas_info["unit"]).filter( lambda w: w.tag not in worker_pool_tags and len(w.orders) == 1 and w.orders[0].ability.id in [AbilityId.HARVEST_GATHER] and w.orders[0].target in mineral_tags ) while workers_near_gas.amount > 0 and worker_pool.amount < deficit_gas_count: w = workers_near_gas.pop() worker_pool.append(w) worker_pool_tags.add(w.tag) # Now we should have enough workers in the pool to saturate all gases, and if there are workers left over, make them mine at townhalls that have mineral workers deficit for _gas_tag, gas_info in deficit_gas_buildings.items(): if performance_heavy: # Sort furthest away to closest (as the pop() function will take the last element) worker_pool.sort(key=lambda x: x.distance_to(gas_info["unit"]), reverse=True) # pyrefly: ignore for _ in range(gas_info["deficit"]): if worker_pool.amount > 0: w = worker_pool.pop() if len(w.orders) == 1 and w.orders[0].ability.id in [AbilityId.HARVEST_RETURN]: # pyrefly: ignore w.gather(gas_info["unit"], queue=True) else: # pyrefly: ignore w.gather(gas_info["unit"]) if not only_saturate_gas: # If we now have left over workers, make them mine at bases with deficit in mineral workers for townhall_tag, townhall_info in deficit_townhalls.items(): if performance_heavy: # Sort furthest away to closest (as the pop() function will take the last element) worker_pool.sort(key=lambda x: x.distance_to(townhall_info["unit"]), reverse=True) # pyrefly: ignore for _ in range(townhall_info["deficit"]): if worker_pool.amount > 0: w = worker_pool.pop() mf = self.mineral_field.closer_than( 10, # pyrefly: ignore townhall_info["unit"], ).closest_to(w) if len(w.orders) == 1 and w.orders[0].ability.id in [AbilityId.HARVEST_RETURN]: w.gather(mf, queue=True) else: w.gather(mf) def main(): # Multiple difficulties for enemy bots available https://github.com/Blizzard/s2client-api/blob/ce2b3c5ac5d0c85ede96cef38ee7ee55714eeb2f/include/sc2api/sc2_gametypes.h#L30 run_game( maps.get("AcropolisLE"), [Bot(Race.Terran, MassReaperBot()), Computer(Race.Zerg, Difficulty.VeryHard)], realtime=False, ) if __name__ == "__main__": main() ================================================ FILE: examples/terran/onebase_battlecruiser.py ================================================ from __future__ import annotations from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId from sc2.main import run_game from sc2.player import Bot, Computer from sc2.position import Point2, Point3 from sc2.unit import Unit from sc2.units import Units class BCRushBot(BotAI): def select_target(self) -> tuple[Point2, bool]: """Select an enemy target the units should attack.""" targets: Units = self.enemy_structures if targets: return targets.random.position, True targets: Units = self.enemy_units if targets: return targets.random.position, True if self.units and min(u.position.distance_to(self.enemy_start_locations[0]) for u in self.units) < 5: return self.enemy_start_locations[0].position, False return self.mineral_field.random.position, False async def on_step(self, iteration: int): ccs: Units = self.townhalls # If we no longer have townhalls, attack with all workers if not ccs: target, target_is_enemy_unit = self.select_target() for unit in self.workers | self.units(UnitTypeId.BATTLECRUISER): if not unit.is_attacking: unit.attack(target) return cc: Unit = ccs.random # Send all BCs to attack a target. bcs: Units = self.units(UnitTypeId.BATTLECRUISER) if bcs: target, target_is_enemy_unit = self.select_target() bc: Unit for bc in bcs: # Order the BC to attack-move the target if target_is_enemy_unit and (bc.is_idle or bc.is_moving): bc.attack(target) # Order the BC to move to the target, and once the select_target returns an attack-target, change it to attack-move elif bc.is_idle: bc.move(target) # Build more SCVs until 22 if self.can_afford(UnitTypeId.SCV) and self.supply_workers < 22 and cc.is_idle: cc.train(UnitTypeId.SCV) # Build more BCs if self.structures(UnitTypeId.FUSIONCORE) and self.can_afford(UnitTypeId.BATTLECRUISER): for starport in self.structures(UnitTypeId.STARPORT).idle: if starport.has_add_on: if self.can_afford(UnitTypeId.BATTLECRUISER): starport.train(UnitTypeId.BATTLECRUISER) # Build more supply depots if self.supply_left < 6 and self.supply_used >= 14 and not self.already_pending(UnitTypeId.SUPPLYDEPOT): if self.can_afford(UnitTypeId.SUPPLYDEPOT): await self.build(UnitTypeId.SUPPLYDEPOT, near=cc.position.towards(self.game_info.map_center, 8)) # Build barracks if we have none if self.tech_requirement_progress(UnitTypeId.BARRACKS) == 1: if not self.structures(UnitTypeId.BARRACKS): if self.can_afford(UnitTypeId.BARRACKS): await self.build(UnitTypeId.BARRACKS, near=cc.position.towards(self.game_info.map_center, 8)) # Build refineries elif self.structures(UnitTypeId.BARRACKS) and self.gas_buildings.amount < 2: if self.can_afford(UnitTypeId.REFINERY): vgs: Units = self.vespene_geyser.closer_than(20, cc) for vg in vgs: has_gas_building = self.gas_buildings.filter(lambda unit: unit.distance_to(vg) < 1) if not has_gas_building: worker: Unit | None = self.select_build_worker(vg.position) if worker is not None: worker.build_gas(vg) # Build factory if we dont have one if self.tech_requirement_progress(UnitTypeId.FACTORY) == 1: factories: Units = self.structures(UnitTypeId.FACTORY) if not factories: if self.can_afford(UnitTypeId.FACTORY): await self.build(UnitTypeId.FACTORY, near=cc.position.towards(self.game_info.map_center, 8)) # Build starport once we can build starports, up to 2 elif ( factories.ready and self.structures.of_type({UnitTypeId.STARPORT, UnitTypeId.STARPORTFLYING}).ready.amount + self.already_pending(UnitTypeId.STARPORT) < 2 ): if self.can_afford(UnitTypeId.STARPORT): await self.build( UnitTypeId.STARPORT, near=cc.position.towards(self.game_info.map_center, 15).random_on_distance(8), ) def starport_points_to_build_addon(sp_position: Point2) -> list[Point2]: """Return all points that need to be checked when trying to build an addon. Returns 4 points.""" addon_offset: Point2 = Point2((2.5, -0.5)) addon_position: Point2 = sp_position + addon_offset addon_points = [ (addon_position + Point2((x - 0.5, y - 0.5))).rounded for x in range(0, 2) for y in range(0, 2) ] return addon_points # Build starport techlab or lift if no room to build techlab starport: Unit for starport in self.structures(UnitTypeId.STARPORT).ready.idle: if not starport.has_add_on and self.can_afford(UnitTypeId.STARPORTTECHLAB): addon_points = starport_points_to_build_addon(starport.position) if all( self.in_map_bounds(addon_point) and self.in_placement_grid(addon_point) and self.in_pathing_grid(addon_point) for addon_point in addon_points ): starport.build(UnitTypeId.STARPORTTECHLAB) else: starport(AbilityId.LIFT) def starport_land_positions(sp_position: Point2) -> list[Point2]: """Return all points that need to be checked when trying to land at a location where there is enough space to build an addon. Returns 13 points.""" land_positions = [(sp_position + Point2((x, y))).rounded for x in range(-1, 2) for y in range(-1, 2)] return land_positions + starport_points_to_build_addon(sp_position) def try_land_starports(): # Find a position to land for a flying starport so that it can build an addon for starport in self.structures(UnitTypeId.STARPORTFLYING).idle: possible_land_positions_offset = sorted( (Point2((x, y)) for x in range(-10, 10) for y in range(-10, 10)), key=lambda point: point.x**2 + point.y**2, ) offset_point: Point2 = Point2((-0.5, -0.5)) possible_land_positions = ( starport.position.rounded + offset_point + p for p in possible_land_positions_offset ) for target_land_position in possible_land_positions: land_and_addon_points: list[Point2] = starport_land_positions(target_land_position) if all( self.in_map_bounds(land_pos) and self.in_placement_grid(land_pos) and self.in_pathing_grid(land_pos) for land_pos in land_and_addon_points ): starport(AbilityId.LAND, target_land_position) return try_land_starports() # Show where it is flying to and show grid for starport in self.structures(UnitTypeId.STARPORTFLYING).filter(lambda unit: not unit.is_idle): if isinstance(starport.order_target, Point2): p: Point3 = Point3((*starport.order_target, self.get_terrain_z_height(starport.order_target))) self.client.debug_box2_out(p, color=Point3((255, 0, 0))) # Build fusion core if self.structures(UnitTypeId.STARPORT).ready: if self.can_afford(UnitTypeId.FUSIONCORE) and not self.structures(UnitTypeId.FUSIONCORE): await self.build(UnitTypeId.FUSIONCORE, near=cc.position.towards(self.game_info.map_center, 8)) # Saturate refineries for refinery in self.gas_buildings: if refinery.assigned_harvesters < refinery.ideal_harvesters: workers: Units = self.workers.closer_than(10, refinery) if workers: workers.random.gather(refinery) # Send workers back to mine if they are idle for scv in self.workers.idle: scv.gather(self.mineral_field.closest_to(cc)) def main(): run_game( maps.get("(2)CatalystLE"), [ # Human(Race.Terran), Bot(Race.Terran, BCRushBot()), Computer(Race.Zerg, Difficulty.Hard), ], realtime=False, ) if __name__ == "__main__": main() ================================================ FILE: examples/terran/proxy_rax.py ================================================ from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race from sc2.ids.unit_typeid import UnitTypeId from sc2.main import run_game from sc2.player import Bot, Computer from sc2.position import Point2 from sc2.unit import Unit from sc2.units import Units class ProxyRaxBot(BotAI): async def on_start(self): self.client.game_step = 2 async def on_step(self, iteration: int): # If we don't have a townhall anymore, send all units to attack ccs: Units = self.townhalls(UnitTypeId.COMMANDCENTER) if not ccs: target: Point2 = self.enemy_structures.random_or(self.enemy_start_locations[0]).position for unit in self.workers | self.units(UnitTypeId.MARINE): unit.attack(target) return cc: Unit = ccs.first # Send marines in waves of 15, each time 15 are idle, send them to their death marines: Units = self.units(UnitTypeId.MARINE).idle if marines.amount > 15: target: Point2 = self.enemy_structures.random_or(self.enemy_start_locations[0]).position for marine in marines: marine.attack(target) # Train more SCVs if self.can_afford(UnitTypeId.SCV) and self.supply_workers < 16 and cc.is_idle: cc.train(UnitTypeId.SCV) # Build more depots elif ( self.supply_left < (2 if self.structures(UnitTypeId.BARRACKS).amount < 3 else 4) and self.supply_used >= 14 ): if self.can_afford(UnitTypeId.SUPPLYDEPOT) and self.already_pending(UnitTypeId.SUPPLYDEPOT) < 2: await self.build(UnitTypeId.SUPPLYDEPOT, near=cc.position.towards(self.game_info.map_center, 5)) # Build proxy barracks elif self.structures(UnitTypeId.BARRACKS).amount < 3 or ( self.minerals > 400 and self.structures(UnitTypeId.BARRACKS).amount < 5 ): if self.can_afford(UnitTypeId.BARRACKS): p: Point2 = self.game_info.map_center.towards(self.enemy_start_locations[0], 25) await self.build(UnitTypeId.BARRACKS, near=p) # Train marines for rax in self.structures(UnitTypeId.BARRACKS).ready.idle: if self.can_afford(UnitTypeId.MARINE): rax.train(UnitTypeId.MARINE) # Send idle workers to gather minerals near command center for scv in self.workers.idle: scv.gather(self.mineral_field.closest_to(cc)) def main(): run_game( maps.get("(2)CatalystLE"), [Bot(Race.Terran, ProxyRaxBot()), Computer(Race.Zerg, Difficulty.Hard)], realtime=False, ) if __name__ == "__main__": main() ================================================ FILE: examples/terran/ramp_wall.py ================================================ import random import numpy as np from loguru import logger from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId from sc2.main import run_game from sc2.player import Bot, Computer from sc2.position import Point2, Point3 from sc2.unit import Unit from sc2.units import Units class RampWallBot(BotAI): def __init__(self): self.unit_command_uses_self_do = False async def on_step(self, iteration: int): ccs: Units = self.townhalls(UnitTypeId.COMMANDCENTER) if not ccs: return cc: Unit = ccs.first await self.distribute_workers() if self.can_afford(UnitTypeId.SCV) and self.workers.amount < 16 and cc.is_idle: cc.train(UnitTypeId.SCV) def raise_and_lower_depots(): # Raise depos when enemies are nearby for depo in self.structures(UnitTypeId.SUPPLYDEPOT).ready: for unit in self.enemy_units: if unit.distance_to(depo) < 15: return else: depo(AbilityId.MORPH_SUPPLYDEPOT_LOWER) # Lower depos when no enemies are nearby for depo in self.structures(UnitTypeId.SUPPLYDEPOTLOWERED).ready: for unit in self.enemy_units: if unit.distance_to(depo) < 10: depo(AbilityId.MORPH_SUPPLYDEPOT_RAISE) return raise_and_lower_depots() # Draw ramp points self.draw_ramp_points() # Draw all detected expansions on the map self.draw_expansions() # # Draw pathing grid self.draw_pathing_grid() # Draw placement grid self.draw_placement_grid() # Draw vision blockers self.draw_vision_blockers() # Draw visibility pixelmap for debugging purposes self.draw_visibility_pixelmap() # Draw some example boxes around units, lines towards command center, text on the screen and barracks self.draw_example() # Draw if two selected units are facing each other - green if this guy is facing the other, red if he is not self.draw_facing_units() depot_placement_positions: set[Point2] = self.main_base_ramp.corner_depots # Uncomment the following if you want to build 3 supply depots in the wall instead of a barracks in the middle + 2 depots in the corner # depot_placement_positions = self.main_base_ramp.corner_depots | {self.main_base_ramp.depot_in_middle} barracks_placement_position: Point2 | None = self.main_base_ramp.barracks_correct_placement # If you prefer to have the barracks in the middle without room for addons, use the following instead # barracks_placement_position = self.main_base_ramp.barracks_in_middle depots: Units = self.structures.of_type({UnitTypeId.SUPPLYDEPOT, UnitTypeId.SUPPLYDEPOTLOWERED}) # Filter locations close to finished supply depots if depots: depot_placement_positions: set[Point2] = { d for d in depot_placement_positions if depots.closest_distance_to(d) > 1 } # Build depots if self.can_afford(UnitTypeId.SUPPLYDEPOT) and self.already_pending(UnitTypeId.SUPPLYDEPOT) == 0: if len(depot_placement_positions) == 0: return # Choose any depot location target_depot_location: Point2 = depot_placement_positions.pop() workers: Units = self.workers.gathering if workers: # if workers were found worker: Unit = workers.random worker.build(UnitTypeId.SUPPLYDEPOT, target_depot_location) # Build barracks if depots.ready and self.can_afford(UnitTypeId.BARRACKS) and self.already_pending(UnitTypeId.BARRACKS) == 0: if self.structures(UnitTypeId.BARRACKS).amount + self.already_pending(UnitTypeId.BARRACKS) > 0: return workers = self.workers.gathering if workers and barracks_placement_position: # if workers were found worker: Unit = workers.random worker.build(UnitTypeId.BARRACKS, barracks_placement_position) async def on_building_construction_started(self, unit: Unit): logger.info(f"Construction of building {unit} started at {unit.position}.") async def on_building_construction_complete(self, unit: Unit): logger.info(f"Construction of building {unit} completed at {unit.position}.") def draw_ramp_points(self): for ramp in self.game_info.map_ramps: for p in ramp.points: h2 = self.get_terrain_z_height(p) pos = Point3((p.x, p.y, h2)) color = Point3((255, 0, 0)) if p in ramp.upper: color = Point3((0, 255, 0)) if p in ramp.upper2_for_ramp_wall: color = Point3((0, 255, 255)) if p in ramp.lower: color = Point3((0, 0, 255)) self.client.debug_box2_out(pos + Point2((0.5, 0.5)), half_vertex_length=0.25, color=color) # Identical to above: # p0 = Point3((pos.x + 0.25, pos.y + 0.25, pos.z + 0.25)) # p1 = Point3((pos.x + 0.75, pos.y + 0.75, pos.z - 0.25)) # logger.info(f"Drawing {p0} to {p1}") # self.client.debug_box_out(p0, p1, color=color) def draw_expansions(self): green = Point3((0, 255, 0)) for expansion_pos in self.expansion_locations_list: height = self.get_terrain_z_height(expansion_pos) expansion_pos3 = Point3((*expansion_pos, height)) self.client.debug_box2_out(expansion_pos3, half_vertex_length=2.5, color=green) def draw_pathing_grid(self): map_area = self.game_info.playable_area for (b, a), value in np.ndenumerate(self.game_info.pathing_grid.data_numpy): if value == 0: continue # Skip values outside of playable map area if not map_area.x <= a < map_area.x + map_area.width: continue if not map_area.y <= b < map_area.y + map_area.height: continue p = Point2((a, b)) h2 = self.get_terrain_z_height(p) pos = Point3((p.x, p.y, h2)) p0 = Point3((pos.x - 0.25, pos.y - 0.25, pos.z + 0.25)) + Point2((0.5, 0.5)) p1 = Point3((pos.x + 0.25, pos.y + 0.25, pos.z - 0.25)) + Point2((0.5, 0.5)) # logger.info(f"Drawing {p0} to {p1}") color = Point3((0, 255, 0)) self.client.debug_box_out(p0, p1, color=color) def draw_placement_grid(self): map_area = self.game_info.playable_area for (b, a), value in np.ndenumerate(self.game_info.placement_grid.data_numpy): if value == 0: continue # Skip values outside of playable map area if not map_area.x <= a < map_area.x + map_area.width: continue if not map_area.y <= b < map_area.y + map_area.height: continue p = Point2((a, b)) h2 = self.get_terrain_z_height(p) pos = Point3((p.x, p.y, h2)) p0 = Point3((pos.x - 0.25, pos.y - 0.25, pos.z + 0.25)) + Point2((0.5, 0.5)) p1 = Point3((pos.x + 0.25, pos.y + 0.25, pos.z - 0.25)) + Point2((0.5, 0.5)) # logger.info(f"Drawing {p0} to {p1}") color = Point3((0, 255, 0)) self.client.debug_box_out(p0, p1, color=color) def draw_vision_blockers(self): for p in self.game_info.vision_blockers: h2 = self.get_terrain_z_height(p) pos = Point3((p.x, p.y, h2)) p0 = Point3((pos.x - 0.25, pos.y - 0.25, pos.z + 0.25)) + Point2((0.5, 0.5)) p1 = Point3((pos.x + 0.25, pos.y + 0.25, pos.z - 0.25)) + Point2((0.5, 0.5)) # logger.info(f"Drawing {p0} to {p1}") color = Point3((255, 0, 0)) self.client.debug_box_out(p0, p1, color=color) def draw_visibility_pixelmap(self): for (y, x), value in np.ndenumerate(self.state.visibility.data_numpy): p = Point2((x, y)) h2 = self.get_terrain_z_height(p) pos = Point3((p.x, p.y, h2)) p0 = Point3((pos.x - 0.25, pos.y - 0.25, pos.z + 0.25)) + Point2((0.5, 0.5)) p1 = Point3((pos.x + 0.25, pos.y + 0.25, pos.z - 0.25)) + Point2((0.5, 0.5)) # Red color = Point3((255, 0, 0)) # If value == 2: show green (= we have vision on that point) if value == 2: color = Point3((0, 255, 0)) self.client.debug_box_out(p0, p1, color=color) def draw_example(self): # Draw green boxes around SCVs if they are gathering, yellow if they are returning cargo, red the rest scv: Unit for scv in self.workers: pos = scv.position3d p0 = Point3((pos.x - 0.25, pos.y - 0.25, pos.z + 0.25)) p1 = Point3((pos.x + 0.25, pos.y + 0.25, pos.z - 0.25)) # Red color = Point3((255, 0, 0)) if scv.is_gathering: color = Point3((0, 255, 0)) elif scv.is_returning: color = Point3((255, 255, 0)) self.client.debug_box_out(p0, p1, color=color) # Draw lines from structures to command center if self.townhalls: cc = self.townhalls[0] p0 = cc.position3d if not self.structures: return structure: Unit for structure in self.structures: if structure == cc: continue p1 = structure.position3d # Red color = Point3((255, 0, 0)) self.client.debug_line_out(p0, p1, color=color) # Draw text on barracks if structure.type_id == UnitTypeId.BARRACKS: # Blue color = Point3((0, 0, 255)) pos = structure.position3d + Point3((0, 0, 0.5)) # TODO: Why is this text flickering self.client.debug_text_world(text="MY RAX", pos=pos, color=color, size=16) # Draw text in top left of screen self.client.debug_text_screen(text="Hello world!", pos=Point2((0, 0)), color=None, size=16) self.client.debug_text_simple(text="Hello world2!") def draw_facing_units(self): """Draws green box on top of selected_unit2, if selected_unit2 is facing selected_unit1""" selected_unit1: Unit selected_unit2: Unit red = Point3((255, 0, 0)) green = Point3((0, 255, 0)) for selected_unit1 in (self.units | self.structures).selected: for selected_unit2 in self.units.selected: if selected_unit1 == selected_unit2: continue if selected_unit2.is_facing(selected_unit1): self.client.debug_box2_out(selected_unit2, half_vertex_length=0.25, color=green) else: self.client.debug_box2_out(selected_unit2, half_vertex_length=0.25, color=red) def main(): _map = random.choice( [ # Most maps have 2 upper points at the ramp (len(self.main_base_ramp.upper) == 2) "AutomatonLE", "BlueshiftLE", "CeruleanFallLE", "KairosJunctionLE", "ParaSiteLE", "PortAleksanderLE", "StasisLE", "DarknessSanctuaryLE", "ParaSiteLE", # Has 5 upper points at the main ramp "AcolyteLE", # Has 4 upper points at the ramp to the in-base natural and 2 upper points at the small ramp "HonorgroundsLE", # Has 4 or 9 upper points at the large main base ramp ] ) _map = "PillarsofGoldLE" run_game( maps.get(_map), [Bot(Race.Terran, RampWallBot()), Computer(Race.Zerg, Difficulty.Hard)], realtime=True, # sc2_version="4.10.1", ) if __name__ == "__main__": main() ================================================ FILE: examples/too_slow_bot.py ================================================ import asyncio import random from examples.terran.proxy_rax import ProxyRaxBot from sc2 import maps from sc2.data import Difficulty, Race from sc2.main import run_game from sc2.player import Bot, Computer class SlowBot(ProxyRaxBot): async def on_step(self, iteration: int): await asyncio.sleep(random.random()) await super().on_step(iteration) def main(): run_game( maps.get("Abyssal Reef LE"), [Bot(Race.Terran, SlowBot()), Computer(Race.Protoss, Difficulty.Medium)], realtime=False, ) if __name__ == "__main__": main() ================================================ FILE: examples/watch_replay.py ================================================ import platform from pathlib import Path from loguru import logger from sc2.main import run_replay from sc2.observer_ai import ObserverAI class ObserverBot(ObserverAI): """ A replay bot that can run replays. Check sc2/observer_ai.py for more available functions """ async def on_start(self): print("Replay on_start() was called") async def on_step(self, iteration: int): print(f"Replay iteration: {iteration}") if __name__ == "__main__": my_observer_ai = ObserverBot() # pyrefly: ignore # Enter replay name here # The replay should be either in this folder and you can give it a relative path, or change it to the absolute path replay_name = "WorkerRush.SC2Replay" if platform.system() == "Linux": home_replay_folder = Path.home() / "Documents" / "StarCraft II" / "Replays" replay_path = home_replay_folder / replay_name if not replay_path.is_file(): logger.warning(f"You are on linux, please put the replay in directory {home_replay_folder}") raise FileNotFoundError elif Path(replay_name).is_absolute(): replay_path = Path(replay_name) else: # Convert relative path to absolute path, assuming this replay is in this folder folder_path = Path(__file__).parent replay_path = folder_path / replay_name assert replay_path.is_file(), ( "Run worker_rush.py in the same folder first to generate a replay. Then run watch_replay.py again." ) run_replay(my_observer_ai, replay_path=str(replay_path)) ================================================ FILE: examples/worker_rush.py ================================================ from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race from sc2.main import run_game from sc2.player import Bot, Computer class WorkerRushBot(BotAI): async def on_step(self, iteration: int): if iteration == 0: for worker in self.workers: worker.attack(self.enemy_start_locations[0]) def main(): run_game( maps.get("Abyssal Reef LE"), [Bot(Race.Zerg, WorkerRushBot()), Computer(Race.Protoss, Difficulty.Medium)], realtime=False, save_replay_as="WorkerRush.SC2Replay", ) if __name__ == "__main__": main() ================================================ FILE: examples/worker_stack_bot.py ================================================ """ This bot attempts to stack workers 'perfectly'. This is only a demo that works on game start, but does not work when adding more workers or bases. This bot exists only to showcase how to keep track of mineral tag over multiple steps / frames. Task for the user who wants to enhance this bot: - Allow mining from vespene geysirs - Remove dead workers and re-assign (new) workers to that mineral patch, or pick a worker from a long distance mineral patch - Re-assign workers when new base is completed (or near complete) - Re-assign workers when base died - Re-assign workers when mineral patch mines out - Re-assign workers when gas mines out """ from __future__ import annotations from loguru import logger from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race from sc2.main import run_game from sc2.player import Bot, Computer from sc2.position import Point2 from sc2.unit import Unit from sc2.units import Units class WorkerStackBot(BotAI): def __init__(self): self.worker_to_mineral_patch_dict: dict[int, int] = {} self.mineral_patch_to_list_of_workers: dict[int, set[int]] = {} self.minerals_sorted_by_distance: Units = Units([], self) # Distance 0.01 to 0.1 seems fine self.townhall_distance_threshold = 0.01 # Distance factor between 0.95 and 1.0 seems fine self.townhall_distance_factor = 1 async def on_start(self): self.client.game_step = 1 await self.assign_workers() async def assign_workers(self): self.minerals_sorted_by_distance = self.mineral_field.closer_than( 10, self.start_location ).sorted_by_distance_to(self.start_location) # Assign workers to mineral patch, start with the mineral patch closest to base for mineral in self.minerals_sorted_by_distance: # Assign workers closest to the mineral patch workers = self.workers.tags_not_in(self.worker_to_mineral_patch_dict).sorted_by_distance_to(mineral) for worker in workers: # Assign at most 2 workers per patch # This dict is not really used further down the code, but useful to keep track of how many workers are assigned to this mineral patch - important for when the mineral patch mines out or a worker dies if len(self.mineral_patch_to_list_of_workers.get(mineral.tag, [])) < 2: if len(self.mineral_patch_to_list_of_workers.get(mineral.tag, [])) == 0: self.mineral_patch_to_list_of_workers[mineral.tag] = {worker.tag} else: self.mineral_patch_to_list_of_workers[mineral.tag].add(worker.tag) # Keep track of which mineral patch the worker is assigned to - if the mineral patch mines out, reassign the worker to another patch self.worker_to_mineral_patch_dict[worker.tag] = mineral.tag else: break async def on_step(self, iteration: int): if self.worker_to_mineral_patch_dict: # Quick-access cache mineral tag to mineral Unit minerals: dict[int, Unit] = {mineral.tag: mineral for mineral in self.mineral_field} worker: Unit for worker in self.workers: if not self.townhalls: logger.error("All townhalls died - can't return resources") break mineral_tag = self.worker_to_mineral_patch_dict[worker.tag] mineral = minerals.get(mineral_tag) if mineral is None: logger.error(f"Mined out mineral with tag {mineral_tag} for worker {worker.tag}") continue # Order worker to mine at target mineral patch if isn't carrying minerals if not worker.is_carrying_minerals: if not worker.is_gathering or worker.order_target != mineral.tag: worker.gather(mineral) # Order worker to return minerals if carrying minerals else: th = self.townhalls.closest_to(worker) # Move worker in front of the nexus to avoid deceleration until the last moment if worker.distance_to(th) > th.radius + worker.radius + self.townhall_distance_threshold: pos: Point2 = th.position worker.move(pos.towards(worker, th.radius * self.townhall_distance_factor)) worker.return_resource(queue=True) else: worker.return_resource() worker.gather(mineral, queue=True) # Print info every 30 game-seconds if self.state.game_loop % (22.4 * 30) == 0: logger.info(f"{self.time_formatted} Mined a total of {int(self.state.score.collected_minerals)} minerals") def main(): run_game( maps.get("AcropolisLE"), [Bot(Race.Protoss, WorkerStackBot()), Computer(Race.Terran, Difficulty.Medium)], realtime=False, random_seed=0, ) if __name__ == "__main__": main() ================================================ FILE: examples/zerg/__init__.py ================================================ from pathlib import Path __all__ = [p.stem for p in Path().iterdir() if p.is_file() and p.suffix == ".py" and p.stem != "__init__"] ================================================ FILE: examples/zerg/banes_banes_banes.py ================================================ import random from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId from sc2.main import run_game from sc2.player import Bot, Computer from sc2.position import Point2 from sc2.unit import Unit from sc2.units import Units class BanesBanesBanes(BotAI): """ A dumb bot that a-moves banes. Use to check if bane morphs are working correctly """ def select_target(self) -> Point2: if self.enemy_structures: return random.choice(self.enemy_structures).position return self.enemy_start_locations[0] async def on_step(self, iteration: int): larvae: Units = self.larva lings: Units = self.units(UnitTypeId.ZERGLING) # Send all idle banes to enemy if banes := [u for u in self.units if u.type_id == UnitTypeId.BANELING and u.is_idle]: for unit in banes: unit.attack(self.select_target()) # If supply is low, train overlords if ( self.supply_left < 2 and larvae and self.can_afford(UnitTypeId.OVERLORD) and not self.already_pending(UnitTypeId.OVERLORD) ): larvae.random.train(UnitTypeId.OVERLORD) return # If bane nest is ready, train banes if lings and self.can_afford(UnitTypeId.BANELING) and self.structures(UnitTypeId.BANELINGNEST).ready: # TODO: Get lings.random.train(UnitTypeId.BANELING) to work # Broken on recent patches # lings.random.train(UnitTypeId.BANELING) # This way is working lings.random(AbilityId.MORPHTOBANELING_BANELING) return # If all our townhalls are dead, send all our units to attack if not self.townhalls: for unit in self.units.of_type({UnitTypeId.DRONE, UnitTypeId.QUEEN, UnitTypeId.ZERGLING}): unit.attack(self.enemy_start_locations[0]) return hq: Unit = self.townhalls.first # Send idle queens with >=25 energy to inject for queen in self.units(UnitTypeId.QUEEN).idle: # The following checks if the inject ability is in the queen abilitys - basically it checks if we have enough energy and if the ability is off-cooldown # abilities = await self.get_available_abilities(queen) # if AbilityId.EFFECT_INJECTLARVA in abilities: if queen.energy >= 25: queen(AbilityId.EFFECT_INJECTLARVA, hq) # Build spawning pool if self.structures(UnitTypeId.SPAWNINGPOOL).amount + self.already_pending(UnitTypeId.SPAWNINGPOOL) == 0: if self.can_afford(UnitTypeId.SPAWNINGPOOL): await self.build( UnitTypeId.SPAWNINGPOOL, near=hq.position.towards(self.game_info.map_center, 5), ) # Upgrade to lair if spawning pool is complete # if self.structures(UnitTypeId.SPAWNINGPOOL).ready: # if hq.is_idle and not self.townhalls(UnitTypeId.LAIR): # if self.can_afford(UnitTypeId.LAIR): # hq.build(UnitTypeId.LAIR) # If lair is ready and we have no hydra den on the way: build hydra den if self.structures(UnitTypeId.SPAWNINGPOOL).ready and self.can_afford(UnitTypeId.BANELINGNEST): if self.structures(UnitTypeId.BANELINGNEST).amount + self.already_pending(UnitTypeId.BANELINGNEST) == 0: await self.build( UnitTypeId.BANELINGNEST, near=hq.position.towards(self.game_info.map_center, 5), ) # If we dont have both extractors: build them if ( self.structures(UnitTypeId.SPAWNINGPOOL) and self.gas_buildings.amount + self.already_pending(UnitTypeId.EXTRACTOR) < 2 and self.can_afford(UnitTypeId.EXTRACTOR) ): # May crash if we dont have any drones for vg in self.vespene_geyser.closer_than(10, hq): drone: Unit = self.workers.random drone.build_gas(vg) return # If we have less than 22 drones, build drones if self.supply_workers + self.already_pending(UnitTypeId.DRONE) < 22: if larvae and self.can_afford(UnitTypeId.DRONE): larva: Unit = larvae.random larva.train(UnitTypeId.DRONE) return # Saturate gas for a in self.gas_buildings: if a.assigned_harvesters < a.ideal_harvesters: w: Units = self.workers.closer_than(10, a) if w: w.random.gather(a) # Build queen once the pool is done if self.structures(UnitTypeId.SPAWNINGPOOL).ready: if not self.units(UnitTypeId.QUEEN) and hq.is_idle: if self.can_afford(UnitTypeId.QUEEN): hq.train(UnitTypeId.QUEEN) # Train zerglings if larvae and self.can_afford(UnitTypeId.ZERGLING): larvae.random.train(UnitTypeId.ZERGLING) def main(): run_game( maps.get("GoldenAura513AIE"), [Bot(Race.Zerg, BanesBanesBanes()), Computer(Race.Terran, Difficulty.Medium)], realtime=False, save_replay_as="ZvT.SC2Replay", ) if __name__ == "__main__": main() ================================================ FILE: examples/zerg/expand_everywhere.py ================================================ import random from contextlib import suppress from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race from sc2.ids.unit_typeid import UnitTypeId from sc2.main import run_game from sc2.player import Bot, Computer from sc2.position import Point2 from sc2.unit import Unit class ExpandEverywhere(BotAI): async def on_start(self): self.client.game_step = 50 await self.client.debug_show_map() async def on_step(self, iteration: int): # Build overlords if about to be supply blocked if ( self.supply_left < 2 and self.supply_cap < 200 and self.already_pending(UnitTypeId.OVERLORD) < 2 and self.can_afford(UnitTypeId.OVERLORD) ): self.train(UnitTypeId.OVERLORD) # While we have less than 16 drones, make more drones if ( self.can_afford(UnitTypeId.DRONE) and self.supply_workers - self.worker_en_route_to_build(UnitTypeId.HATCHERY) < (self.townhalls.amount + self.placeholders(UnitTypeId.HATCHERY).amount) * 16 ): self.train(UnitTypeId.DRONE) # Send workers across bases await self.distribute_workers() # Expand if we have 300 minerals, try to expand if there is one more expansion location available with suppress(AssertionError): if self.can_afford(UnitTypeId.HATCHERY): planned_hatch_locations: set[Point2] = {placeholder.position for placeholder in self.placeholders} my_structure_locations: set[Point2] = {structure.position for structure in self.structures} enemy_structure_locations: set[Point2] = {structure.position for structure in self.enemy_structures} blocked_locations: set[Point2] = ( my_structure_locations | planned_hatch_locations | enemy_structure_locations ) shuffled_expansions = self.expansion_locations_list.copy() random.shuffle(shuffled_expansions) for exp_pos in shuffled_expansions: if exp_pos in blocked_locations: continue for drone in self.workers.collecting: drone: Unit drone.build(UnitTypeId.HATCHERY, exp_pos) assert False, "Break out of 2 for loops" # Kill all enemy units in vision / sight if self.enemy_units: await self.client.debug_kill_unit(self.enemy_units) async def on_building_construction_complete(self, unit: Unit): """Set rally point of new hatcheries.""" if unit.type_id == UnitTypeId.HATCHERY and self.mineral_field: mf = self.mineral_field.closest_to(unit) unit.smart(mf) def main(): run_game( maps.get("AcropolisLE"), [Bot(Race.Zerg, ExpandEverywhere()), Computer(Race.Terran, Difficulty.Medium)], realtime=False, save_replay_as="ZvT.SC2Replay", ) if __name__ == "__main__": main() ================================================ FILE: examples/zerg/hydralisk_push.py ================================================ import random from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId from sc2.ids.upgrade_id import UpgradeId from sc2.main import run_game from sc2.player import Bot, Computer from sc2.position import Point2 from sc2.unit import Unit from sc2.units import Units class Hydralisk(BotAI): def select_target(self) -> Point2: if self.enemy_structures: return random.choice(self.enemy_structures).position return self.enemy_start_locations[0] async def on_step(self, iteration: int): larvae: Units = self.larva forces: Units = self.units.of_type({UnitTypeId.ZERGLING, UnitTypeId.HYDRALISK}) # Send all idle lings + hydras to attack-move if we have at least 10 hydras, every 400th frame if self.units(UnitTypeId.HYDRALISK).amount >= 10 and iteration % 50 == 0: for unit in forces.idle: unit.attack(self.select_target()) # If supply is low, train overlords if self.supply_left < 2 and larvae and self.can_afford(UnitTypeId.OVERLORD): larvae.random.train(UnitTypeId.OVERLORD) return # If hydra den is ready and idle, research upgrades hydra_dens = self.structures(UnitTypeId.HYDRALISKDEN) if hydra_dens: for hydra_den in hydra_dens.ready.idle: if self.already_pending_upgrade(UpgradeId.EVOLVEGROOVEDSPINES) == 0 and self.can_afford( UpgradeId.EVOLVEGROOVEDSPINES ): hydra_den.research(UpgradeId.EVOLVEGROOVEDSPINES) elif self.already_pending_upgrade(UpgradeId.EVOLVEMUSCULARAUGMENTS) == 0 and self.can_afford( UpgradeId.EVOLVEMUSCULARAUGMENTS ): hydra_den.research(UpgradeId.EVOLVEMUSCULARAUGMENTS) # If hydra den is ready, train hydra if larvae and self.can_afford(UnitTypeId.HYDRALISK) and self.structures(UnitTypeId.HYDRALISKDEN).ready: larvae.random.train(UnitTypeId.HYDRALISK) return # If all our townhalls are dead, send all our units to attack if not self.townhalls: for unit in self.units.of_type( {UnitTypeId.DRONE, UnitTypeId.QUEEN, UnitTypeId.ZERGLING, UnitTypeId.HYDRALISK} ): unit.attack(self.enemy_start_locations[0]) return hq: Unit = self.townhalls.first # Send idle queens with >=25 energy to inject for queen in self.units(UnitTypeId.QUEEN).idle: # The following checks if the inject ability is in the queen abilitys - basically it checks if we have enough energy and if the ability is off-cooldown # abilities = await self.get_available_abilities(queen) # if AbilityId.EFFECT_INJECTLARVA in abilities: if queen.energy >= 25: queen(AbilityId.EFFECT_INJECTLARVA, hq) # Build spawning pool if self.structures(UnitTypeId.SPAWNINGPOOL).amount + self.already_pending(UnitTypeId.SPAWNINGPOOL) == 0: if self.can_afford(UnitTypeId.SPAWNINGPOOL): await self.build(UnitTypeId.SPAWNINGPOOL, near=hq.position.towards(self.game_info.map_center, 5)) # Upgrade to lair if spawning pool is complete if self.structures(UnitTypeId.SPAWNINGPOOL).ready: if hq.is_idle and not self.townhalls(UnitTypeId.LAIR): if self.can_afford(UnitTypeId.LAIR): hq.build(UnitTypeId.LAIR) # If lair is ready and we have no hydra den on the way: build hydra den if self.townhalls(UnitTypeId.LAIR).ready: if self.structures(UnitTypeId.HYDRALISKDEN).amount + self.already_pending(UnitTypeId.HYDRALISKDEN) == 0: if self.can_afford(UnitTypeId.HYDRALISKDEN): await self.build(UnitTypeId.HYDRALISKDEN, near=hq.position.towards(self.game_info.map_center, 5)) # If we have less than 22 drones, build drones if self.supply_workers + self.already_pending(UnitTypeId.DRONE) < 22: if larvae and self.can_afford(UnitTypeId.DRONE): larva: Unit = larvae.random larva.train(UnitTypeId.DRONE) return # If we dont have both extractors: build them if ( self.structures(UnitTypeId.SPAWNINGPOOL) and self.gas_buildings.amount + self.already_pending(UnitTypeId.EXTRACTOR) < 2 ): if self.can_afford(UnitTypeId.EXTRACTOR): # May crash if we dont have any drones for vespene_geyser in self.vespene_geyser.closer_than(10, hq): drone: Unit = self.workers.random drone.build_gas(vespene_geyser) return # Saturate gas for a in self.gas_buildings: if a.assigned_harvesters < a.ideal_harvesters: w: Units = self.workers.closer_than(10, a) if w: w.random.gather(a) # Build queen once the pool is done if self.structures(UnitTypeId.SPAWNINGPOOL).ready: if not self.units(UnitTypeId.QUEEN) and hq.is_idle: if self.can_afford(UnitTypeId.QUEEN): hq.train(UnitTypeId.QUEEN) # Train zerglings if we have much more minerals than vespene (not enough gas for hydras) if self.units(UnitTypeId.ZERGLING).amount < 20 and self.minerals > 1000: if larvae and self.can_afford(UnitTypeId.ZERGLING): larvae.random.train(UnitTypeId.ZERGLING) def main(): run_game( maps.get("(2)CatalystLE"), [Bot(Race.Zerg, Hydralisk()), Computer(Race.Terran, Difficulty.Medium)], realtime=False, save_replay_as="ZvT.SC2Replay", ) if __name__ == "__main__": main() ================================================ FILE: examples/zerg/onebase_broodlord.py ================================================ # noqa: SIM102 import random from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId from sc2.main import run_game from sc2.player import Bot, Computer from sc2.position import Point2 from sc2.unit import Unit from sc2.units import Units class BroodlordBot(BotAI): def select_target(self) -> Point2: if self.enemy_structures: return random.choice(self.enemy_structures).position return self.enemy_start_locations[0] async def on_step(self, iteration: int): larvae: Units = self.larva forces: Units = self.units.of_type({UnitTypeId.ZERGLING, UnitTypeId.CORRUPTOR, UnitTypeId.BROODLORD}) if self.units(UnitTypeId.BROODLORD).amount > 2 and iteration % 50 == 0: for unit in forces: unit.attack(self.select_target()) if self.supply_left < 2: if larvae and self.can_afford(UnitTypeId.OVERLORD): larvae.random.train(UnitTypeId.OVERLORD) return if self.structures(UnitTypeId.GREATERSPIRE).ready: corruptors: Units = self.units(UnitTypeId.CORRUPTOR) # build half-and-half corruptors and broodlords if corruptors and corruptors.amount > self.units(UnitTypeId.BROODLORD).amount: if self.can_afford(UnitTypeId.BROODLORD): corruptors.random.train(UnitTypeId.BROODLORD) elif larvae and self.can_afford(UnitTypeId.CORRUPTOR): larvae.random.train(UnitTypeId.CORRUPTOR) return # Send all units to attack if we dont have any more townhalls if not self.townhalls: all_attack_units: Units = self.units.of_type( {UnitTypeId.DRONE, UnitTypeId.QUEEN, UnitTypeId.ZERGLING, UnitTypeId.CORRUPTOR, UnitTypeId.BROODLORD} ) for unit in all_attack_units: unit.attack(self.enemy_start_locations[0]) return hq: Unit = self.townhalls.first # Make idle queens inject for queen in self.units(UnitTypeId.QUEEN).idle: if queen.energy >= 25: queen(AbilityId.EFFECT_INJECTLARVA, hq) # Build pool if self.structures(UnitTypeId.SPAWNINGPOOL).amount + self.already_pending(UnitTypeId.SPAWNINGPOOL) == 0: if self.can_afford(UnitTypeId.SPAWNINGPOOL): await self.build(UnitTypeId.SPAWNINGPOOL, near=hq) # Upgrade to lair if self.structures(UnitTypeId.SPAWNINGPOOL).ready: if not self.townhalls(UnitTypeId.LAIR) and not self.townhalls(UnitTypeId.HIVE) and hq.is_idle: if self.can_afford(UnitTypeId.LAIR): hq.build(UnitTypeId.LAIR) # Build infestation pit if self.townhalls(UnitTypeId.LAIR).ready: if self.structures(UnitTypeId.INFESTATIONPIT).amount + self.already_pending(UnitTypeId.INFESTATIONPIT) == 0: if self.can_afford(UnitTypeId.INFESTATIONPIT): await self.build(UnitTypeId.INFESTATIONPIT, near=hq) # Build spire if self.structures(UnitTypeId.SPIRE).amount + self.already_pending(UnitTypeId.SPIRE) == 0: if self.can_afford(UnitTypeId.SPIRE): await self.build(UnitTypeId.SPIRE, near=hq) # Upgrade to hive if self.structures(UnitTypeId.INFESTATIONPIT).ready and not self.townhalls(UnitTypeId.HIVE) and hq.is_idle: if self.can_afford(UnitTypeId.HIVE): hq.build(UnitTypeId.HIVE) # Upgrade to greater spire if self.townhalls(UnitTypeId.HIVE).ready: spires: Units = self.structures(UnitTypeId.SPIRE).ready if spires: spire: Unit = spires.random if self.can_afford(UnitTypeId.GREATERSPIRE) and spire.is_idle: spire.build(UnitTypeId.GREATERSPIRE) # Build extractor if self.gas_buildings.amount + self.already_pending(UnitTypeId.EXTRACTOR) < 2: if self.can_afford(UnitTypeId.EXTRACTOR): drone: Unit = self.workers.random target: Unit = self.vespene_geyser.closest_to(drone.position) drone.build_gas(target) # Build up to 22 drones if self.supply_workers + self.already_pending(UnitTypeId.DRONE) < 22: if larvae and self.can_afford(UnitTypeId.DRONE): larva: Unit = larvae.random larva.train(UnitTypeId.DRONE) return # Saturate gas for extractor in self.gas_buildings: if extractor.assigned_harvesters < extractor.ideal_harvesters: workers: Units = self.workers.closer_than(20, extractor) if workers: workers.random.gather(extractor) # Build queen if self.structures(UnitTypeId.SPAWNINGPOOL).ready: if not self.units(UnitTypeId.QUEEN) and hq.is_idle: if self.can_afford(UnitTypeId.QUEEN): hq.train(UnitTypeId.QUEEN) # Build zerglings if we have not enough gas to build corruptors and broodlords if self.units(UnitTypeId.ZERGLING).amount < 40 and self.minerals > 1000: if larvae and self.can_afford(UnitTypeId.ZERGLING): larvae.random.train(UnitTypeId.ZERGLING) def main(): run_game( maps.get("AcropolisLE"), [Bot(Race.Zerg, BroodlordBot()), Computer(Race.Terran, Difficulty.Medium)], realtime=False, save_replay_as="ZvT.SC2Replay", ) if __name__ == "__main__": main() ================================================ FILE: examples/zerg/worker_split.py ================================================ """ This bot is just to demonstrate that you can do worker split at game start without having to use 'synchronous_do()'. This is especially important when your bot runs on realtime=True and you want your bot to be reliable against Human opponents. """ import asyncio from loguru import logger from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race from sc2.main import run_game from sc2.player import Bot, Computer from sc2.units import Units class WorkerSplitBot(BotAI): async def on_before_start(self): """This function is run before the expansion locations and ramps are calculated. These calculations can take up to a second, depending on the CPU.""" mf: Units = self.mineral_field for w in self.workers: w.gather(mf.closest_to(w)) await self._do_actions(self.actions) self.actions.clear() await asyncio.sleep(3) async def on_start(self): """This function is run after the expansion locations and ramps are calculated.""" async def on_step(self, iteration: int): if iteration % 10 == 0: await asyncio.sleep(3) # In realtime=False, this should print "8*x" and "x" if # self.client.game_step is set to 8 (default value) # But if your bot takes too long, it will skip game loops. logger.info(f"Bot's game loop is {self.state.game_loop} and iteration {iteration}") def main(): run_game( maps.get("AcropolisLE"), [Bot(Race.Zerg, WorkerSplitBot()), Computer(Race.Terran, Difficulty.Medium)], realtime=True, ) if __name__ == "__main__": main() ================================================ FILE: examples/zerg/zerg_rush.py ================================================ import numpy as np from loguru import logger from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race, Result from sc2.ids.ability_id import AbilityId from sc2.ids.buff_id import BuffId from sc2.ids.unit_typeid import UnitTypeId from sc2.ids.upgrade_id import UpgradeId from sc2.main import run_game from sc2.player import Bot, Computer from sc2.position import Point2, Point3 from sc2.unit import Unit from sc2.units import Units class ZergRushBot(BotAI): def __init__(self): self.on_end_called = False async def on_start(self): self.client.game_step = 2 async def on_step(self, iteration: int): if iteration == 0: await self.chat_send("(glhf)") # Draw creep pixelmap for debugging # self.draw_creep_pixelmap() # If townhall no longer exists: attack move with all units to enemy start location if not self.townhalls: for unit in self.units.exclude_type({UnitTypeId.EGG, UnitTypeId.LARVA}): unit.attack(self.enemy_start_locations[0]) return hatch: Unit = self.townhalls[0] # Pick a target location target_pos: Point2 = self.enemy_structures.not_flying.random_or(self.enemy_start_locations[0]).position # Give all zerglings an attack command for zergling in self.units(UnitTypeId.ZERGLING): zergling.attack(target=target_pos) # Inject hatchery if queen has more than 25 energy for queen in self.units(UnitTypeId.QUEEN): if queen.energy >= 25 and not hatch.has_buff(BuffId.QUEENSPAWNLARVATIMER): queen(AbilityId.EFFECT_INJECTLARVA, hatch) # Pull workers out of gas if we have almost enough gas mined, this will stop mining when we reached 100 gas mined if self.vespene >= 88 or self.already_pending_upgrade(UpgradeId.ZERGLINGMOVEMENTSPEED) > 0: gas_drones: Units = self.workers.filter(lambda w: w.is_carrying_vespene and len(w.orders) < 2) drone: Unit for drone in gas_drones: minerals: Units = self.mineral_field.closer_than(10, hatch) if minerals: mineral: Unit = minerals.closest_to(drone) drone.gather(mineral, queue=True) # If we have 100 vespene, this will try to research zergling speed once the spawning pool is at 100% completion if self.already_pending_upgrade(UpgradeId.ZERGLINGMOVEMENTSPEED) == 0 and self.can_afford( UpgradeId.ZERGLINGMOVEMENTSPEED ): spawning_pools_ready: Units = self.structures(UnitTypeId.SPAWNINGPOOL).ready if spawning_pools_ready: self.research(UpgradeId.ZERGLINGMOVEMENTSPEED) # If we have less than 2 supply left and no overlord is in the queue: train an overlord if self.supply_left < 2 and self.already_pending(UnitTypeId.OVERLORD) < 1: self.train(UnitTypeId.OVERLORD, 1) # While we have less than 88 vespene mined: send drones into extractor one frame at a time if ( self.gas_buildings.ready and self.vespene < 88 and self.already_pending_upgrade(UpgradeId.ZERGLINGMOVEMENTSPEED) == 0 ): extractor: Unit = self.gas_buildings.first if extractor.surplus_harvesters < 0: self.workers.random.gather(extractor) # If we have lost of minerals, make a macro hatchery if self.minerals > 500: for d in range(4, 15): pos: Point2 = hatch.position.towards(self.game_info.map_center, d) if await self.can_place_single(UnitTypeId.HATCHERY, pos): self.workers.random.build(UnitTypeId.HATCHERY, pos) break # While we have less than 16 drones, make more drones if self.can_afford(UnitTypeId.DRONE) and self.supply_workers < 16: self.train(UnitTypeId.DRONE) # If our spawningpool is completed, start making zerglings if self.structures(UnitTypeId.SPAWNINGPOOL).ready and self.larva and self.can_afford(UnitTypeId.ZERGLING): _amount_trained: int = self.train(UnitTypeId.ZERGLING, self.larva.amount) # If we have no extractor, build extractor if ( self.gas_buildings.amount + self.already_pending(UnitTypeId.EXTRACTOR) == 0 and self.can_afford(UnitTypeId.EXTRACTOR) and self.workers ): drone: Unit = self.workers.random target: Unit = self.vespene_geyser.closest_to(drone) drone.build_gas(target) # If we have no spawning pool, try to build spawning pool elif self.structures(UnitTypeId.SPAWNINGPOOL).amount + self.already_pending(UnitTypeId.SPAWNINGPOOL) == 0: if self.can_afford(UnitTypeId.SPAWNINGPOOL): for d in range(4, 15): pos: Point2 = hatch.position.towards(self.game_info.map_center, d) if await self.can_place_single(UnitTypeId.SPAWNINGPOOL, pos): drone: Unit = self.workers.closest_to(pos) drone.build(UnitTypeId.SPAWNINGPOOL, pos) # If we have no queen, try to build a queen if we have a spawning pool compelted elif ( self.units(UnitTypeId.QUEEN).amount + self.already_pending(UnitTypeId.QUEEN) < self.townhalls.amount and self.structures(UnitTypeId.SPAWNINGPOOL).ready ): if self.can_afford(UnitTypeId.QUEEN): self.train(UnitTypeId.QUEEN) def draw_creep_pixelmap(self): for (y, x), value in np.ndenumerate(self.state.creep.data_numpy): p = Point2((x, y)) h2 = self.get_terrain_z_height(p) pos = Point3((p.x, p.y, h2)) # Red if there is no creep color = Point3((255, 0, 0)) if value == 1: # Green if there is creep color = Point3((0, 255, 0)) self.client.debug_box2_out(pos, half_vertex_length=0.25, color=color) async def on_end(self, game_result: Result): self.on_end_called = True logger.info(f"{self.time_formatted} On end was called") def main(): run_game( maps.get("AcropolisLE"), [Bot(Race.Zerg, ZergRushBot()), Computer(Race.Terran, Difficulty.Medium)], realtime=False, save_replay_as="ZvT.SC2Replay", ) if __name__ == "__main__": main() ================================================ FILE: generate_dicts_from_data_json.py ================================================ """ This script does the following: - Loop over all abilities, checking what unit they create and if it requires a placement position - Loop over all units, checking what abilities they have and which of those create units, and what tech requirements they have - Loop over all all upgrades and get their creation ability, which unit can research it and what building requirements there are - Loop over all units and get their unit and tech aliases data.json origin: https://github.com/BurnySc2/sc2-techtree/tree/develop/data """ from __future__ import annotations import json import lzma import pickle from collections import OrderedDict from pathlib import Path from loguru import logger from sc2.game_data import GameData from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId from sc2.ids.upgrade_id import UpgradeId def get_map_file_path() -> Path: return Path(__file__).parent / "test" / "pickle_data" / "DeathAuraLE.xz" # Custom repr function so that the output is always the same and only changes when there were changes in the data.json tech tree file # The output just needs to be ordered (sorted by enum name), but it does not matter anymore if the bot then imports an unordered dict and set class OrderedDict2(OrderedDict): def __repr__(self): if not self: return "{}" return ( "{" + ", ".join(f"{repr(key)}: {repr(value)}" for key, value in sorted(self.items(), key=lambda u: u[0].name)) + "}" ) class OrderedSet2(set): def __repr__(self): if not self: return "set()" return "{" + ", ".join(repr(item) for item in sorted(self, key=lambda u: u.name)) + "}" def dump_dict_to_file( my_dict: OrderedDict2, file_path: Path, dict_name: str, file_header: str = "", dict_type_annotation: str = "" ): with file_path.open("w") as f: f.write(file_header) f.write("\n") f.write(f"{dict_name}{dict_type_annotation} = ") assert isinstance(my_dict, OrderedDict2) logger.info(my_dict) f.write(repr(my_dict)) def generate_init_file(dict_file_paths: list[Path], file_path: Path, file_header: str): base_file_names = sorted(path.stem for path in dict_file_paths) with file_path.open("w") as f: f.write(file_header) f.write("\n") all_line = f"__all__ = {base_file_names}" logger.info(all_line) f.write(all_line) def get_unit_train_build_abilities(data): ability_data = data["Ability"] unit_data = data["Unit"] _upgrade_data = data["Upgrade"] # From which abilities can a unit be trained train_abilities: dict[UnitTypeId, set[AbilityId]] = OrderedDict2() # If the ability requires a placement position ability_requires_placement: set[AbilityId] = set() # Map ability to unittypeid ability_to_unittypeid_dict: dict[AbilityId, UnitTypeId] = OrderedDict2() # From which abilities can a unit be morphed # unit_morph_abilities: dict[UnitTypeId, set[AbilityId]] = {} entry: dict for entry in ability_data: """ "target": "PointOrUnit" """ if isinstance(entry.get("target", {}), str): continue ability_id: AbilityId = AbilityId(entry["id"]) created_unit_type_id: UnitTypeId # Check if it is a unit train ability requires_placement = False train_unit_type_id_value: int = entry.get("target", {}).get("Train", {}).get("produces", 0) train_place_unit_type_id_value: int = entry.get("target", {}).get("TrainPlace", {}).get("produces", 0) morph_unit_type_id_value: int = entry.get("target", {}).get("Morph", {}).get("produces", 0) build_unit_type_id_value: int = entry.get("target", {}).get("Build", {}).get("produces", 0) build_on_unit_unit_type_id_value: int = entry.get("target", {}).get("BuildOnUnit", {}).get("produces", 0) if not train_unit_type_id_value and train_place_unit_type_id_value: train_unit_type_id_value = train_place_unit_type_id_value requires_placement = True # Collect larva morph abilities, and one way morphs (exclude burrow, hellbat morph, siege tank siege) # Also doesnt include building addons if not train_unit_type_id_value and ( "LARVATRAIN_" in ability_id.name or ability_id in { AbilityId.MORPHTOBROODLORD_BROODLORD, AbilityId.MORPHZERGLINGTOBANELING_BANELING, AbilityId.MORPHTORAVAGER_RAVAGER, AbilityId.MORPHTOBANELING_BANELING, AbilityId.MORPH_LURKER, AbilityId.UPGRADETOLAIR_LAIR, AbilityId.UPGRADETOHIVE_HIVE, AbilityId.UPGRADETOGREATERSPIRE_GREATERSPIRE, AbilityId.UPGRADETOORBITAL_ORBITALCOMMAND, AbilityId.UPGRADETOPLANETARYFORTRESS_PLANETARYFORTRESS, AbilityId.MORPH_OVERLORDTRANSPORT, AbilityId.MORPH_OVERSEER, } ): # If all morph units are used, unit_trained_from.py will be "wrong" because it will list that a siege tank can be trained from siegetanksieged and similar: # UnitTypeId.SIEGETANK: {UnitTypeId.SIEGETANKSIEGED, UnitTypeId.FACTORY}, # if not train_unit_type_id_value and morph_unit_type_id_value: train_unit_type_id_value = morph_unit_type_id_value # Add all build abilities, like construct buildings and train queen (exception) if not train_unit_type_id_value and build_unit_type_id_value: train_unit_type_id_value = build_unit_type_id_value if "BUILD_" in entry["name"]: requires_placement = True # Add build gas building (refinery, assimilator, extractor) # TODO: target needs to be a unit, not a position, but i dont want to store an extra line just for this - needs to be an exception in bot_ai.py if not train_unit_type_id_value and build_on_unit_unit_type_id_value: train_unit_type_id_value = build_on_unit_unit_type_id_value if train_unit_type_id_value: created_unit_type_id = UnitTypeId(train_unit_type_id_value) if created_unit_type_id not in train_abilities: train_abilities[created_unit_type_id] = {ability_id} else: train_abilities[created_unit_type_id].add(ability_id) if requires_placement: ability_requires_placement.add(ability_id) ability_to_unittypeid_dict[ability_id] = created_unit_type_id """ unit_train_abilities = { UnitTypeId.GATEWAY: { UnitTypeId.ADEPT: { "ability": AbilityId.TRAIN_ADEPT, "requires_techlab": False, "required_building": UnitTypeId.CYBERNETICSCORE, # Or None "requires_placement_position": False, # True for warp gate "requires_power": True, # If a pylon nearby is required }, UnitTypeId.Zealot: { "ability": AbilityId.GATEWAYTRAIN_ZEALOT, ... } } } """ unit_train_abilities: dict[UnitTypeId, dict[str, AbilityId | bool | UnitTypeId]] = OrderedDict2() for entry in unit_data: unit_abilities = entry.get("abilities", []) unit_type = UnitTypeId(entry["id"]) current_unit_train_abilities = OrderedDict2() for ability_info in unit_abilities: ability_id_value: int = ability_info.get("ability", 0) if ability_id_value: ability_id: AbilityId = AbilityId(ability_id_value) # Ability is not a train ability if ability_id not in ability_to_unittypeid_dict: continue requires_techlab: bool = False required_building: UnitTypeId | None = None requires_placement_position: bool = False requires_power: bool = False """ requirements = [ { "addon": 5 }, { "building": 29 } ] """ requirements: list[dict[str, int]] = ability_info.get("requirements", []) if requirements: # Assume train abilities only have one tech building requirement; thors requiring armory and techlab is seperatedly counted assert len([req for req in requirements if req.get("building", 0)]) <= 1, ( f"Error: Building {unit_type} has more than one tech requirements with train ability {ability_id}" ) # UnitTypeId 5 == Techlab requires_techlab: bool = any(req for req in requirements if req.get("addon", 0) == 5) requires_tech_builing_id_value: int = next( (req["building"] for req in requirements if req.get("building", 0)), 0 ) if requires_tech_builing_id_value: required_building = UnitTypeId(requires_tech_builing_id_value) if ability_id in ability_requires_placement: requires_placement_position = True requires_power = entry.get("needs_power", False) resulting_unit = ability_to_unittypeid_dict[ability_id] ability_dict = {"ability": ability_id} # Only add boolean values and tech requirement if they actually exist, to make the resulting dict file smaller if requires_techlab: ability_dict["requires_techlab"] = requires_techlab if required_building: ability_dict["required_building"] = required_building if requires_placement_position: ability_dict["requires_placement_position"] = requires_placement_position if requires_power: ability_dict["requires_power"] = requires_power current_unit_train_abilities[resulting_unit] = ability_dict if current_unit_train_abilities: unit_train_abilities[unit_type] = current_unit_train_abilities return unit_train_abilities def get_upgrade_abilities(data): ability_data = data["Ability"] unit_data = data["Unit"] _upgrade_data = data["Upgrade"] ability_to_upgrade_dict: dict[AbilityId, UpgradeId] = OrderedDict2() """ We want to be able to research an upgrade by doing await self.can_research(UpgradeId, return_idle_structures=True) -> returns list of idle structures that can research it So we need to assign each upgrade id one building type, and its research ability and requirements (e.g. armory for infantry level 2) """ # Collect all upgrades and their corresponding abilities entry: dict for entry in ability_data: if isinstance(entry.get("target", {}), str): continue ability_id: AbilityId = AbilityId(entry["id"]) upgrade_id_value: int = entry.get("target", {}).get("Research", {}).get("upgrade", 0) if upgrade_id_value: upgrade_id: UpgradeId = UpgradeId(upgrade_id_value) ability_to_upgrade_dict[ability_id] = upgrade_id """ unit_research_abilities = { UnitTypeId.ENGINEERINGBAY: { UpgradeId.TERRANINFANTRYWEAPONSLEVEL1: { "ability": AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL1, "required_building": None, "requires_power": False, # If a pylon nearby is required }, UpgradeId.TERRANINFANTRYWEAPONSLEVEL2: { "ability": AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL2, "required_building": UnitTypeId.ARMORY, "requires_power": False, # If a pylon nearby is required }, } } """ unit_research_abilities = OrderedDict2() for entry in unit_data: unit_abilities = entry.get("abilities", []) unit_type = UnitTypeId(entry["id"]) if unit_type == UnitTypeId.TECHLAB: continue current_unit_research_abilities = OrderedDict2() for ability_info in unit_abilities: ability_id_value: int = ability_info.get("ability", 0) if ability_id_value: ability_id: AbilityId = AbilityId(ability_id_value) # Upgrade is not a known upgrade ability if ability_id not in ability_to_upgrade_dict: continue required_building = None required_upgrade = None requirements = ability_info.get("requirements", []) if requirements: req_building_id_value = next( (req["building"] for req in requirements if req.get("building", 0)), None ) if req_building_id_value: req_building_id = UnitTypeId(req_building_id_value) required_building = req_building_id req_upgrade_id_value = next((req["upgrade"] for req in requirements if req.get("upgrade", 0)), None) if req_upgrade_id_value: req_upgrade_id = UpgradeId(req_upgrade_id_value) required_upgrade = req_upgrade_id requires_power = entry.get("needs_power", False) resulting_upgrade = ability_to_upgrade_dict[ability_id] research_info = {"ability": ability_id} if required_building: research_info["required_building"] = required_building if required_upgrade: research_info["required_upgrade"] = required_upgrade if requires_power: research_info["requires_power"] = requires_power current_unit_research_abilities[resulting_upgrade] = research_info if current_unit_research_abilities: unit_research_abilities[unit_type] = current_unit_research_abilities return unit_research_abilities def get_unit_created_from(unit_train_abilities: dict): unit_created_from = OrderedDict2() for creator_unit, create_abilities in unit_train_abilities.items(): for created_unit, create_info in create_abilities.items(): if created_unit not in unit_created_from: unit_created_from[created_unit] = OrderedSet2() unit_created_from[created_unit].add(creator_unit) return unit_created_from def get_upgrade_researched_from(unit_research_abilities: dict): upgrade_researched_from = OrderedDict2() for researcher_unit, research_abilities in unit_research_abilities.items(): for upgrade, research_info in research_abilities.items(): # This if statement is to prevent LAIR and HIVE overriding "UpgradeId.OVERLORDSPEED" as well as greater spire overriding upgrade abilities if upgrade not in upgrade_researched_from: upgrade_researched_from[upgrade] = researcher_unit return upgrade_researched_from def get_unit_abilities(data: dict): _ability_data = data["Ability"] unit_data = data["Unit"] _upgrade_data = data["Upgrade"] all_unit_abilities: dict[UnitTypeId, set[AbilityId]] = OrderedDict2() entry: dict for entry in unit_data: entry_unit_abilities = entry.get("abilities", []) unit_type = UnitTypeId(entry["id"]) current_collected_unit_abilities: set[AbilityId] = OrderedSet2() for ability_info in entry_unit_abilities: ability_id_value: int = ability_info.get("ability", 0) if ability_id_value: ability_id: AbilityId = AbilityId(ability_id_value) current_collected_unit_abilities.add(ability_id) # logger.info(unit_type, current_unit_abilities) if current_collected_unit_abilities: all_unit_abilities[unit_type] = current_collected_unit_abilities return all_unit_abilities def generate_unit_alias_dict(data: dict): _ability_data = data["Ability"] unit_data = data["Unit"] _upgrade_data = data["Upgrade"] # Load pickled game data files from one of the test files pickled_file_path = get_map_file_path() assert pickled_file_path.is_file(), f"Could not find pickled data file {pickled_file_path}" logger.info(f"Loading pickled game data file {pickled_file_path}") with lzma.open(pickled_file_path.absolute(), "rb") as f: raw_game_data, raw_game_info, raw_observation = pickle.load(f) game_data = GameData(raw_game_data.data) all_unit_aliases: dict[UnitTypeId, UnitTypeId] = OrderedDict2() all_tech_aliases: dict[UnitTypeId, set[UnitTypeId]] = OrderedDict2() entry: dict for entry in unit_data: unit_type_value = entry["id"] unit_type = UnitTypeId(entry["id"]) current_unit_tech_aliases: set[UnitTypeId] = OrderedSet2() assert unit_type_value in game_data.units, ( f"Unit {unit_type} not listed in game_data.units - perhaps pickled file {pickled_file_path} is outdated?" ) unit_alias: int = game_data.units[unit_type_value]._proto.unit_alias if unit_alias: # Might be 0 if it has no alias unit_alias_unit_type_id = UnitTypeId(unit_alias) all_unit_aliases[unit_type] = unit_alias_unit_type_id tech_aliases: list[int] = game_data.units[unit_type_value]._proto.tech_alias for tech_alias in tech_aliases: # Might be 0 if it has no alias unit_alias_unit_type_id = UnitTypeId(tech_alias) current_unit_tech_aliases.add(unit_alias_unit_type_id) if current_unit_tech_aliases: all_tech_aliases[unit_type] = current_unit_tech_aliases return all_unit_aliases, all_tech_aliases def generate_redirect_abilities_dict(data: dict): ability_data = data["Ability"] _unit_data = data["Unit"] _upgrade_data = data["Upgrade"] all_redirect_abilities: dict[AbilityId, AbilityId] = OrderedDict2() entry: dict for entry in ability_data: ability_id_value: int = entry["id"] try: ability_id: AbilityId = AbilityId(ability_id_value) except Exception: logger.info(f"Error with ability id value {ability_id_value}") continue generic_redirect_ability_value = entry.get("remaps_to_ability_id", 0) if generic_redirect_ability_value == 0: # No generic ability available continue all_redirect_abilities[ability_id] = AbilityId(generic_redirect_ability_value) return all_redirect_abilities def main(): path = Path(__file__).parent data_path = path / "data" / "data.json" with data_path.open() as f: data = json.load(f) dicts_path = path / "sc2" / "dicts" Path(dicts_path).mkdir(parents=True, exist_ok=True) # All unit train and build abilities unit_train_abilities = get_unit_train_build_abilities(data=data) unit_creation_dict_path = dicts_path / "unit_train_build_abilities.py" # All upgrades and which building can research which upgrade unit_research_abilities = get_upgrade_abilities(data=data) unit_research_abilities_dict_path = dicts_path / "unit_research_abilities.py" # All train abilities (where a unit can be trained from) unit_trained_from = get_unit_created_from(unit_train_abilities=unit_train_abilities) unit_trained_from_dict_path = dicts_path / "unit_trained_from.py" # All research abilities (where an upgrade can be researched from) upgrade_researched_from = get_upgrade_researched_from(unit_research_abilities=unit_research_abilities) upgrade_researched_from_dict_path = dicts_path / "upgrade_researched_from.py" # All unit abilities without requirements unit_abilities = get_unit_abilities(data=data) unit_abilities_dict_path = dicts_path / "unit_abilities.py" # All unit_alias and tech_alias of a unit type unit_unit_alias, unit_tech_alias = generate_unit_alias_dict(data=data) unit_unit_alias_dict_path = dicts_path / "unit_unit_alias.py" unit_tech_alias_dict_path = dicts_path / "unit_tech_alias.py" # All redirect (generic) abilities of abilities all_redirect_abilities = generate_redirect_abilities_dict(data=data) all_redirect_abilities_path = dicts_path / "generic_redirect_abilities.py" file_name = Path(__file__).name file_header = f""" # THIS FILE WAS AUTOMATICALLY GENERATED BY "{file_name}" DO NOT CHANGE MANUALLY! # ANY CHANGE WILL BE OVERWRITTEN from sc2.ids.unit_typeid import UnitTypeId from sc2.ids.ability_id import AbilityId from sc2.ids.upgrade_id import UpgradeId # from sc2.ids.buff_id import BuffId # from sc2.ids.effect_id import EffectId from typing import Union """ dict_file_paths = [ unit_creation_dict_path, unit_research_abilities_dict_path, unit_trained_from_dict_path, upgrade_researched_from_dict_path, unit_abilities_dict_path, unit_unit_alias_dict_path, unit_tech_alias_dict_path, all_redirect_abilities_path, ] init_file_path = dicts_path / "__init__.py" init_header = f"""# DO NOT EDIT! # This file was automatically generated by "{file_name}" """ generate_init_file(dict_file_paths=dict_file_paths, file_path=init_file_path, file_header=init_header) dump_dict_to_file( unit_train_abilities, unit_creation_dict_path, dict_name="TRAIN_INFO", file_header=file_header, dict_type_annotation=": dict[UnitTypeId, dict[UnitTypeId, dict[str, Union[AbilityId, bool, UnitTypeId]]]]", ) dump_dict_to_file( unit_research_abilities, unit_research_abilities_dict_path, dict_name="RESEARCH_INFO", file_header=file_header, dict_type_annotation=": dict[UnitTypeId, dict[UpgradeId, dict[str, Union[AbilityId, bool, UnitTypeId, UpgradeId]]]]", ) dump_dict_to_file( unit_trained_from, unit_trained_from_dict_path, dict_name="UNIT_TRAINED_FROM", file_header=file_header, dict_type_annotation=": dict[UnitTypeId, set[UnitTypeId]]", ) dump_dict_to_file( upgrade_researched_from, upgrade_researched_from_dict_path, dict_name="UPGRADE_RESEARCHED_FROM", file_header=file_header, dict_type_annotation=": dict[UpgradeId, UnitTypeId]", ) dump_dict_to_file( unit_abilities, unit_abilities_dict_path, dict_name="UNIT_ABILITIES", file_header=file_header, dict_type_annotation=": dict[UnitTypeId, set[AbilityId]]", ) dump_dict_to_file( unit_unit_alias, unit_unit_alias_dict_path, dict_name="UNIT_UNIT_ALIAS", file_header=file_header, dict_type_annotation=": dict[UnitTypeId, UnitTypeId]", ) dump_dict_to_file( unit_tech_alias, unit_tech_alias_dict_path, dict_name="UNIT_TECH_ALIAS", file_header=file_header, dict_type_annotation=": dict[UnitTypeId, set[UnitTypeId]]", ) dump_dict_to_file( all_redirect_abilities, all_redirect_abilities_path, dict_name="GENERIC_REDIRECT_ABILITIES", file_header=file_header, dict_type_annotation=": dict[AbilityId, AbilityId]", ) if __name__ == "__main__": main() ================================================ FILE: generate_id_constants_from_stableid.py ================================================ from sc2.generate_ids import IdGenerator if __name__ == "__main__": id_updater = IdGenerator() id_updater.update_ids_from_stableid_json() ================================================ FILE: pyproject.toml ================================================ [project] name = "burnysc2" version = "7.2.1" description = "A StarCraft II API Client for Python 3" authors = [{ name = "BurnySc2", email = "gamingburny@gmail.com" }] requires-python = ">=3.9, <3.15" keywords = ["StarCraft", "StarCraft 2", "StarCraft II", "AI", "Bot"] classifiers = [ "Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Science/Research", "Topic :: Games/Entertainment", "Topic :: Games/Entertainment :: Real Time Strategy", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Operating System :: POSIX :: Linux", "Operating System :: Microsoft :: Windows", "Operating System :: MacOS :: MacOS X", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] readme = "README.md" license-files = ["LICENSE"] dependencies = [ "aiohttp>=3.11.10", "loguru>=0.7.3", "mpyq>=0.2.5", "numpy>=2.3.3; python_full_version >= '3.14'", "numpy>=2.1.0; python_full_version >= '3.13'", "numpy>=2.0.0; python_full_version < '3.13'", "portpicker>=1.6.0", "pys2clientprotocol>=1.0.2", "scipy>=1.16.3; python_full_version >= '3.14'", "scipy>=1.14.1; python_full_version >= '3.13'", "scipy>=1.7.1; python_full_version < '3.13'", ] [dependency-groups] dev = [ "coverage>=7.6.9", "hypothesis>=6.122.3", "matplotlib>=3.9.4", "mypy>=1.13.0", "pillow>=12.0.0; python_full_version >= '3.14'", "pillow>=11.0.0; python_full_version < '3.14'", "pre-commit>=4.0.1", "protobuf>=6,<8", "pyglet>=2.0.20", "pylint>=3.3.2", # Type checker "pyrefly>=0.58.0", "pytest>=8.3.4", "pytest-asyncio>=0.25.0", "pytest-benchmark>=5.1.0", "pytest-cov>=6.0.0", "radon>=6.0.1", # Linter "ruff>=0.8.3", "sphinx-book-theme>=1.1.3", "sphinx>=7.4.7", "sphinx-autodoc-typehints>=2.3.0", "toml>=0.10.2", "yapf>=0.43.0", ] [tool.setuptools] package-dir = { sc2 = "sc2" } [tool.setuptools.package-data] sc2 = ["py.typed", "*.pyi"] [build-system] # https://packaging.python.org/en/latest/tutorials/packaging-projects/#choosing-a-build-backend # https://setuptools.pypa.io/en/latest/userguide/package_discovery.html#custom-discovery requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project.urls] Repository = "https://github.com/Burnysc2/python-sc2" Documentation = "https://burnysc2.github.io/python-sc2" [tool.yapf] based_on_style = "pep8" column_limit = 120 split_arguments_when_comma_terminated = true dedent_closing_brackets = true allow_split_before_dict_value = false [tool.pyrefly] project_includes = [ "sc2", "examples", "test" ] project-excludes = [ # Disable for those files and folders "sc2/data.py", ] [tool.pyrefly.errors] bad-override = false inconsistent-overload = false [tool.ruff] target-version = 'py310' line-length = 120 [tool.ruff.lint] # Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" select = [ "C4", # flake8-comprehensions "E", # Error "F", # pyflakes "BLE", # flake8-blind-except # "I", # isort "N", # pep8-naming "PGH", # pygrep-hooks "PTH", # flake8-use-pathlib "SIM", # flake8-simplify "W", # Warning "Q", # flake8-quotes "YTT", # flake8-2020 "UP", # pyupgrade # "A", # flake8-builtins ] # Allow Pydantic's `@validator` decorator to trigger class method treatment. pep8-naming.classmethod-decorators = ["pydantic.validator", "classmethod"] ignore = [ "E501", # Line too long "E402", # Module level import not at top of file "F841", # Local variable `...` is assigned to but never used "BLE001", # Do not catch blind exception: `Exception` "N802", # Function name `...` should be lowercase "N806", # Variable `...` in function should be lowercase. "SIM102", # Use a single `if` statement instead of nested `if` statements "UP007", # Use `X | Y` for type annotations "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` ] [tool.pyupgrade] # Preserve types, even if a file imports `from __future__ import annotations`. # Remove once support for py3.8 and 3.9 is dropped keep-runtime-typing = true [tool.pep8-naming] # Allow Pydantic's `@validator` decorator to trigger class method treatment. classmethod-decorators = ["pydantic.validator", "classmethod"] ================================================ FILE: sc2/__init__.py ================================================ from pathlib import Path def is_submodule(path): if path.is_file(): return path.suffix == ".py" and path.stem != "__init__" if path.is_dir(): return (path / "__init__.py").exists() return False __all__ = [p.stem for p in Path(__file__).parent.iterdir() if is_submodule(p)] ================================================ FILE: sc2/action.py ================================================ from __future__ import annotations from itertools import groupby from typing import TYPE_CHECKING from s2clientprotocol import raw_pb2 as raw_pb from sc2.position import Point2 from sc2.unit import Unit if TYPE_CHECKING: from sc2.ids.ability_id import AbilityId from sc2.unit_command import UnitCommand def combine_actions(action_iter: list[UnitCommand]): """ Example input: [ # Each entry in the list is a unit command, with an ability, unit, target, and queue=boolean UnitCommand(AbilityId.TRAINQUEEN_QUEEN, Unit(name='Hive', tag=4353687554), None, False), UnitCommand(AbilityId.TRAINQUEEN_QUEEN, Unit(name='Lair', tag=4359979012), None, False), UnitCommand(AbilityId.TRAINQUEEN_QUEEN, Unit(name='Hatchery', tag=4359454723), None, False), ] """ for key, items in groupby(action_iter, key=lambda a: a.combining_tuple): ability: AbilityId target: None | Point2 | Unit queue: bool # See constants.py for combineable abilities combineable: bool ability, target, queue, combineable = key if combineable: # Combine actions with no target, e.g. lift, burrowup, burrowdown, siege, unsiege, uproot spines cmd = raw_pb.ActionRawUnitCommand( ability_id=ability.value, # pyrefly: ignore unit_tags={u.unit.tag for u in items}, queue_command=queue, ) # Combine actions with target point, e.g. attack_move or move commands on a position if isinstance(target, Point2): cmd.target_world_space_pos.x = target.x cmd.target_world_space_pos.y = target.y # Combine actions with target unit, e.g. attack commands directly on a unit elif isinstance(target, Unit): cmd.target_unit_tag = target.tag elif target is not None: raise RuntimeError(f"Must target a unit, point or None, found '{target!r}'") yield raw_pb.ActionRaw(unit_command=cmd) else: """ Return one action for each unit; this is required for certain commands that would otherwise be grouped, and only executed once Examples: Select 3 hatcheries, build a queen with each hatch - the grouping function would group these unit tags and only issue one train command once to all 3 unit tags - resulting in one total train command I imagine the same thing would happen to certain other abilities: Battlecruiser yamato on same target, queen transfuse on same target, ghost snipe on same target, all build commands with the same unit type and also all morphs (zergling to banelings) However, other abilities can and should be grouped, see constants.py 'COMBINEABLE_ABILITIES' """ if target is None: for u in items: cmd = raw_pb.ActionRawUnitCommand( ability_id=ability.value, # pyrefly: ignore unit_tags={u.unit.tag}, queue_command=queue, ) yield raw_pb.ActionRaw(unit_command=cmd) elif isinstance(target, Point2): for u in items: cmd = raw_pb.ActionRawUnitCommand( ability_id=ability.value, # pyrefly: ignore unit_tags={u.unit.tag}, queue_command=queue, target_world_space_pos=target.as_Point2D, ) yield raw_pb.ActionRaw(unit_command=cmd) elif isinstance(target, Unit): for u in items: cmd = raw_pb.ActionRawUnitCommand( ability_id=ability.value, # pyrefly: ignore unit_tags={u.unit.tag}, queue_command=queue, target_unit_tag=target.tag, ) yield raw_pb.ActionRaw(unit_command=cmd) else: raise RuntimeError(f"Must target a unit, point or None, found '{target!r}'") ================================================ FILE: sc2/bot_ai.py ================================================ from __future__ import annotations import math import random import warnings from collections import Counter from functools import cached_property from typing import TYPE_CHECKING from loguru import logger from sc2.bot_ai_internal import BotAIInternal from sc2.cache import property_cache_once_per_frame from sc2.constants import ( CREATION_ABILITY_FIX, EQUIVALENTS_FOR_TECH_PROGRESS, PROTOSS_TECH_REQUIREMENT, TERRAN_STRUCTURES_REQUIRE_SCV, TERRAN_TECH_REQUIREMENT, ZERG_TECH_REQUIREMENT, ) from sc2.data import Alert, Race, Result, Target from sc2.dicts.unit_research_abilities import RESEARCH_INFO from sc2.dicts.unit_train_build_abilities import TRAIN_INFO from sc2.dicts.unit_trained_from import UNIT_TRAINED_FROM from sc2.dicts.upgrade_researched_from import UPGRADE_RESEARCHED_FROM from sc2.game_data import AbilityData, Cost from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId from sc2.ids.upgrade_id import UpgradeId from sc2.position import Point2 from sc2.unit import Unit from sc2.units import Units if TYPE_CHECKING: from sc2.game_info import Ramp class BotAI(BotAIInternal): """Base class for bots.""" EXPANSION_GAP_THRESHOLD = 15 @property def time(self) -> float: """Returns time in seconds, assumes the game is played on 'faster'""" return self.state.game_loop / 22.4 # / (1/1.4) * (1/16) @property def time_formatted(self) -> str: """Returns time as string in min:sec format""" t = self.time return f"{int(t // 60):02}:{int(t % 60):02}" @property def step_time(self) -> tuple[float, float, float, float]: """Returns a tuple of step duration in milliseconds. First value is the minimum step duration - the shortest the bot ever took Second value is the average step duration Third value is the maximum step duration - the longest the bot ever took (including on_start()) Fourth value is the step duration the bot took last iteration If called in the first iteration, it returns (inf, 0, 0, 0)""" avg_step_duration = ( (self._total_time_in_on_step / self._total_steps_iterations) if self._total_steps_iterations else 0 ) return ( self._min_step_time * 1000, avg_step_duration * 1000, self._max_step_time * 1000, self._last_step_step_time * 1000, ) def alert(self, alert_code: Alert) -> bool: """ Check if alert is triggered in the current step. Possible alerts are listed here https://github.com/Blizzard/s2client-proto/blob/e38efed74c03bec90f74b330ea1adda9215e655f/s2clientprotocol/sc2api.proto#L679-L702 Example use:: from sc2.data import Alert if self.alert(Alert.AddOnComplete): print("Addon Complete") Alert codes:: AlertError AddOnComplete BuildingComplete BuildingUnderAttack LarvaHatched MergeComplete MineralsExhausted MorphComplete MothershipComplete MULEExpired NuclearLaunchDetected NukeComplete NydusWormDetected ResearchComplete TrainError TrainUnitComplete TrainWorkerComplete TransformationComplete UnitUnderAttack UpgradeComplete VespeneExhausted WarpInComplete :param alert_code: """ assert isinstance(alert_code, Alert), f"alert_code {alert_code} is no Alert" return alert_code.value in self.state.alerts @property def start_location(self) -> Point2: """ Returns the spawn location of the bot, using the position of the first created townhall. This will be None if the bot is run on an arcade or custom map that does not feature townhalls at game start. """ return self.game_info.player_start_location @property def enemy_start_locations(self) -> list[Point2]: """Possible start locations for enemies.""" return self.game_info.start_locations @cached_property def main_base_ramp(self) -> Ramp: """Returns the Ramp instance of the closest main-ramp to start location. Look in game_info.py for more information about the Ramp class Example: See terran ramp wall bot """ # The reason for len(ramp.upper) in {2, 5} is: # ParaSite map has 5 upper points, and most other maps have 2 upper points at the main ramp. # The map Acolyte has 4 upper points at the wrong ramp (which is closest to the start position). try: found_main_base_ramp = min( (ramp for ramp in self.game_info.map_ramps if len(ramp.upper) in {2, 5}), key=lambda r: self.start_location.distance_to(r.top_center), ) except ValueError: # Hardcoded hotfix for Honorgrounds LE map, as that map has a large main base ramp with inbase natural found_main_base_ramp = min( (ramp for ramp in self.game_info.map_ramps if len(ramp.upper) in {4, 9}), key=lambda r: self.start_location.distance_to(r.top_center), ) return found_main_base_ramp @property_cache_once_per_frame def expansion_locations_list(self) -> list[Point2]: """Returns a list of expansion positions, not sorted in any way.""" assert self._expansion_positions_list, ( "self._find_expansion_locations() has not been run yet, so accessing the list of expansion locations is pointless." ) return self._expansion_positions_list @property_cache_once_per_frame def expansion_locations_dict(self) -> dict[Point2, Units]: """ Returns dict with the correct expansion position Point2 object as key, resources as Units (mineral fields and vespene geysers) as value. Caution: This function is slow. If you only need the expansion locations, use the property above. """ assert self._expansion_positions_list, ( "self._find_expansion_locations() has not been run yet, so accessing the list of expansion locations is pointless." ) expansion_locations: dict[Point2, Units] = {pos: Units([], self) for pos in self._expansion_positions_list} for resource in self.resources: # It may be that some resources are not mapped to an expansion location exp_positions: set[Point2] | None = self._resource_location_to_expansion_position_dict.get( resource.position, None ) if exp_positions: for exp_position in exp_positions: assert exp_position in expansion_locations expansion_locations[exp_position].append(resource) return expansion_locations @property def units_created(self) -> Counter[UnitTypeId]: """Returns a Counter for all your units and buildings you have created so far. This may be used for statistics (at the end of the game) or for strategic decision making. CAUTION: This does not properly work at the moment for morphing units and structures. Please use the 'on_unit_type_changed' event to add these morphing unit types manually to 'self._units_created'. Issues would arrise in e.g. siege tank morphing to sieged tank, and then morphing back (suddenly the counter counts 2 tanks have been created). Examples:: # Give attack command to enemy base every time 10 marines have been trained async def on_unit_created(self, unit: Unit): if unit.type_id == UnitTypeId.MARINE: if self.units_created[MARINE] % 10 == 0: for marine in self.units(UnitTypeId.MARINE): marine.attack(self.enemy_start_locations[0]) """ return self._units_created async def get_available_abilities( self, units: list[Unit] | Units, ignore_resource_requirements: bool = False ) -> list[list[AbilityId]]: """Returns available abilities of one or more units. Right now only checks cooldown, energy cost, and whether the ability has been researched. Examples:: units_abilities = await self.get_available_abilities(self.units) or:: units_abilities = await self.get_available_abilities([self.units.random]) :param units: :param ignore_resource_requirements:""" return await self.client.query_available_abilities(units, ignore_resource_requirements) async def expand_now( self, building: UnitTypeId | None = None, max_distance: int = 10, location: Point2 | None = None, ) -> None: """Finds the next possible expansion via 'self.get_next_expansion()'. If the target expansion is blocked (e.g. an enemy unit), it will misplace the expansion. :param building: :param max_distance: :param location:""" if building is None: # self.race is never Race.Random start_townhall_type = { Race.Protoss: UnitTypeId.NEXUS, Race.Terran: UnitTypeId.COMMANDCENTER, Race.Zerg: UnitTypeId.HATCHERY, } building = start_townhall_type[self.race] assert isinstance(building, UnitTypeId), f"{building} is no UnitTypeId" if not location: location = await self.get_next_expansion() if not location: # All expansions are used up or mined out logger.warning("Trying to expand_now() but bot is out of locations to expand to") return await self.build(building, near=location, max_distance=max_distance, random_alternative=False, placement_step=1) async def get_next_expansion(self) -> Point2 | None: """Find next expansion location.""" closest = None best_distance = math.inf start_position = self.game_info.player_start_location for position in self.expansion_locations_list: def is_near_to_expansion(t): return t.distance_to(position) < self.EXPANSION_GAP_THRESHOLD if any(map(is_near_to_expansion, self.townhalls)): # already taken continue distance = await self.client.query_pathing(start_position, position) if distance is None: continue if distance < best_distance: best_distance = distance closest = position return closest async def distribute_workers(self, resource_ratio: float = 2) -> None: """ Distributes workers across all the bases taken. Keyword `resource_ratio` takes a float. If the current minerals to gas ratio is bigger than `resource_ratio`, this function prefer filling gas_buildings first, if it is lower, it will prefer sending workers to minerals first. NOTE: This function is far from optimal, if you really want to have refined worker control, you should write your own distribution function. For example long distance mining control and moving workers if a base was killed are not being handled. WARNING: This is quite slow when there are lots of workers or multiple bases. :param resource_ratio:""" if not self.mineral_field or not self.workers or not self.townhalls.ready: return worker_pool = self.workers.idle bases = self.townhalls.ready gas_buildings = self.gas_buildings.ready # list of places that need more workers deficit_mining_places = [] for mining_place in bases | gas_buildings: difference = mining_place.surplus_harvesters # perfect amount of workers, skip mining place if not difference: continue if mining_place.has_vespene: # get all workers that target the gas extraction site # or are on their way back from it local_workers = self.workers.filter( lambda unit: unit.order_target == mining_place.tag or (unit.is_carrying_vespene and unit.order_target == bases.closest_to(mining_place).tag) ) else: # get tags of minerals around expansion local_minerals_tags = { mineral.tag for mineral in self.mineral_field if mineral.distance_to(mining_place) <= 8 } # get all target tags a worker can have # tags of the minerals he could mine at that base # get workers that work at that gather site local_workers = self.workers.filter( lambda unit: unit.order_target in local_minerals_tags or (unit.is_carrying_minerals and unit.order_target == mining_place.tag) ) # too many workers if difference > 0: for worker in local_workers[:difference]: worker_pool.append(worker) # too few workers # add mining place to deficit bases for every missing worker else: deficit_mining_places += [mining_place for _ in range(-difference)] # prepare all minerals near a base if we have too many workers # and need to send them to the closest patch all_minerals_near_base = [] if len(worker_pool) > len(deficit_mining_places): all_minerals_near_base = [ mineral for mineral in self.mineral_field if any(mineral.distance_to(base) <= 8 for base in self.townhalls.ready) ] # distribute every worker in the pool for worker in worker_pool: # as long as have workers and mining places if deficit_mining_places: # choose only mineral fields first if current mineral to gas ratio is less than target ratio if self.vespene and self.minerals / self.vespene < resource_ratio: possible_mining_places = [place for place in deficit_mining_places if not place.vespene_contents] # else prefer gas else: possible_mining_places = [place for place in deficit_mining_places if place.vespene_contents] # if preferred type is not available any more, get all other places if not possible_mining_places: possible_mining_places = deficit_mining_places # find closest mining place current_place = min(deficit_mining_places, key=lambda place: place.distance_to(worker)) # remove it from the list deficit_mining_places.remove(current_place) # if current place is a gas extraction site, go there if current_place.vespene_contents: worker.gather(current_place) # if current place is a gas extraction site, # go to the mineral field that is near and has the most minerals left else: local_minerals = ( mineral for mineral in self.mineral_field if mineral.distance_to(current_place) <= 8 ) # local_minerals can be empty if townhall is misplaced target_mineral = max(local_minerals, key=lambda mineral: mineral.mineral_contents, default=None) if target_mineral: worker.gather(target_mineral) # more workers to distribute than free mining spots # send to closest if worker is doing nothing elif worker.is_idle and all_minerals_near_base: target_mineral = min(all_minerals_near_base, key=lambda mineral: mineral.distance_to(worker)) worker.gather(target_mineral) else: # there are no deficit mining places and worker is not idle # so dont move him pass @property_cache_once_per_frame def owned_expansions(self) -> dict[Point2, Unit]: """Dict of expansions owned by the player with mapping {expansion_location: townhall_structure}.""" owned = {} for el in self.expansion_locations_list: def is_near_to_expansion(t): return t.distance_to(el) < self.EXPANSION_GAP_THRESHOLD th = next((x for x in self.townhalls if is_near_to_expansion(x)), None) if th: owned[el] = th return owned def calculate_supply_cost(self, unit_type: UnitTypeId) -> float: """ This function calculates the required supply to train or morph a unit. The total supply of a baneling is 0.5, but a zergling already uses up 0.5 supply, so the morph supply cost is 0. The total supply of a ravager is 3, but a roach already uses up 2 supply, so the morph supply cost is 1. The required supply to build zerglings is 1 because they pop in pairs, so this function returns 1 because the larva morph command requires 1 free supply. Example:: roach_supply_cost = self.calculate_supply_cost(UnitTypeId.ROACH) # Is 2 ravager_supply_cost = self.calculate_supply_cost(UnitTypeId.RAVAGER) # Is 1 baneling_supply_cost = self.calculate_supply_cost(UnitTypeId.BANELING) # Is 0 :param unit_type:""" if unit_type in {UnitTypeId.ZERGLING}: return 1 if unit_type in {UnitTypeId.BANELING}: return 0 unit_supply_cost = self.game_data.units[unit_type.value]._proto.food_required if unit_supply_cost > 0 and unit_type in UNIT_TRAINED_FROM and len(UNIT_TRAINED_FROM[unit_type]) == 1: producer: UnitTypeId for producer in UNIT_TRAINED_FROM[unit_type]: producer_unit_data = self.game_data.units[producer.value] if producer_unit_data._proto.food_required <= unit_supply_cost: producer_supply_cost = producer_unit_data._proto.food_required unit_supply_cost -= producer_supply_cost return unit_supply_cost def can_feed(self, unit_type: UnitTypeId) -> bool: """Checks if you have enough free supply to build the unit Example:: cc = self.townhalls.idle.random_or(None) # self.townhalls can be empty or there are no idle townhalls if cc and self.can_feed(UnitTypeId.SCV): cc.train(UnitTypeId.SCV) :param unit_type:""" required = self.calculate_supply_cost(unit_type) # "required <= 0" in case self.supply_left is negative return required <= 0 or self.supply_left >= required def calculate_unit_value(self, unit_type: UnitTypeId) -> Cost: """ Unlike the function below, this function returns the value of a unit given by the API (e.g. the resources lost value on kill). Examples:: self.calculate_value(UnitTypeId.ORBITALCOMMAND) == Cost(550, 0) self.calculate_value(UnitTypeId.RAVAGER) == Cost(100, 100) self.calculate_value(UnitTypeId.ARCHON) == Cost(175, 275) :param unit_type: """ unit_data = self.game_data.units[unit_type.value] return Cost(unit_data._proto.mineral_cost, unit_data._proto.vespene_cost) def calculate_cost(self, item_id: UnitTypeId | UpgradeId | AbilityId) -> Cost: """ Calculate the required build, train or morph cost of a unit. It is recommended to use the UnitTypeId instead of the ability to create the unit. The total cost to create a ravager is 100/100, but the actual morph cost from roach to ravager is only 25/75, so this function returns 25/75. It is adviced to use the UnitTypeId instead of the AbilityId. Instead of:: self.calculate_cost(AbilityId.UPGRADETOORBITAL_ORBITALCOMMAND) use:: self.calculate_cost(UnitTypeId.ORBITALCOMMAND) More examples:: from sc2.game_data import Cost self.calculate_cost(UnitTypeId.BROODLORD) == Cost(150, 150) self.calculate_cost(UnitTypeId.RAVAGER) == Cost(25, 75) self.calculate_cost(UnitTypeId.BANELING) == Cost(25, 25) self.calculate_cost(UnitTypeId.ORBITALCOMMAND) == Cost(150, 0) self.calculate_cost(UnitTypeId.REACTOR) == Cost(50, 50) self.calculate_cost(UnitTypeId.TECHLAB) == Cost(50, 25) self.calculate_cost(UnitTypeId.QUEEN) == Cost(150, 0) self.calculate_cost(UnitTypeId.HATCHERY) == Cost(300, 0) self.calculate_cost(UnitTypeId.LAIR) == Cost(150, 100) self.calculate_cost(UnitTypeId.HIVE) == Cost(200, 150) :param item_id: """ if isinstance(item_id, UnitTypeId): # Fix cost for reactor and techlab where the API returns 0 for both if item_id in {UnitTypeId.REACTOR, UnitTypeId.TECHLAB, UnitTypeId.ARCHON, UnitTypeId.BANELING}: if item_id == UnitTypeId.REACTOR: return Cost(50, 50) if item_id == UnitTypeId.TECHLAB: return Cost(50, 25) if item_id == UnitTypeId.BANELING: return Cost(25, 25) if item_id == UnitTypeId.ARCHON: return self.calculate_unit_value(UnitTypeId.ARCHON) unit_data = self.game_data.units[item_id.value] # Cost of morphs is automatically correctly calculated by 'calculate_ability_cost' creation_ability = unit_data.creation_ability if creation_ability is None: logger.error(f"Unknown creation_ability in calculate_cost for item_id: {item_id}") return Cost(0, 0) return self.game_data.calculate_ability_cost(creation_ability.exact_id) if isinstance(item_id, UpgradeId): cost = self.game_data.upgrades[item_id.value].cost else: # Is already AbilityId cost = self.game_data.calculate_ability_cost(item_id) return cost def can_afford(self, item_id: UnitTypeId | UpgradeId | AbilityId, check_supply_cost: bool = True) -> bool: """Tests if the player has enough resources to build a unit or structure. Example:: cc = self.townhalls.idle.random_or(None) # self.townhalls can be empty or there are no idle townhalls if cc and self.can_afford(UnitTypeId.SCV): cc.train(UnitTypeId.SCV) Example:: # Current state: we have 150 minerals and one command center and a barracks can_afford_morph = self.can_afford(UnitTypeId.ORBITALCOMMAND, check_supply_cost=False) # Will be 'True' although the API reports that an orbital is worth 550 minerals, but the morph cost is only 150 minerals :param item_id: :param check_supply_cost:""" cost = self.calculate_cost(item_id) if cost.minerals > self.minerals or cost.vespene > self.vespene: return False if check_supply_cost and isinstance(item_id, UnitTypeId): supply_cost = self.calculate_supply_cost(item_id) if supply_cost and supply_cost > self.supply_left: return False return True async def can_cast( self, unit: Unit, ability_id: AbilityId, target: Unit | Point2 | None = None, only_check_energy_and_cooldown: bool = False, cached_abilities_of_unit: list[AbilityId] | None = None, ) -> bool: """Tests if a unit has an ability available and enough energy to cast it. Example:: stalkers = self.units(UnitTypeId.STALKER) stalkers_that_can_blink = stalkers.filter(lambda unit: unit.type_id == UnitTypeId.STALKER and (await self.can_cast(unit, AbilityId.EFFECT_BLINK_STALKER, only_check_energy_and_cooldown=True))) See data_pb2.py (line 161) for the numbers 1-5 to make sense :param unit: :param ability_id: :param target: :param only_check_energy_and_cooldown: :param cached_abilities_of_unit:""" assert isinstance(unit, Unit), f"{unit} is no Unit object" assert isinstance(ability_id, AbilityId), f"{ability_id} is no AbilityId" assert isinstance(target, (type(None), Unit, Point2)) # check if unit has enough energy to cast or if ability is on cooldown if cached_abilities_of_unit: abilities = cached_abilities_of_unit else: abilities = (await self.get_available_abilities([unit], ignore_resource_requirements=False))[0] if ability_id in abilities: if only_check_energy_and_cooldown: return True cast_range = self.game_data.abilities[ability_id.value]._proto.cast_range ability_target: int = self.game_data.abilities[ability_id.value]._proto.target # Check if target is in range (or is a self cast like stimpack) if ( # Can't replace 1 with "Target.None.value" because ".None" doesn't seem to be a valid enum name ability_target == 1 or ability_target == Target.PointOrNone.value and ( # Target is unit isinstance(target, Unit) and unit.distance_to(target) <= unit.radius + target.radius + cast_range # Target is position or isinstance(target, Point2) and unit.distance_to(target) <= unit.radius + cast_range ) ): return True # Check if able to use ability on a unit if ( ability_target in {Target.Unit.value, Target.PointOrUnit.value} and isinstance(target, Unit) and unit.distance_to(target) <= unit.radius + target.radius + cast_range ): return True # Check if able to use ability on a position if ( ability_target in {Target.Point.value, Target.PointOrUnit.value} and isinstance(target, Point2) and unit.distance_to(target) <= unit.radius + cast_range ): return True return False def select_build_worker(self, pos: Unit | Point2, force: bool = False) -> Unit | None: """Select a worker to build a building with. Example:: barracks_placement_position = self.main_base_ramp.barracks_correct_placement worker = self.select_build_worker(barracks_placement_position) # Can return None if worker: worker.build(UnitTypeId.BARRACKS, barracks_placement_position) :param pos: :param force:""" workers = ( self.workers.filter(lambda w: (w.is_gathering or w.is_idle) and w.distance_to(pos) < 20) or self.workers ) if workers: for worker in workers.sorted_by_distance_to(pos).prefer_idle: if ( worker not in self.unit_tags_received_action and not worker.orders or len(worker.orders) == 1 and worker.orders[0].ability.id in {AbilityId.MOVE, AbilityId.HARVEST_GATHER} ): return worker return workers.random if force else None return None async def can_place_single(self, building: AbilityId | UnitTypeId, position: Point2) -> bool: """Checks the placement for only one position.""" if isinstance(building, UnitTypeId): creation_ability = self.game_data.units[building.value].creation_ability if creation_ability is None: logger.error(f"Unknown creation_ability in can_place_single for building: {building}") return False creation_ability_id = creation_ability.id return (await self.client._query_building_placement_fast(creation_ability_id, [position]))[0] return (await self.client._query_building_placement_fast(building, [position]))[0] async def can_place(self, building: AbilityData | AbilityId | UnitTypeId, positions: list[Point2]) -> list[bool]: """Tests if a building can be placed in the given locations. Example:: barracks_placement_position = self.main_base_ramp.barracks_correct_placement worker = self.select_build_worker(barracks_placement_position) # Can return None if worker and (await self.can_place(UnitTypeId.BARRACKS, [barracks_placement_position])[0]: worker.build(UnitTypeId.BARRACKS, barracks_placement_position) :param building: :param position:""" if isinstance(building, UnitTypeId): creation_ability = self.game_data.units[building.value].creation_ability if creation_ability is None: return [False for _ in positions] building = creation_ability.id elif isinstance(building, AbilityData): warnings.warn( "Using AbilityData is deprecated and may be removed soon. Please use AbilityId or UnitTypeId instead.", DeprecationWarning, stacklevel=2, ) building = building.id if isinstance(positions, (Point2, tuple)): warnings.warn( "The support for querying single entries will be removed soon. Please use either 'await self.can_place_single(building, position)' or 'await (self.can_place(building, [position]))[0]", DeprecationWarning, stacklevel=2, ) # pyrefly: ignore return await self.can_place_single(building, positions) assert isinstance(positions, list), f"Expected an iterable (list, tuple), but was: {positions}" assert isinstance(positions[0], Point2), ( f"List is expected to have Point2, but instead had: {positions[0]} {type(positions[0])}" ) return await self.client._query_building_placement_fast(building, positions) async def find_placement( self, building: UnitTypeId | AbilityId, near: Point2, max_distance: int = 20, random_alternative: bool = True, placement_step: int = 2, addon_place: bool = False, ) -> Point2 | None: """Finds a placement location for building. Example:: if self.townhalls: cc = self.townhalls[0] depot_position = await self.find_placement(UnitTypeId.SUPPLYDEPOT, near=cc) :param building: :param near: :param max_distance: :param random_alternative: :param placement_step: :param addon_place:""" assert isinstance(building, (AbilityId, UnitTypeId)) assert isinstance(near, Point2), f"{near} is no Point2 object" if isinstance(building, UnitTypeId): creation_ability = self.game_data.units[building.value].creation_ability if creation_ability is None: return None building = creation_ability.id if await self.can_place_single(building, near) and ( not addon_place or await self.can_place_single(AbilityId.TERRANBUILD_SUPPLYDEPOT, near.offset((2.5, -0.5))) ): return near if max_distance == 0: return None for distance in range(placement_step, max_distance, placement_step): possible_positions = [ Point2(p).offset(near).to2 for p in ( [(dx, -distance) for dx in range(-distance, distance + 1, placement_step)] + [(dx, distance) for dx in range(-distance, distance + 1, placement_step)] + [(-distance, dy) for dy in range(-distance, distance + 1, placement_step)] + [(distance, dy) for dy in range(-distance, distance + 1, placement_step)] ) ] res = await self.client._query_building_placement_fast(building, possible_positions) # Filter all positions if building can be placed possible = [p for r, p in zip(res, possible_positions) if r] if addon_place: # Filter remaining positions if addon can be placed res = await self.client._query_building_placement_fast( AbilityId.TERRANBUILD_SUPPLYDEPOT, [p.offset((2.5, -0.5)) for p in possible], ) possible = [p for r, p in zip(res, possible) if r] if not possible: continue if random_alternative: return random.choice(possible) return min(possible, key=lambda p: p.distance_to_point2(near)) return None # TODO: improve using cache per frame def already_pending_upgrade(self, upgrade_type: UpgradeId) -> float: """Check if an upgrade is being researched Returns values are:: 0 # not started 0 < x < 1 # researching 1 # completed Example:: stim_completion_percentage = self.already_pending_upgrade(UpgradeId.STIMPACK) :param upgrade_type: """ assert isinstance(upgrade_type, UpgradeId), f"{upgrade_type} is no UpgradeId" if upgrade_type in self.state.upgrades: return 1 research_ability = self.game_data.upgrades[upgrade_type.value].research_ability if research_ability is None: return 0 creation_ability_id = research_ability.exact_id for structure in self.structures.filter(lambda unit: unit.is_ready): for order in structure.orders: if order.ability.exact_id == creation_ability_id: return order.progress return 0 def structure_type_build_progress(self, structure_type: UnitTypeId | int) -> float: """ Returns the build progress of a structure type. Return range: 0 <= x <= 1 where 0: no such structure exists 0 < x < 1: at least one structure is under construction, returns the progress of the one with the highest progress 1: we have at least one such structure complete Example:: # Assuming you have one barracks building at 0.5 build progress: progress = self.structure_type_build_progress(UnitTypeId.BARRACKS) print(progress) # This prints out 0.5 # If you want to save up money for mutalisks, you can now save up once the spire is nearly completed: spire_almost_completed: bool = self.structure_type_build_progress(UnitTypeId.SPIRE) > 0.75 # If you have a Hive completed but no lair, this function returns 1.0 for the following: self.structure_type_build_progress(UnitTypeId.LAIR) # Assume you have 2 command centers in production, one has 0.5 build_progress and the other 0.2, the following returns 0.5 highest_progress_of_command_center: float = self.structure_type_build_progress(UnitTypeId.COMMANDCENTER) :param structure_type: """ assert isinstance(structure_type, (int, UnitTypeId)), ( f"Needs to be int or UnitTypeId, but was: {type(structure_type)}" ) if isinstance(structure_type, int): structure_type_value: int = structure_type structure_type = UnitTypeId(structure_type_value) else: structure_type_value = structure_type.value assert structure_type_value, f"structure_type can not be 0 or NOTAUNIT, but was: {structure_type_value}" equiv_values: set[int] = {structure_type_value} | { s_type.value for s_type in EQUIVALENTS_FOR_TECH_PROGRESS.get(structure_type, set()) } # SUPPLYDEPOTDROP is not in self.game_data.units, so bot_ai should not check the build progress via creation ability (worker abilities) if structure_type_value not in self.game_data.units: return max((s.build_progress for s in self.structures if s._proto.unit_type in equiv_values), default=0) creation_ability_data = self.game_data.units[structure_type_value].creation_ability if creation_ability_data is None: return 0 creation_ability: AbilityId = creation_ability_data.exact_id max_value = max( [s.build_progress for s in self.structures if s._proto.unit_type in equiv_values] + [self._abilities_count_and_build_progress[1].get(creation_ability, 0)], default=0, ) return max_value def tech_requirement_progress(self, structure_type: UnitTypeId) -> float: """Returns the tech requirement progress for a specific building Example:: # Current state: supply depot is at 50% completion tech_requirement = self.tech_requirement_progress(UnitTypeId.BARRACKS) print(tech_requirement) # Prints 0.5 because supply depot is half way done Example:: # Current state: your bot has one hive, no lair tech_requirement = self.tech_requirement_progress(UnitTypeId.HYDRALISKDEN) print(tech_requirement) # Prints 1 because a hive exists even though only a lair is required Example:: # Current state: One factory is flying and one is half way done tech_requirement = self.tech_requirement_progress(UnitTypeId.STARPORT) print(tech_requirement) # Prints 1 because even though the type id of the flying factory is different, it still has build progress of 1 and thus tech requirement is completed :param structure_type:""" race_dict = { Race.Protoss: PROTOSS_TECH_REQUIREMENT, Race.Terran: TERRAN_TECH_REQUIREMENT, Race.Zerg: ZERG_TECH_REQUIREMENT, } unit_info_id = race_dict[self.race][structure_type] unit_info_id_value = unit_info_id.value # The following commented out line is unreliable for ghost / thor as they return 0 which is incorrect # unit_info_id_value = self.game_data.units[structure_type.value]._proto.tech_requirement if not unit_info_id_value: # Equivalent to "if unit_info_id_value == 0:" return 1 progresses: list[float] = [self.structure_type_build_progress(unit_info_id_value)] for equiv_structure in EQUIVALENTS_FOR_TECH_PROGRESS.get(unit_info_id, []): progresses.append(self.structure_type_build_progress(equiv_structure.value)) return max(progresses) def already_pending(self, unit_type: UpgradeId | UnitTypeId) -> float: """ Returns a number of buildings or units already in progress, or if a worker is en route to build it. This also includes queued orders for workers and build queues of buildings. Example:: amount_of_scv_in_production: int = self.already_pending(UnitTypeId.SCV) amount_of_CCs_in_queue_and_production: int = self.already_pending(UnitTypeId.COMMANDCENTER) amount_of_lairs_morphing: int = self.already_pending(UnitTypeId.LAIR) :param unit_type: """ if isinstance(unit_type, UpgradeId): return self.already_pending_upgrade(unit_type) if unit_type in CREATION_ABILITY_FIX: # Hotfix for checking pending archons and other abilities if unit_type == UnitTypeId.ARCHON: return self._abilities_count_and_build_progress[0][AbilityId.ARCHON_WARP_TARGET] / 2 # Hotfix for rich geysirs return self._abilities_count_and_build_progress[0][CREATION_ABILITY_FIX[unit_type]] creation_ability = self.game_data.units[unit_type.value].creation_ability if creation_ability is None: logger.error(f"Unknown creation_ability in already_pending for unit_type: {unit_type}") return 0 ability_id = creation_ability.exact_id return self._abilities_count_and_build_progress[0][ability_id] def worker_en_route_to_build(self, unit_type: UnitTypeId) -> float: """This function counts how many workers are on the way to start the construction a building. :param unit_type:""" creation_ability = self.game_data.units[unit_type.value].creation_ability if creation_ability is None: logger.error(f"Unknown creation_ability in worker_en_route_to_build for unit_type: {unit_type}") return 0 ability = creation_ability.exact_id return self._worker_orders[ability] @property_cache_once_per_frame def structures_without_construction_SCVs(self) -> Units: """Returns all structures that do not have an SCV constructing it. Warning: this function may move to become a Units filter.""" worker_targets: set[int | Point2] = set() for worker in self.workers: # Ignore repairing workers if not worker.is_constructing_scv: continue for order in worker.orders: # When a construction is resumed, the worker.orders[0].target is the tag of the structure, else it is a Point2 worker_targets.add(order.target) # pyrefly: ignore return self.structures.filter( lambda structure: structure.build_progress < 1 # Redundant check? and structure.type_id in TERRAN_STRUCTURES_REQUIRE_SCV and structure.position not in worker_targets and structure.tag not in worker_targets and structure.tag in self._structures_previous_map and self._structures_previous_map[structure.tag].build_progress == structure.build_progress ) async def build( self, building: UnitTypeId, near: Unit | Point2, max_distance: int = 20, build_worker: Unit | None = None, random_alternative: bool = True, placement_step: int = 2, ) -> bool: """Not recommended as this function checks many positions if it "can place" on them until it found a valid position. Also if the given position is not placeable, this function tries to find a nearby position to place the structure. Then orders the worker to start the construction. :param building: :param near: :param max_distance: :param build_worker: :param random_alternative: :param placement_step:""" assert isinstance(near, (Unit, Point2)) if not self.can_afford(building): return False position = None gas_buildings = {UnitTypeId.EXTRACTOR, UnitTypeId.ASSIMILATOR, UnitTypeId.REFINERY} if isinstance(near, Unit) and building not in gas_buildings: near = near.position if isinstance(near, Point2): near = near.to2 if isinstance(near, Point2): position = await self.find_placement(building, near, max_distance, random_alternative, placement_step) if position is None: return False builder = build_worker or self.select_build_worker(near) if builder is None: return False if building in gas_buildings: assert isinstance(near, Unit) builder.build_gas(near) return True # pyrefly: ignore self.do(builder.build(building, position), subtract_cost=True, ignore_warning=True) return True def train( self, unit_type: UnitTypeId, amount: int = 1, closest_to: Point2 | None = None, train_only_idle_buildings: bool = True, ) -> int: """Trains a specified number of units. Trains only one if amount is not specified. Warning: currently has issues with warp gate warp ins Very generic function. Please use with caution and report any bugs! Example Zerg:: self.train(UnitTypeId.QUEEN, 5) # This should queue 5 queens in 5 different townhalls if you have enough townhalls, enough minerals and enough free supply left Example Terran:: # Assuming you have 2 idle barracks with reactors, one barracks without addon and one with techlab # It should only queue 4 marines in the 2 idle barracks with reactors self.train(UnitTypeId.MARINE, 4) Example distance to:: # If you want to train based on distance to a certain point, you can use "closest_to" self.train(UnitTypeId.MARINE, 4, closest_to = self.game_info.map_center) :param unit_type: :param amount: :param closest_to: :param train_only_idle_buildings:""" # Tech requirement not met if self.tech_requirement_progress(unit_type) < 1: race_dict = { Race.Protoss: PROTOSS_TECH_REQUIREMENT, Race.Terran: TERRAN_TECH_REQUIREMENT, Race.Zerg: ZERG_TECH_REQUIREMENT, } unit_info_id = race_dict[self.race][unit_type] logger.warning( f"{self.time_formatted} Trying to produce unit {unit_type} in self.train() but tech requirement is not met: {unit_info_id}" ) return 0 # Not affordable if not self.can_afford(unit_type): return 0 trained_amount = 0 # All train structure types: queen can made from hatchery, lair, hive train_structure_type: set[UnitTypeId] = UNIT_TRAINED_FROM[unit_type] train_structures = self.structures if self.race != Race.Zerg else self.structures | self.larva requires_techlab = any( TRAIN_INFO[structure_type][unit_type].get("requires_techlab", False) for structure_type in train_structure_type ) is_protoss = self.race == Race.Protoss is_terran = self.race == Race.Terran can_have_addons = any( u in train_structure_type for u in {UnitTypeId.BARRACKS, UnitTypeId.FACTORY, UnitTypeId.STARPORT} ) # Sort structures closest to a point if closest_to is not None: train_structures = train_structures.sorted_by_distance_to(closest_to) elif can_have_addons: # This should sort the structures in ascending order: first structures with reactor, then naked, then with techlab train_structures = train_structures.sorted( key=lambda structure: -1 * (structure.add_on_tag in self.reactor_tags) + 1 * (structure.add_on_tag in self.techlab_tags) ) structure: Unit for structure in train_structures: # Exit early if we can't afford if not self.can_afford(unit_type): return trained_amount if ( # If structure hasn't received an action/order this frame structure.tag not in self.unit_tags_received_action # If structure can train this unit at all and structure.type_id in train_structure_type # Structure has to be completed to be able to train and structure.build_progress == 1 # If structure is protoss, it needs to be powered to train and (not is_protoss or structure.is_powered or structure.type_id == UnitTypeId.NEXUS) # Either parameter "train_only_idle_buildings" is False or structure is idle or structure has less than 2 orders and has reactor and ( not train_only_idle_buildings or len(structure.orders) < 1 + int(structure.add_on_tag in self.reactor_tags) ) # If structure type_id does not accept addons, it cant require a techlab # Else we have to check if building has techlab as addon and (not requires_techlab or structure.add_on_tag in self.techlab_tags) ): # Warp in at location # TODO: find fast warp in locations either random location or closest to the given parameter "closest_to" # TODO: find out which pylons have fast warp in by checking distance to nexus and warpgates.ready if structure.type_id == UnitTypeId.WARPGATE: pylons = self.structures(UnitTypeId.PYLON) location = pylons.random.position.random_on_distance(4) successfully_trained = structure.warp_in(unit_type, location) else: # Normal train a unit from larva or inside a structure successfully_trained = self.do( # pyrefly: ignore structure.train(unit_type), subtract_cost=True, subtract_supply=True, ignore_warning=True, ) # Check if structure has reactor: queue same unit again if ( # Only terran can have reactors is_terran # Check if we have enough cost or supply for this unit type and self.can_afford(unit_type) # Structure needs to be idle in the current frame and not structure.orders # We are at least 2 away from goal and trained_amount + 1 < amount # Unit type does not require techlab and not requires_techlab # Train structure has reactor and structure.add_on_tag in self.reactor_tags ): trained_amount += 1 # With one command queue=False and one queue=True, you can queue 2 marines in a reactored barracks in one frame successfully_trained = self.do( # pyrefly: ignore structure.train(unit_type, queue=True), subtract_cost=True, subtract_supply=True, ignore_warning=True, ) if successfully_trained: trained_amount += 1 if trained_amount == amount: # Target unit train amount reached return trained_amount else: # Some error occured and we couldn't train the unit return trained_amount return trained_amount def research(self, upgrade_type: UpgradeId) -> bool: """ Researches an upgrade from a structure that can research it, if it is idle and powered (protoss). Returns True if the research was started. Return False if the requirement was not met, or the bot did not have enough resources to start the upgrade, or the building to research the upgrade was missing or not idle. New function. Please report any bugs! Example:: # Try to research zergling movement speed if we can afford it # and if at least one pool is at build_progress == 1 # and we are not researching it yet if self.already_pending_upgrade(UpgradeId.ZERGLINGMOVEMENTSPEED) == 0 and self.can_afford(UpgradeId.ZERGLINGMOVEMENTSPEED): spawning_pools_ready = self.structures(UnitTypeId.SPAWNINGPOOL).ready if spawning_pools_ready: self.research(UpgradeId.ZERGLINGMOVEMENTSPEED) :param upgrade_type: """ assert upgrade_type in UPGRADE_RESEARCHED_FROM, ( f"Could not find upgrade {upgrade_type} in 'research from'-dictionary" ) # Not affordable if not self.can_afford(upgrade_type): return False research_structure_type: UnitTypeId = UPGRADE_RESEARCHED_FROM[upgrade_type] # pyrefly: ignore required_tech_building: UnitTypeId | None = RESEARCH_INFO[research_structure_type][upgrade_type].get( "required_building", None ) requirement_met = ( required_tech_building is None or self.structure_type_build_progress(required_tech_building) == 1 ) if not requirement_met: return False is_protoss = self.race == Race.Protoss # All upgrades right now that can be researched in spire and hatch can also be researched in their morphs equiv_structures = { UnitTypeId.SPIRE: {UnitTypeId.SPIRE, UnitTypeId.GREATERSPIRE}, UnitTypeId.GREATERSPIRE: {UnitTypeId.SPIRE, UnitTypeId.GREATERSPIRE}, UnitTypeId.HATCHERY: {UnitTypeId.HATCHERY, UnitTypeId.LAIR, UnitTypeId.HIVE}, UnitTypeId.LAIR: {UnitTypeId.HATCHERY, UnitTypeId.LAIR, UnitTypeId.HIVE}, UnitTypeId.HIVE: {UnitTypeId.HATCHERY, UnitTypeId.LAIR, UnitTypeId.HIVE}, } # Convert to a set, or equivalent structures are chosen # Overlord speed upgrade can be researched from hatchery, lair or hive research_structure_types: set[UnitTypeId] = equiv_structures.get( research_structure_type, {research_structure_type} ) structure: Unit for structure in self.structures: if ( # Structure can research this upgrade structure.type_id in research_structure_types # If structure hasn't received an action/order this frame and structure.tag not in self.unit_tags_received_action # Structure is ready / completed and structure.is_ready # Structure is idle and structure.is_idle # Structure belongs to protoss and is powered (near pylon) and (not is_protoss or structure.is_powered) ): # Can_afford check was already done earlier in this function successful_action: bool = self.do( # pyrefly: ignore structure.research(upgrade_type), subtract_cost=True, ignore_warning=True, ) return successful_action return False async def chat_send(self, message: str, team_only: bool = False) -> None: """Send a chat message to the SC2 Client. Example:: await self.chat_send("Hello, this is a message from my bot!") :param message: :param team_only:""" assert isinstance(message, str), f"{message} is not a string" await self.client.chat_send(message, team_only) def in_map_bounds(self, pos: Point2 | tuple[float, float] | list[float]) -> bool: """Tests if a 2 dimensional point is within the map boundaries of the pixelmaps. :param pos:""" return ( self.game_info.playable_area.x <= pos[0] < self.game_info.playable_area.x + self.game_info.playable_area.width and self.game_info.playable_area.y <= pos[1] < self.game_info.playable_area.y + self.game_info.playable_area.height ) # For the functions below, make sure you are inside the boundaries of the map size. def get_terrain_height(self, pos: Point2 | Unit) -> int: """Returns terrain height at a position. Caution: terrain height is different from a unit's z-coordinate. :param pos:""" assert isinstance(pos, (Point2, Unit)), "pos is not of type Point2 or Unit" pos = pos.position.rounded return self.game_info.terrain_height[pos] def get_terrain_z_height(self, pos: Point2 | Unit) -> float: """Returns terrain z-height at a position. :param pos:""" assert isinstance(pos, (Point2, Unit)), "pos is not of type Point2 or Unit" pos = pos.position.rounded return -16 + 32 * self.game_info.terrain_height[pos] / 255 def in_placement_grid(self, pos: Point2 | Unit) -> bool: """Returns True if you can place something at a position. Remember, buildings usually use 2x2, 3x3 or 5x5 of these grid points. Caution: some x and y offset might be required, see ramp code in game_info.py :param pos:""" assert isinstance(pos, (Point2, Unit)), "pos is not of type Point2 or Unit" pos = pos.position.rounded return self.game_info.placement_grid[pos] == 1 def in_pathing_grid(self, pos: Point2 | Unit) -> bool: """Returns True if a ground unit can pass through a grid point. :param pos:""" assert isinstance(pos, (Point2, Unit)), "pos is not of type Point2 or Unit" pos = pos.position.rounded return self.game_info.pathing_grid[pos] == 1 def is_visible(self, pos: Point2 | Unit) -> bool: """Returns True if you have vision on a grid point. :param pos:""" # more info: https://github.com/Blizzard/s2client-proto/blob/9906df71d6909511907d8419b33acc1a3bd51ec0/s2clientprotocol/spatial.proto#L19 assert isinstance(pos, (Point2, Unit)), "pos is not of type Point2 or Unit" pos = pos.position.rounded return self.state.visibility[pos] == 2 def has_creep(self, pos: Point2 | Unit) -> bool: """Returns True if there is creep on the grid point. :param pos:""" assert isinstance(pos, (Point2, Unit)), "pos is not of type Point2 or Unit" pos = pos.position.rounded return self.state.creep[pos] == 1 async def on_unit_destroyed(self, unit_tag: int) -> None: """ Override this in your bot class. Note that this function uses unit tags and not the unit objects because the unit does not exist any more. This will event will be called when a unit (or structure, friendly or enemy) dies. For enemy units, this only works if the enemy unit was in vision on death. :param unit_tag: """ async def on_unit_created(self, unit: Unit) -> None: """Override this in your bot class. This function is called when a unit is created. :param unit:""" async def on_unit_type_changed(self, unit: Unit, previous_type: UnitTypeId) -> None: """Override this in your bot class. This function is called when a unit type has changed. To get the current UnitTypeId of the unit, use 'unit.type_id' This may happen when a larva morphed to an egg, siege tank sieged, a zerg unit burrowed, a hatchery morphed to lair, a corruptor morphed to broodlordcocoon, etc.. Examples:: print(f"My unit changed type: {unit} from {previous_type} to {unit.type_id}") :param unit: :param previous_type: """ async def on_building_construction_started(self, unit: Unit) -> None: """ Override this in your bot class. This function is called when a building construction has started. :param unit: """ async def on_building_construction_complete(self, unit: Unit) -> None: """ Override this in your bot class. This function is called when a building construction is completed. :param unit: """ async def on_upgrade_complete(self, upgrade: UpgradeId) -> None: """ Override this in your bot class. This function is called with the upgrade id of an upgrade that was not finished last step and is now. :param upgrade: """ async def on_unit_took_damage(self, unit: Unit, amount_damage_taken: float) -> None: """ Override this in your bot class. This function is called when your own unit (unit or structure) took damage. It will not be called if the unit died this frame. This may be called frequently for terran structures that are burning down, or zerg buildings that are off creep, or terran bio units that just used stimpack ability. TODO: If there is a demand for it, then I can add a similar event for when enemy units took damage Examples:: print(f"My unit took damage: {unit} took {amount_damage_taken} damage") :param unit: :param amount_damage_taken: """ async def on_enemy_unit_entered_vision(self, unit: Unit) -> None: """ Override this in your bot class. This function is called when an enemy unit (unit or structure) entered vision (which was not visible last frame). :param unit: """ async def on_enemy_unit_left_vision(self, unit_tag: int) -> None: """ Override this in your bot class. This function is called when an enemy unit (unit or structure) left vision (which was visible last frame). Same as the self.on_unit_destroyed event, this function is called with the unit's tag because the unit is no longer visible anymore. If you want to store a snapshot of the unit, use self._enemy_units_previous_map[unit_tag] for units or self._enemy_structures_previous_map[unit_tag] for structures. Examples:: last_known_unit = self._enemy_units_previous_map.get(unit_tag, None) or self._enemy_structures_previous_map[unit_tag] print(f"Enemy unit left vision, last known location: {last_known_unit.position}") :param unit_tag: """ async def on_before_start(self) -> None: """ Override this in your bot class. This function is called before "on_start" and before "prepare_first_step" that calculates expansion locations. Not all data is available yet. This function is useful in realtime=True mode to split your workers or start producing the first worker. """ async def on_start(self) -> None: """ Override this in your bot class. At this point, game_data, game_info and the first iteration of game_state (self.state) are available. """ async def on_step(self, iteration: int): """ You need to implement this function! Override this in your bot class. This function is called on every game step (looped in realtime mode). :param iteration: """ raise NotImplementedError async def on_end(self, game_result: Result) -> None: """Override this in your bot class. This function is called at the end of a game. Unsure if this function will be called on the laddermanager client as the bot process may forcefully be terminated. :param game_result:""" ================================================ FILE: sc2/bot_ai_internal.py ================================================ from __future__ import annotations import itertools import math import time import warnings from abc import ABC from collections import Counter from collections.abc import Generator, Iterable from contextlib import suppress from typing import TYPE_CHECKING, Any, final import numpy as np from loguru import logger from s2clientprotocol import sc2api_pb2 as sc_pb from sc2.cache import property_cache_once_per_frame from sc2.constants import ( ALL_GAS, CREATION_ABILITY_FIX, IS_PLACEHOLDER, TERRAN_STRUCTURES_REQUIRE_SCV, FakeEffectID, abilityid_to_unittypeid, geyser_ids, mineral_ids, ) from sc2.data import ActionResult, Race, race_townhalls from sc2.game_data import Cost, GameData from sc2.game_state import Blip, EffectData, GameState from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId from sc2.ids.upgrade_id import UpgradeId from sc2.pixel_map import PixelMap from sc2.position import Point2, _PointLike from sc2.unit import Unit from sc2.unit_command import UnitCommand from sc2.units import Units with warnings.catch_warnings(): warnings.simplefilter("ignore") from scipy.spatial.distance import cdist, pdist if TYPE_CHECKING: from sc2.client import Client from sc2.game_info import GameInfo from sc2.bot_ai import BotAI class BotAIInternal(ABC): """Base class for bots.""" def __init__(self: BotAI) -> None: self._initialize_variables() @final def _initialize_variables(self: BotAI) -> None: """Called from main.py internally""" self.cache: dict[str, Any] = {} # Specific opponent bot ID used in sc2ai ladder games http://sc2ai.net/ and on ai arena https://aiarena.net # The bot ID will stay the same each game so your bot can "adapt" to the opponent if not hasattr(self, "opponent_id"): # Prevent overwriting the opponent_id which is set here https://github.com/Hannessa/python-sc2-ladderbot/blob/master/__init__.py#L40 # otherwise set it to None self.opponent_id: str | None = None # Select distance calculation method, see _distances_override_functions function if not hasattr(self, "distance_calculation_method"): self.distance_calculation_method: int = 2 # Select if the Unit.command should return UnitCommand objects. Set this to True if your bot uses 'unit(ability, target)' if not hasattr(self, "unit_command_uses_self_do"): self.unit_command_uses_self_do: bool = False # This value will be set to True by main.py in self._prepare_start if game is played in realtime (if true, the bot will have limited time per step) self.realtime: bool = False self.base_build: int = -1 self.all_units: Units = Units([], self) self.units: Units = Units([], self) self.workers: Units = Units([], self) self.larva: Units = Units([], self) self.structures: Units = Units([], self) self.townhalls: Units = Units([], self) self.gas_buildings: Units = Units([], self) self.all_own_units: Units = Units([], self) self.enemy_units: Units = Units([], self) self.enemy_structures: Units = Units([], self) self.all_enemy_units: Units = Units([], self) self.resources: Units = Units([], self) self.destructables: Units = Units([], self) self.watchtowers: Units = Units([], self) self.mineral_field: Units = Units([], self) self.vespene_geyser: Units = Units([], self) self.placeholders: Units = Units([], self) self.techlab_tags: set[int] = set() self.reactor_tags: set[int] = set() self.minerals: int = 50 self.vespene: int = 0 self.supply_army: float = 0 self.supply_workers: float = 12 # Doesn't include workers in production self.supply_cap: float = 15 self.supply_used: float = 12 self.supply_left: float = 3 self.idle_worker_count: int = 0 self.army_count: int = 0 self.warp_gate_count: int = 0 self.actions: list[UnitCommand] = [] self.blips: set[Blip] = set() # Will be set on AbstractPlayer init self.race: Race = None # pyrefly: ignore self.enemy_race: Race | None = None self._generated_frame = -100 self._units_created: Counter = Counter() self._unit_tags_seen_this_game: set[int] = set() self._units_previous_map: dict[int, Unit] = {} self._structures_previous_map: dict[int, Unit] = {} self._enemy_units_previous_map: dict[int, Unit] = {} self._enemy_structures_previous_map: dict[int, Unit] = {} self._all_units_previous_map: dict[int, Unit] = {} self._previous_upgrades: set[UpgradeId] = set() self._expansion_positions_list: list[Point2] = [] self._resource_location_to_expansion_position_dict: dict[Point2, set[Point2]] = {} self._time_before_step: float = 0 self._time_after_step: float = 0 self._min_step_time: float = math.inf self._max_step_time: float = 0 self._last_step_step_time: float = 0 self._total_time_in_on_step: float = 0 self._total_steps_iterations: int = 0 # Internally used to keep track which units received an action in this frame, so that self.train() function does not give the same larva two orders - cleared every frame self.unit_tags_received_action: set[int] = set() @final @property def _game_info(self) -> GameInfo: """See game_info.py""" warnings.warn( "Using self._game_info is deprecated and may be removed soon. Please use self.game_info directly.", DeprecationWarning, stacklevel=2, ) return self.game_info @final @property def _game_data(self) -> GameData: """See game_data.py""" warnings.warn( "Using self._game_data is deprecated and may be removed soon. Please use self.game_data directly.", DeprecationWarning, stacklevel=2, ) return self.game_data @final @property def _client(self) -> Client: """See client.py""" warnings.warn( "Using self._client is deprecated and may be removed soon. Please use self.client directly.", DeprecationWarning, stacklevel=2, ) return self.client @final @property_cache_once_per_frame def expansion_locations(self: BotAI) -> dict[Point2, Units]: """Same as the function above.""" assert self._expansion_positions_list, ( "self._find_expansion_locations() has not been run yet, so accessing the list of expansion locations is pointless." ) warnings.warn( "You are using 'self.expansion_locations', please use 'self.expansion_locations_list' (fast) or 'self.expansion_locations_dict' (slow) instead.", DeprecationWarning, stacklevel=2, ) return self.expansion_locations_dict def _cluster_center(self, group: list[Unit]) -> Point2: """ Calculates the geometric center (centroid) of a given group of units. Parameters: group: A list of Unit objects representing the group of units for which the center is to be calculated. Raises: ValueError: If the provided group is empty. Returns: Point2: The calculated centroid of the group as a Point2 object. """ if not group: raise ValueError("Cannot calculate center of empty group") total_x: float = 0 total_y: float = 0 for unit in group: total_x += unit.position.x total_y += unit.position.y count = len(group) return Point2((total_x / count, total_y / count)) def _find_expansion_location( self, resources: Units | list[Unit], amount: int, offsets: list[tuple[float, float]] ) -> Point2: """ Finds the most suitable expansion location for resources. Parameters: resources: The list of resource entities or units near which the expansion location needs to be found. amount: The total number of resource entities or units to consider. offsets (list[tuple[float, float]): A list of coordinate pairs denoting position offsets to consider around the center of resources. Returns: The calculated optimal expansion Point2 if a suitable position is found; otherwise, None. """ # Normal single expansion logic for regular bases # Calculate center, round and add 0.5 because expansion location will have (x.5, y.5) # coordinates because bases have size 5. center_x = int(sum(resource.position.x for resource in resources) / amount) + 0.5 center_y = int(sum(resource.position.y for resource in resources) / amount) + 0.5 possible_points = (Point2((offset[0] + center_x, offset[1] + center_y)) for offset in offsets) # Filter out points that are too near possible_points = [ point for point in possible_points # Check if point can be built on if self.game_info.placement_grid[point.rounded] == 1 # Check if all resources have enough space to point and all( point.distance_to(resource) >= (7 if resource._proto.unit_type in geyser_ids else 6) for resource in resources ) ] # Choose best fitting point result: Point2 = min( possible_points, key=lambda point: sum(point.distance_to(resource_) for resource_ in resources) ) return result def _has_opposite_side_geyser_layout(self, minerals: list[Unit], gas_geysers: list[Unit]) -> bool: """ Determines whether the gas geysers have an opposite-side mineral line layout. The method evaluates if two gas geysers are located on opposite sides of a mineral line. If this returns True we consider this location has 2 valid expansion locations either side of the mineral line. Parameters: minerals: A list of mineral fields at this location. gas_geysers : list[Unit] A list of gas geysers at this location. Returns: bool True if the geysers fulfill the opposite-side layout condition with respect to the mineral line, otherwise False. """ # Need exactly 2 geysers and enough minerals for a line if len(gas_geysers) != 2 or len(minerals) < 6: return False # Find the two minerals that are furthest apart max_distance: float = 0.0 mineral_1: Unit = minerals[0] mineral_2: Unit = minerals[1] for i, m1 in enumerate(minerals): for m2 in minerals[i + 1 :]: distance = m1.distance_to(m2) if distance > max_distance: max_distance = distance mineral_1 = m1 mineral_2 = m2 # ensure line is long enough if max_distance < 4: return False # Create line from the two furthest minerals x1, y1 = mineral_1.position.x, mineral_1.position.y x2, y2 = mineral_2.position.x, mineral_2.position.y geyser_1, geyser_2 = gas_geysers # Check if the mineral line is more vertical than horizontal if abs(x2 - x1) < 0.1: # Vertical line: use x-coordinate to determine sides line_x = (x1 + x2) / 2 side_1 = geyser_1.position.x - line_x side_2 = geyser_2.position.x - line_x # Must be on opposite sides and far enough from the line return side_1 * side_2 < 0 and abs(side_1) > 3 and abs(side_2) > 3 # Calculate line equation: y = mx + b slope = (y2 - y1) / (x2 - x1) intercept = y1 - slope * x1 # Function to determine which side of the line a point is on def side_of_line(point: Point2) -> float: return point.y - slope * point.x - intercept side_1 = side_of_line(geyser_1.position) side_2 = side_of_line(geyser_2.position) # Check if geysers are on opposite sides opposite_sides = side_1 * side_2 < 0 return opposite_sides @final def _find_expansion_locations(self) -> None: """Ran once at the start of the game to calculate expansion locations.""" # Idea: create a group for every resource, then merge these groups if # any resource in a group is closer than a threshold to any resource of another group # Distance we group resources by resource_spread_threshold: float = 10.5 # Create a group for every resource resource_groups: list[list[Unit]] = [ [resource] for resource in self.resources if resource.name != "MineralField450" # dont use low mineral count patches ] # Loop the merging process as long as we change something merged_group = True height_grid: PixelMap = self.game_info.terrain_height while merged_group: merged_group = False # Check every combination of two groups for group_a, group_b in itertools.combinations(resource_groups, 2): # Check if any pair of resource of these groups is closer than threshold together # And that they are on the same terrain level center_a = self._cluster_center(group_a) center_b = self._cluster_center(group_b) if center_a.distance_to(center_b) <= resource_spread_threshold and all( abs(height_grid[res_a.position.rounded] - height_grid[res_b.position.rounded]) <= 10 for res_a in group_a for res_b in group_b ): # Remove the single groups and add the merged group resource_groups.remove(group_a) resource_groups.remove(group_b) resource_groups.append(group_a + group_b) merged_group = True break # Distance offsets we apply to center of each resource group to find expansion position offset_range: int = 7 offsets: list[tuple[float, float]] = [ (x, y) for x, y in itertools.product(range(-offset_range, offset_range + 1), repeat=2) if 4 < math.hypot(x, y) <= 8 ] # Dict we want to return centers = {} # For every resource group: for resources in resource_groups: # Possible expansion points amount = len(resources) # this check is needed for TorchesAIE where the gold mineral wall has a # unit type of `RichMineralField` so we can only filter out by amount of resources if amount > 12: continue minerals = [r for r in resources if r._proto.unit_type not in geyser_ids] gas_geysers = [r for r in resources if r._proto.unit_type in geyser_ids] # Check if we have exactly 2 gas geysers positioned above/below the mineral line # Needed for TorchesAIE where one gold base has 2 expansion locations if self._has_opposite_side_geyser_layout(minerals, gas_geysers): # Create expansion locations for each geyser + minerals for geyser in gas_geysers: local_resources = minerals + [geyser] result: Point2 = self._find_expansion_location(local_resources, len(local_resources), offsets) centers[result] = local_resources # Put all expansion locations in a list self._expansion_positions_list.append(result) # Maps all resource positions to the expansion position for resource in local_resources: if resource.position in self._resource_location_to_expansion_position_dict: self._resource_location_to_expansion_position_dict[resource.position].add(result) else: self._resource_location_to_expansion_position_dict[resource.position] = {result} continue # Choose best fitting point result: Point2 = self._find_expansion_location(resources, amount, offsets) centers[result] = resources # Put all expansion locations in a list self._expansion_positions_list.append(result) # Maps all resource positions to the expansion position for resource in resources: self._resource_location_to_expansion_position_dict[resource.position] = {result} @final def _correct_zerg_supply(self) -> None: """The client incorrectly rounds zerg supply down instead of up (see https://github.com/Blizzard/s2client-proto/issues/123), so self.supply_used and friends return the wrong value when there are an odd number of zerglings and banelings. This function corrects the bad values.""" # TODO: remove when Blizzard/sc2client-proto#123 gets fixed. half_supply_units = { UnitTypeId.ZERGLING, UnitTypeId.ZERGLINGBURROWED, UnitTypeId.BANELING, UnitTypeId.BANELINGBURROWED, UnitTypeId.BANELINGCOCOON, } correction = self.units(half_supply_units).amount % 2 self.supply_used += correction self.supply_army += correction self.supply_left -= correction @final @property_cache_once_per_frame def _abilities_count_and_build_progress(self) -> tuple[Counter[AbilityId], dict[AbilityId, float]]: """Cache for the already_pending function, includes protoss units warping in, all units in production and all structures, and all morphs""" abilities_amount: Counter[AbilityId] = Counter() max_build_progress: dict[AbilityId, float] = {} unit: Unit for unit in self.units + self.structures: for order in unit.orders: abilities_amount[order.ability.exact_id] += 1 if not unit.is_ready and (self.race != Race.Terran or not unit.is_structure): # If an SCV is constructing a building, already_pending would count this structure twice # (once from the SCV order, and once from "not structure.is_ready") if unit.type_id in CREATION_ABILITY_FIX: if unit.type_id == UnitTypeId.ARCHON: # Hotfix for archons in morph state creation_ability_id = AbilityId.ARCHON_WARP_TARGET abilities_amount[creation_ability_id] += 2 else: # Hotfix for rich geysirs creation_ability_id = CREATION_ABILITY_FIX[unit.type_id] abilities_amount[creation_ability_id] += 1 else: creation_ability = self.game_data.units[unit.type_id.value].creation_ability if creation_ability is None: continue creation_ability_id = creation_ability.exact_id abilities_amount[creation_ability_id] += 1 max_build_progress[creation_ability_id] = max( max_build_progress.get(creation_ability_id, 0), unit.build_progress ) return abilities_amount, max_build_progress @final @property_cache_once_per_frame def _worker_orders(self) -> Counter[AbilityId]: """This function is used internally, do not use! It is to store all worker abilities.""" abilities_amount: Counter[AbilityId] = Counter() structures_in_production: set[Point2 | int] = set() for structure in self.structures: if structure.type_id in TERRAN_STRUCTURES_REQUIRE_SCV: structures_in_production.add(structure.position) structures_in_production.add(structure.tag) for worker in self.workers: for order in worker.orders: # Skip if the SCV is constructing (not isinstance(order.target, int)) # or resuming construction (isinstance(order.target, int)) if order.target in structures_in_production: continue abilities_amount[order.ability.exact_id] += 1 return abilities_amount @final def do( self: BotAI, action: UnitCommand, subtract_cost: bool = False, subtract_supply: bool = False, can_afford_check: bool = False, ignore_warning: bool = False, ) -> bool: """Adds a unit action to the 'self.actions' list which is then executed at the end of the frame. Training a unit:: # Train an SCV from a random idle command center cc = self.townhalls.idle.random_or(None) # self.townhalls can be empty or there are no idle townhalls if cc and self.can_afford(UnitTypeId.SCV): cc.train(UnitTypeId.SCV) Building a building:: # Building a barracks at the main ramp, requires 150 minerals and a depot worker = self.workers.random_or(None) barracks_placement_position = self.main_base_ramp.barracks_correct_placement if worker and self.can_afford(UnitTypeId.BARRACKS): worker.build(UnitTypeId.BARRACKS, barracks_placement_position) Moving a unit:: # Move a random worker to the center of the map worker = self.workers.random_or(None) # worker can be None if all are dead if worker: worker.move(self.game_info.map_center) :param action: :param subtract_cost: :param subtract_supply: :param can_afford_check: """ if not self.unit_command_uses_self_do and isinstance(action, bool): if not ignore_warning: warnings.warn( "You have used self.do(). Please consider putting 'self.unit_command_uses_self_do = True' in your bot __init__() function or removing self.do().", DeprecationWarning, stacklevel=2, ) return action assert isinstance(action, UnitCommand), ( f"Given unit command is not a command, but instead of type {type(action)}" ) if subtract_cost: cost: Cost = self.game_data.calculate_ability_cost(action.ability) if can_afford_check and not (self.minerals >= cost.minerals and self.vespene >= cost.vespene): # Dont do action if can't afford return False self.minerals -= cost.minerals self.vespene -= cost.vespene if subtract_supply and action.ability in abilityid_to_unittypeid: unit_type = abilityid_to_unittypeid[action.ability] required_supply = self.calculate_supply_cost(unit_type) # Overlord has -8 if required_supply > 0: self.supply_used += required_supply self.supply_left -= required_supply self.actions.append(action) self.unit_tags_received_action.add(action.unit.tag) return True @final async def synchronous_do(self: BotAI, action: UnitCommand): """ Not recommended. Use self.do instead to reduce lag. This function is only useful for realtime=True in the first frame of the game to instantly produce a worker and split workers on the mineral patches. """ assert isinstance(action, UnitCommand), ( f"Given unit command is not a command, but instead of type {type(action)}" ) if not self.can_afford(action.ability): logger.warning(f"Cannot afford action {action}") return ActionResult.Error r: ActionResult = await self.client.actions(action) # pyrefly: ignore if not r: # success cost = self.game_data.calculate_ability_cost(action.ability) self.minerals -= cost.minerals self.vespene -= cost.vespene self.unit_tags_received_action.add(action.unit.tag) else: logger.error(f"Error: {r} (action: {action})") return r @final async def _do_actions(self, actions: list[UnitCommand], prevent_double: bool = True): """Used internally by main.py after each step :param actions: :param prevent_double:""" if not actions: return None if prevent_double: actions = list(filter(self.prevent_double_actions, actions)) result = await self.client.actions(actions) return result @final @staticmethod def prevent_double_actions(action: UnitCommand) -> bool: """ :param action: """ # Always add actions if queued if action.queue: return True if action.unit.orders: # action: UnitCommand # current_action: UnitOrder current_action = action.unit.orders[0] if action.ability not in {current_action.ability.id, current_action.ability.exact_id}: # Different action, return True return True with suppress(AttributeError): if current_action.target == action.target.tag: # pyrefly: ignore # Same action, remove action if same target unit return False with suppress(AttributeError): if ( # pyrefly: ignore action.target.x == current_action.target.x and action.target.y == current_action.target.y ): # Same action, remove action if same target position return False return True return True @final def _prepare_start( self, client: Client, player_id: int, game_info: GameInfo, game_data: GameData, realtime: bool = False, base_build: int = -1, ) -> None: """ Ran until game start to set game and player data. :param client: :param player_id: :param game_info: :param game_data: :param realtime: """ self.client: Client = client self.player_id: int = player_id self.game_info: GameInfo = game_info self.game_data: GameData = game_data self.realtime: bool = realtime self.base_build: int = base_build # Get the player's race. As observer, get Race.NoRace=0 self.race: Race = Race(self.game_info.player_races.get(self.player_id, 0)) # Get the enemy's race only if we are not observer (replay) and the game has 2 players if self.player_id > 0 and len(self.game_info.player_races) == 2: self.enemy_race: Race = Race(self.game_info.player_races[3 - self.player_id]) self._distances_override_functions(self.distance_calculation_method) @final def _prepare_first_step(self) -> None: """First step extra preparations. Must not be called before _prepare_step.""" if self.townhalls: self.game_info.player_start_location = self.townhalls.first.position # Calculate and cache expansion locations forever inside 'self._cache_expansion_locations', this is done to prevent a bug when this is run and cached later in the game self._find_expansion_locations() self.game_info.map_ramps, self.game_info.vision_blockers = self.game_info._find_ramps_and_vision_blockers() self._time_before_step: float = time.perf_counter() @final def _prepare_step(self: BotAI, state: GameState, proto_game_info: sc_pb.Response) -> None: """ :param state: :param proto_game_info: """ # Set attributes from new state before on_step.""" self.state = state # See game_state.py # update pathing grid, which unfortunately is in GameInfo instead of GameState self.game_info.pathing_grid = PixelMap(proto_game_info.game_info.start_raw.pathing_grid, in_bits=True) # Required for events, needs to be before self.units are initialized so the old units are stored self._units_previous_map: dict[int, Unit] = {unit.tag: unit for unit in self.units} self._structures_previous_map: dict[int, Unit] = {structure.tag: structure for structure in self.structures} self._enemy_units_previous_map: dict[int, Unit] = {unit.tag: unit for unit in self.enemy_units} self._enemy_structures_previous_map: dict[int, Unit] = { structure.tag: structure for structure in self.enemy_structures } self._all_units_previous_map: dict[int, Unit] = {unit.tag: unit for unit in self.all_units} self._prepare_units() self.minerals: int = state.common.minerals self.vespene: int = state.common.vespene self.supply_army: int = state.common.food_army self.supply_workers: int = state.common.food_workers # Doesn't include workers in production self.supply_cap: int = state.common.food_cap self.supply_used: int = state.common.food_used self.supply_left: int = self.supply_cap - self.supply_used if self.race == Race.Zerg: # Workaround Zerg supply rounding bug self._correct_zerg_supply() elif self.race == Race.Protoss: self.warp_gate_count: int = state.common.warp_gate_count self.idle_worker_count: int = state.common.idle_worker_count self.army_count: int = state.common.army_count self._time_before_step: float = time.perf_counter() if self.enemy_race == Race.Random and self.all_enemy_units: self.enemy_race = Race(self.all_enemy_units.first.race) @final def _prepare_units(self: BotAI) -> None: # Set of enemy units detected by own sensor tower, as blips have less unit information than normal visible units self.blips: set[Blip] = set() self.all_units: Units = Units([], self) self.units: Units = Units([], self) self.workers: Units = Units([], self) self.larva: Units = Units([], self) self.structures: Units = Units([], self) self.townhalls: Units = Units([], self) self.gas_buildings: Units = Units([], self) self.all_own_units: Units = Units([], self) self.enemy_units: Units = Units([], self) self.enemy_structures: Units = Units([], self) self.all_enemy_units: Units = Units([], self) self.resources: Units = Units([], self) self.destructables: Units = Units([], self) self.watchtowers: Units = Units([], self) self.mineral_field: Units = Units([], self) self.vespene_geyser: Units = Units([], self) self.placeholders: Units = Units([], self) self.techlab_tags: set[int] = set() self.reactor_tags: set[int] = set() worker_types: set[UnitTypeId] = {UnitTypeId.DRONE, UnitTypeId.DRONEBURROWED, UnitTypeId.SCV, UnitTypeId.PROBE} index: int = 0 for unit in self.state.observation_raw.units: if unit.is_blip: self.blips.add(Blip(unit)) else: unit_type: int = unit.unit_type # Convert these units to effects: reaper grenade, parasitic bomb dummy, forcefield if unit_type in FakeEffectID: self.state.effects.add(EffectData(unit, fake=True)) continue unit_obj = Unit(unit, self, distance_calculation_index=index, base_build=self.base_build) index += 1 self.all_units.append(unit_obj) if unit.display_type == IS_PLACEHOLDER: self.placeholders.append(unit_obj) continue alliance = unit.alliance # Alliance.Neutral.value = 3 if alliance == 3: # XELNAGATOWER = 149 if unit_type == 149: self.watchtowers.append(unit_obj) # mineral field enums elif unit_type in mineral_ids: self.mineral_field.append(unit_obj) self.resources.append(unit_obj) # geyser enums elif unit_type in geyser_ids: self.vespene_geyser.append(unit_obj) self.resources.append(unit_obj) # all destructable rocks else: self.destructables.append(unit_obj) # Alliance.Self.value = 1 elif alliance == 1: self.all_own_units.append(unit_obj) unit_id: UnitTypeId = unit_obj.type_id if unit_obj.is_structure: self.structures.append(unit_obj) if unit_id in race_townhalls[self.race]: self.townhalls.append(unit_obj) elif unit_id in ALL_GAS or unit_obj.vespene_contents: # TODO: remove "or unit_obj.vespene_contents" when a new linux client newer than version 4.10.0 is released self.gas_buildings.append(unit_obj) elif unit_id in { UnitTypeId.TECHLAB, UnitTypeId.BARRACKSTECHLAB, UnitTypeId.FACTORYTECHLAB, UnitTypeId.STARPORTTECHLAB, }: self.techlab_tags.add(unit_obj.tag) elif unit_id in { UnitTypeId.REACTOR, UnitTypeId.BARRACKSREACTOR, UnitTypeId.FACTORYREACTOR, UnitTypeId.STARPORTREACTOR, }: self.reactor_tags.add(unit_obj.tag) else: self.units.append(unit_obj) if unit_id in worker_types: self.workers.append(unit_obj) elif unit_id == UnitTypeId.LARVA: self.larva.append(unit_obj) # Alliance.Enemy.value = 4 elif alliance == 4: self.all_enemy_units.append(unit_obj) if unit_obj.is_structure: self.enemy_structures.append(unit_obj) else: self.enemy_units.append(unit_obj) # Force distance calculation and caching on all units using scipy pdist or cdist if self.distance_calculation_method == 1: _ = self._pdist elif self.distance_calculation_method in {2, 3}: _ = self._cdist @final async def _after_step(self) -> int: """Executed by main.py after each on_step function.""" # Keep track of the bot on_step duration self._time_after_step: float = time.perf_counter() step_duration = self._time_after_step - self._time_before_step self._min_step_time = min(step_duration, self._min_step_time) self._max_step_time = max(step_duration, self._max_step_time) self._last_step_step_time = step_duration self._total_time_in_on_step += step_duration self._total_steps_iterations += 1 # Commit and clear bot actions if self.actions: await self._do_actions(self.actions) self.actions.clear() # Clear set of unit tags that were given an order this frame self.unit_tags_received_action.clear() # Commit debug queries await self.client._send_debug() return self.state.game_loop @final async def _advance_steps(self: BotAI, steps: int) -> None: """Advances the game loop by amount of 'steps'. This function is meant to be used as a debugging and testing tool only. If you are using this, please be aware of the consequences, e.g. 'self.units' will be filled with completely new data.""" await self._after_step() # Advance simulation by exactly "steps" frames await self.client.step(steps) state = await self.client.observation() gs = GameState(state.observation) proto_game_info = await self.client._execute(game_info=sc_pb.RequestGameInfo()) self._prepare_step(gs, proto_game_info) await self.issue_events() @final async def issue_events(self: BotAI) -> None: """This function will be automatically run from main.py and triggers the following functions: - on_unit_created - on_unit_destroyed - on_building_construction_started - on_building_construction_complete - on_upgrade_complete """ await self._issue_unit_dead_events() await self._issue_unit_added_events() await self._issue_building_events() await self._issue_upgrade_events() await self._issue_vision_events() @final async def _issue_unit_added_events(self: BotAI) -> None: for unit in self.units: if unit.tag not in self._units_previous_map and unit.tag not in self._unit_tags_seen_this_game: self._unit_tags_seen_this_game.add(unit.tag) self._units_created[unit.type_id] += 1 await self.on_unit_created(unit) elif unit.tag in self._units_previous_map: previous_frame_unit: Unit = self._units_previous_map[unit.tag] # Check if a unit took damage this frame and then trigger event if unit.health < previous_frame_unit.health or unit.shield < previous_frame_unit.shield: damage_amount = previous_frame_unit.health - unit.health + previous_frame_unit.shield - unit.shield await self.on_unit_took_damage(unit, damage_amount) # Check if a unit type has changed if previous_frame_unit.type_id != unit.type_id: await self.on_unit_type_changed(unit, previous_frame_unit.type_id) @final async def _issue_upgrade_events(self: BotAI) -> None: difference = self.state.upgrades - self._previous_upgrades for upgrade_completed in difference: await self.on_upgrade_complete(upgrade_completed) self._previous_upgrades = self.state.upgrades @final async def _issue_building_events(self: BotAI) -> None: for structure in self.structures: if structure.tag not in self._structures_previous_map: if structure.build_progress < 1: await self.on_building_construction_started(structure) else: # Include starting townhall self._units_created[structure.type_id] += 1 await self.on_building_construction_complete(structure) elif structure.tag in self._structures_previous_map: # Check if a structure took damage this frame and then trigger event previous_frame_structure: Unit = self._structures_previous_map[structure.tag] if ( structure.health < previous_frame_structure.health or structure.shield < previous_frame_structure.shield ): damage_amount = ( previous_frame_structure.health - structure.health + previous_frame_structure.shield - structure.shield ) await self.on_unit_took_damage(structure, damage_amount) # Check if a structure changed its type if previous_frame_structure.type_id != structure.type_id: await self.on_unit_type_changed(structure, previous_frame_structure.type_id) # Check if structure completed if structure.build_progress == 1 and previous_frame_structure.build_progress < 1: self._units_created[structure.type_id] += 1 await self.on_building_construction_complete(structure) @final async def _issue_vision_events(self: BotAI) -> None: # Call events for enemy unit entered vision for enemy_unit in self.enemy_units: if enemy_unit.tag not in self._enemy_units_previous_map: await self.on_enemy_unit_entered_vision(enemy_unit) for enemy_structure in self.enemy_structures: if enemy_structure.tag not in self._enemy_structures_previous_map: await self.on_enemy_unit_entered_vision(enemy_structure) # Call events for enemy unit left vision enemy_units_left_vision: set[int] = set(self._enemy_units_previous_map) - self.enemy_units.tags for enemy_unit_tag in enemy_units_left_vision: await self.on_enemy_unit_left_vision(enemy_unit_tag) enemy_structures_left_vision: set[int] = set(self._enemy_structures_previous_map) - self.enemy_structures.tags for enemy_structure_tag in enemy_structures_left_vision: await self.on_enemy_unit_left_vision(enemy_structure_tag) @final async def _issue_unit_dead_events(self: BotAI) -> None: for unit_tag in self.state.dead_units & set(self._all_units_previous_map): await self.on_unit_destroyed(unit_tag) # DISTANCE CALCULATION @final @property def _units_count(self) -> int: return len(self.all_units) @final @property def _pdist(self) -> np.ndarray: """As property, so it will be recalculated each time it is called, or return from cache if it is called multiple times in teh same game_loop.""" if self._generated_frame != self.state.game_loop: return self.calculate_distances() return self._cached_pdist @final @property def _cdist(self) -> np.ndarray: """As property, so it will be recalculated each time it is called, or return from cache if it is called multiple times in teh same game_loop.""" if self._generated_frame != self.state.game_loop: return self.calculate_distances() return self._cached_cdist @final def _calculate_distances_method1(self) -> np.ndarray: self._generated_frame = self.state.game_loop # Converts tuple [(1, 2), (3, 4)] to flat list like [1, 2, 3, 4] flat_positions = (coord for unit in self.all_units for coord in unit.position_tuple) # Converts to numpy array, then converts the flat array back to shape (n, 2): [[1, 2], [3, 4]] positions_array: np.ndarray = np.fromiter( flat_positions, dtype=float, count=2 * self._units_count, ).reshape((self._units_count, 2)) assert len(positions_array) == self._units_count # See performance benchmarks self._cached_pdist = pdist(positions_array, "sqeuclidean") return self._cached_pdist @final def _calculate_distances_method2(self) -> np.ndarray: self._generated_frame = self.state.game_loop # Converts tuple [(1, 2), (3, 4)] to flat list like [1, 2, 3, 4] flat_positions = (coord for unit in self.all_units for coord in unit.position_tuple) # Converts to numpy array, then converts the flat array back to shape (n, 2): [[1, 2], [3, 4]] positions_array: np.ndarray = np.fromiter( flat_positions, dtype=float, count=2 * self._units_count, ).reshape((self._units_count, 2)) assert len(positions_array) == self._units_count # See performance benchmarks self._cached_cdist = cdist(positions_array, positions_array, "sqeuclidean") return self._cached_cdist @final def _calculate_distances_method3(self) -> np.ndarray: """Nearly same as above, but without asserts""" self._generated_frame = self.state.game_loop flat_positions = (coord for unit in self.all_units for coord in unit.position_tuple) positions_array: np.ndarray = np.fromiter( flat_positions, dtype=float, count=2 * self._units_count, ).reshape((-1, 2)) # See performance benchmarks self._cached_cdist = cdist(positions_array, positions_array, "sqeuclidean") return self._cached_cdist # Helper functions @final def square_to_condensed(self, i, j) -> int: # Converts indices of a square matrix to condensed matrix # https://stackoverflow.com/a/36867493/10882657 assert i != j, "No diagonal elements in condensed matrix! Diagonal elements are zero" if i < j: i, j = j, i return self._units_count * j - j * (j + 1) // 2 + i - 1 - j @final @staticmethod def convert_tuple_to_numpy_array(pos: tuple[float, float]) -> np.ndarray: """Converts a single position to a 2d numpy array with 1 row and 2 columns.""" return np.fromiter(pos, dtype=float, count=2).reshape((1, 2)) # Fast and simple calculation functions @final @staticmethod def distance_math_hypot( p1: _PointLike, p2: _PointLike, ) -> float: return math.hypot(p1[0] - p2[0], p1[1] - p2[1]) @final @staticmethod def distance_math_hypot_squared( p1: _PointLike, p2: _PointLike, ) -> float: return pow(p1[0] - p2[0], 2) + pow(p1[1] - p2[1], 2) @final def _distance_squared_unit_to_unit_method0(self, unit1: Unit, unit2: Unit) -> float: return self.distance_math_hypot_squared(unit1.position_tuple, unit2.position_tuple) # Distance calculation using the pre-calculated matrix above @final def _distance_squared_unit_to_unit_method1(self, unit1: Unit, unit2: Unit) -> float: # If checked on units if they have the same tag, return distance 0 as these are not in the 1 dimensional pdist array - would result in an error otherwise if unit1.tag == unit2.tag: return 0 # Calculate index, needs to be after pdist has been calculated and cached condensed_index = self.square_to_condensed(unit1.distance_calculation_index, unit2.distance_calculation_index) assert condensed_index < len(self._cached_pdist), ( f"Condensed index is larger than amount of calculated distances: {condensed_index} < {len(self._cached_pdist)}, units that caused the assert error: {unit1} and {unit2}" ) distance = self._pdist[condensed_index] return distance @final def _distance_squared_unit_to_unit_method2(self, unit1: Unit, unit2: Unit) -> float: # Calculate index, needs to be after cdist has been calculated and cached return self._cdist[unit1.distance_calculation_index, unit2.distance_calculation_index] # Distance calculation using the fastest distance calculation functions @final def _distance_pos_to_pos( self, pos1: tuple[float, float] | Point2, pos2: tuple[float, float] | Point2, ) -> float: return self.distance_math_hypot(pos1, pos2) @final def _distance_units_to_pos( self, units: Units, pos: tuple[float, float] | Point2, ) -> Generator[float, None, None]: """This function does not scale well, if len(units) > 100 it gets fairly slow""" return (self.distance_math_hypot(u.position_tuple, pos) for u in units) @final def _distance_unit_to_points( self, unit: Unit, points: Iterable[tuple[float, float]], ) -> Generator[float, None, None]: """This function does not scale well, if len(points) > 100 it gets fairly slow""" pos = unit.position_tuple return (self.distance_math_hypot(p, pos) for p in points) @final def _distances_override_functions(self, method: int = 0) -> None: """Overrides the internal distance calculation functions at game start in bot_ai.py self._prepare_start() function method 0: Use python's math.hypot The following methods calculate the distances between all units once: method 1: Use scipy's pdist condensed matrix (1d array) method 2: Use scipy's cidst square matrix (2d array) method 3: Use scipy's cidst square matrix (2d array) without asserts (careful: very weird error messages, but maybe slightly faster)""" assert 0 <= method <= 3, f"Selected method was: {method}" if method == 0: self._distance_squared_unit_to_unit = self._distance_squared_unit_to_unit_method0 elif method == 1: self._distance_squared_unit_to_unit = self._distance_squared_unit_to_unit_method1 self.calculate_distances = self._calculate_distances_method1 elif method == 2: self._distance_squared_unit_to_unit = self._distance_squared_unit_to_unit_method2 self.calculate_distances = self._calculate_distances_method2 elif method == 3: self._distance_squared_unit_to_unit = self._distance_squared_unit_to_unit_method2 self.calculate_distances = self._calculate_distances_method3 ================================================ FILE: sc2/cache.py ================================================ from __future__ import annotations from collections.abc import Callable, Hashable from typing import TYPE_CHECKING, Any, TypeVar if TYPE_CHECKING: from sc2.bot_ai import BotAI T = TypeVar("T") class CacheDict(dict[Hashable, Any]): def retrieve_and_set(self, key: Hashable, func: Callable[[], T]) -> T: """Either return the value at a certain key, or set the return value of a function to that key, then return that value.""" if key not in self: self[key] = func() return self[key] class property_cache_once_per_frame(property): # noqa: N801 """This decorator caches the return value for one game loop, then clears it if it is accessed in a different game loop. Only works on properties of the bot object, because it requires access to self.state.game_loop This decorator compared to the above runs a little faster, however you should only use this decorator if you are sure that you do not modify the mutable once it is calculated and cached. Copied and modified from https://tedboy.github.io/flask/_modules/werkzeug/utils.html#cached_property #""" def __init__(self, func: Callable[[BotAI], T], name: str | None = None) -> None: self.__name__ = name or func.__name__ self.__frame__ = f"__frame__{self.__name__}" # pyrefly: ignore self.func = func def __set__(self, obj: BotAI, value: T) -> None: obj.cache[self.__name__] = value obj.cache[self.__frame__] = obj.state.game_loop # pyre-fixme[34] def __get__(self, obj: BotAI, _type=None) -> T: value = obj.cache.get(self.__name__, None) bot_frame = obj.state.game_loop if value is None or obj.cache[self.__frame__] < bot_frame: value = self.func(obj) obj.cache[self.__name__] = value obj.cache[self.__frame__] = bot_frame return value ================================================ FILE: sc2/client.py ================================================ from __future__ import annotations from collections.abc import Iterable from pathlib import Path from typing import Any, Literal from aiohttp import ClientWebSocketResponse from loguru import logger from s2clientprotocol import debug_pb2 as debug_pb from s2clientprotocol import query_pb2 as query_pb from s2clientprotocol import raw_pb2 as raw_pb from s2clientprotocol import sc2api_pb2 as sc_pb from s2clientprotocol import spatial_pb2 as spatial_pb from sc2.action import combine_actions from sc2.data import ActionResult, ChatChannel, Race, Result, Status from sc2.game_data import AbilityData, GameData from sc2.game_info import GameInfo from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId from sc2.portconfig import Portconfig from sc2.position import Point2, Point3 from sc2.protocol import ConnectionAlreadyClosedError, Protocol, ProtocolError from sc2.renderer import Renderer from sc2.unit import Unit from sc2.unit_command import UnitCommand from sc2.units import Units class Client(Protocol): def __init__(self, ws: ClientWebSocketResponse, save_replay_path: str | None = None) -> None: """ :param ws: """ super().__init__(ws) # How many frames will be waited between iterations before the next one is called self.game_step: int = 4 self.save_replay_path: str | None = save_replay_path # The following will be set on join_game() self._player_id: int = None # pyrefly: ignore # The following will be set on leave() self._game_result: dict[int, Result] = None # pyrefly: ignore # Store a hash value of all the debug requests to prevent sending the same ones again if they haven't changed last frame self._debug_hash_tuple_last_iteration: tuple[int, int, int, int] = (0, 0, 0, 0) self._debug_draw_last_frame = False self._debug_texts = [] self._debug_lines = [] self._debug_boxes = [] self._debug_spheres = [] self._renderer = None self.raw_affects_selection = False @property def in_game(self) -> bool: return self._status in {Status.in_game, Status.in_replay} async def join_game( self, name: str | None = None, race: Race | None = None, observed_player_id: int | None = None, portconfig: Portconfig | None = None, rgb_render_config: dict[str, Any] | None = None, ): ifopts = sc_pb.InterfaceOptions( raw=True, score=True, show_cloaked=True, show_burrowed_shadows=True, raw_affects_selection=self.raw_affects_selection, raw_crop_to_playable_area=False, show_placeholders=True, ) if rgb_render_config: assert isinstance(rgb_render_config, dict) assert "window_size" in rgb_render_config and "minimap_size" in rgb_render_config window_size = rgb_render_config["window_size"] minimap_size = rgb_render_config["minimap_size"] self._renderer = Renderer(self, window_size, minimap_size) map_width, map_height = window_size minimap_width, minimap_height = minimap_size ifopts.render.resolution.x = map_width ifopts.render.resolution.y = map_height ifopts.render.minimap_resolution.x = minimap_width ifopts.render.minimap_resolution.y = minimap_height if race is None: assert isinstance(observed_player_id, int), f"observed_player_id is of type {type(observed_player_id)}" # join as observer request = sc_pb.RequestJoinGame(observed_player_id=observed_player_id, options=ifopts) else: assert isinstance(race, Race) request = sc_pb.RequestJoinGame(race=race.value, options=ifopts) # pyrefly: ignore[bad-argument-type] if portconfig: request.server_ports.game_port = portconfig.server[0] request.server_ports.base_port = portconfig.server[1] for ppc in portconfig.players: p = request.client_ports.add() # pyrefly: ignore p.game_port = ppc[0] p.base_port = ppc[1] if name is not None: assert isinstance(name, str), f"name is of type {type(name)}" request.player_name = name result = await self._execute(join_game=request) self._game_result = None # pyrefly: ignore self._player_id = result.join_game.player_id return result.join_game.player_id async def leave(self) -> None: """You can use 'await self.client.leave()' to surrender midst game.""" is_resign = self._game_result is None if is_resign: # For all clients that can leave, result of leaving the game either # loss, or the client will ignore the result self._game_result = {self._player_id: Result.Defeat} try: if self.save_replay_path is not None: await self.save_replay(self.save_replay_path) self.save_replay_path = None await self._execute(leave_game=sc_pb.RequestLeaveGame()) except (ProtocolError, ConnectionAlreadyClosedError): if is_resign: raise async def save_replay(self, path: str) -> None: logger.debug("Requesting replay from server") result = await self._execute(save_replay=sc_pb.RequestSaveReplay()) with Path(path).open("wb") as f: f.write(result.save_replay.data) logger.info(f"Saved replay to {path}") async def observation(self, game_loop: int | None = None): if game_loop is not None: result = await self._execute(observation=sc_pb.RequestObservation(game_loop=game_loop)) else: result = await self._execute(observation=sc_pb.RequestObservation()) assert result.HasField("observation") if not self.in_game or result.observation.player_result: # Sometimes game ends one step before results are available if not result.observation.player_result: result = await self._execute(observation=sc_pb.RequestObservation()) assert result.observation.player_result player_id_to_result = dict[int, Result]() for pr in result.observation.player_result: player_id_to_result[pr.player_id] = Result(pr.result) self._game_result = player_id_to_result # if render_data is available, then RGB rendering was requested if self._renderer and result.observation.observation.HasField("render_data"): await self._renderer.render(result.observation) return result async def step(self, step_size: int | None = None): """EXPERIMENTAL: Change self._client.game_step during the step function to increase or decrease steps per second""" step_size = step_size or self.game_step return await self._execute(step=sc_pb.RequestStep(count=step_size)) async def get_game_data(self) -> GameData: result: sc_pb.Response = await self._execute( data=sc_pb.RequestData(ability_id=True, unit_type_id=True, upgrade_id=True, buff_id=True, effect_id=True) ) return GameData(result.data) async def dump_data( self, ability_id: bool = True, unit_type_id: bool = True, upgrade_id: bool = True, buff_id: bool = True, effect_id: bool = True, ) -> None: """ Dump the game data files choose what data to dump in the keywords this function writes to a text file call it one time in on_step with: await self._client.dump_data() """ result = await self._execute( data=sc_pb.RequestData( ability_id=ability_id, unit_type_id=unit_type_id, upgrade_id=upgrade_id, buff_id=buff_id, effect_id=effect_id, ) ) with Path("data_dump.txt").open("a") as file: file.write(str(result.data)) async def get_game_info(self) -> GameInfo: result = await self._execute(game_info=sc_pb.RequestGameInfo()) return GameInfo(result.game_info) async def actions(self, actions: list[UnitCommand], return_successes: bool = False) -> list[ActionResult]: if not actions: return [] if not isinstance(actions, list): actions = [actions] # On realtime=True, might get an error here: sc2.protocol.ProtocolError: ['Not in a game'] try: response = await self._execute( action=sc_pb.RequestAction( # pyrefly: ignore actions=(sc_pb.Action(action_raw=action) for action in combine_actions(actions)) ) ) except ProtocolError: return [] if return_successes: return [ActionResult(result) for result in response.action.result] return [ ActionResult(result) for result in response.action.result if ActionResult(result) != ActionResult.Success ] async def query_pathing(self, start: Unit | Point2 | Point3, end: Point2 | Point3) -> float | None: """Caution: returns "None" when path not found Try to combine queries with the function below because the pathing query is generally slow. :param start: :param end:""" assert isinstance(start, (Point2, Unit)) assert isinstance(end, Point2) if isinstance(start, Point2): path = [query_pb.RequestQueryPathing(start_pos=start.as_Point2D, end_pos=end.as_Point2D)] else: path = [query_pb.RequestQueryPathing(unit_tag=start.tag, end_pos=end.as_Point2D)] result = await self._execute(query=query_pb.RequestQuery(pathing=path)) distance = float(result.query.pathing[0].distance) if distance <= 0.0: return None return distance async def query_pathings(self, zipped_list: list[tuple[Unit | Point2 | Point3, Point2 | Point3]]) -> list[float]: """Usage: await self.query_pathings([[unit1, target2], [unit2, target2]]) -> returns [distance1, distance2] Caution: returns 0 when path not found :param zipped_list: """ assert zipped_list, "No entry in zipped_list" path = ( query_pb.RequestQueryPathing( # pyrefly: ignore unit_tag=p1.tag if isinstance(p1, Unit) else None, # pyrefly: ignore start_pos=None if isinstance(p1, Unit) else p1.as_Point2D, end_pos=p2.as_Point2D, ) for p1, p2 in zipped_list ) # pyrefly: ignore results = await self._execute(query=query_pb.RequestQuery(pathing=path)) return [float(d.distance) for d in results.query.pathing] async def _query_building_placement_fast( self, ability: AbilityId, positions: list[Point2 | Point3], ignore_resources: bool = True ) -> list[bool]: """ Returns a list of booleans. Return True for positions that are valid, False otherwise. :param ability: :param positions: :param ignore_resources: """ result = await self._execute( query=query_pb.RequestQuery( # pyrefly: ignore placements=( query_pb.RequestQueryBuildingPlacement(ability_id=ability.value, target_pos=position.as_Point2D) for position in positions ), ignore_resource_requirements=ignore_resources, ) ) # Success enum value is 1, see https://github.com/Blizzard/s2client-proto/blob/9906df71d6909511907d8419b33acc1a3bd51ec0/s2clientprotocol/error.proto#L7 return [p.result == 1 for p in result.query.placements] async def query_building_placement( self, ability: AbilityData, positions: list[Point2 | Point3], ignore_resources: bool = True, # pyre-fixme[11] ) -> list[ActionResult]: """This function might be deleted in favor of the function above (_query_building_placement_fast). :param ability: :param positions: :param ignore_resources:""" assert isinstance(ability, AbilityData) result = await self._execute( query=query_pb.RequestQuery( # pyrefly: ignore placements=( query_pb.RequestQueryBuildingPlacement(ability_id=ability.id.value, target_pos=position.as_Point2D) for position in positions ), ignore_resource_requirements=ignore_resources, ) ) # Unnecessary converting to ActionResult? return [ActionResult(p.result) for p in result.query.placements] async def query_available_abilities( self, units: list[Unit] | Units, ignore_resource_requirements: bool = False ) -> list[list[AbilityId]]: """Query abilities of multiple units""" input_was_a_list = True if not isinstance(units, list): """ Deprecated, accepting a single unit may be removed in the future, query a list of units instead """ assert isinstance(units, Unit) units = [units] input_was_a_list = False assert units result = await self._execute( query=query_pb.RequestQuery( # pyrefly: ignore abilities=(query_pb.RequestQueryAvailableAbilities(unit_tag=unit.tag) for unit in units), ignore_resource_requirements=ignore_resource_requirements, ) ) """ Fix for bots that only query a single unit, may be removed soon """ if not input_was_a_list: # pyrefly: ignore return [[AbilityId(a.ability_id) for a in b.abilities] for b in result.query.abilities][0] return [[AbilityId(a.ability_id) for a in b.abilities] for b in result.query.abilities] async def query_available_abilities_with_tag( self, units: list[Unit] | Units, ignore_resource_requirements: bool = False ) -> dict[int, set[AbilityId]]: """Query abilities of multiple units""" result = await self._execute( query=query_pb.RequestQuery( # pyrefly: ignore abilities=(query_pb.RequestQueryAvailableAbilities(unit_tag=unit.tag) for unit in units), ignore_resource_requirements=ignore_resource_requirements, ) ) return {b.unit_tag: {AbilityId(a.ability_id) for a in b.abilities} for b in result.query.abilities} async def chat_send(self, message: str, team_only: bool) -> None: """Writes a message to the chat""" ch = ChatChannel.Team if team_only else ChatChannel.Broadcast await self._execute( action=sc_pb.RequestAction( actions=[ sc_pb.Action( action_chat=sc_pb.ActionChat(channel=ch.value, message=message) # type: ignore[bad-argument-type] ) ] ) ) async def toggle_autocast(self, units: list[Unit] | Units, ability: AbilityId) -> None: """Toggle autocast of all specified units :param units: :param ability:""" assert units assert isinstance(units, list) assert all(isinstance(u, Unit) for u in units) assert isinstance(ability, AbilityId) await self._execute( action=sc_pb.RequestAction( actions=[ sc_pb.Action( action_raw=raw_pb.ActionRaw( toggle_autocast=raw_pb.ActionRawToggleAutocast( ability_id=ability.value, # pyrefly: ignore unit_tags=(u.tag for u in units), ) ) ) ] ) ) async def debug_create_unit( self, unit_spawn_commands: list[tuple[UnitTypeId, int, Point2 | Point3, Literal[1, 2]]] ) -> None: """Usage example (will spawn 5 marines in the center of the map for player ID 1): await self._client.debug_create_unit([[UnitTypeId.MARINE, 5, self._game_info.map_center, 1]]) :param unit_spawn_commands:""" assert unit_spawn_commands, "List is empty" await self._execute( debug=sc_pb.RequestDebug( # pyrefly: ignore debug=( debug_pb.DebugCommand( create_unit=debug_pb.DebugCreateUnit( unit_type=unit_type.value, owner=owner_id, pos=position.as_Point2D, quantity=amount_of_units, ) ) for unit_type, amount_of_units, position, owner_id in unit_spawn_commands ) ) ) async def debug_kill_unit(self, unit_tags: Unit | Units | list[int] | set[int]) -> None: """ :param unit_tags: """ if isinstance(unit_tags, Units): unit_tags = unit_tags.tags if isinstance(unit_tags, Unit): unit_tags = [unit_tags.tag] assert unit_tags await self._execute( # pyrefly: ignore debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(kill_unit=debug_pb.DebugKillUnit(tag=unit_tags))]) ) async def move_camera(self, position: Unit | Units | Point2 | Point3) -> None: """Moves camera to the target position :param position:""" assert isinstance(position, (Unit, Units, Point2, Point3)) if isinstance(position, Units): position = position.center if isinstance(position, Unit): position = position.position await self._execute( action=sc_pb.RequestAction( actions=[ sc_pb.Action( action_raw=raw_pb.ActionRaw( camera_move=raw_pb.ActionRawCameraMove(center_world_space=position.to3.as_Point) ) ) ] ) ) async def obs_move_camera(self, position: Unit | Units | Point2 | Point3, distance: float = 0) -> None: """Moves observer camera to the target position. Only works when observing (e.g. watching the replay). :param position:""" assert isinstance(position, (Unit, Units, Point2, Point3)) if isinstance(position, Units): position = position.center if isinstance(position, Unit): position = position.position await self._execute( obs_action=sc_pb.RequestObserverAction( actions=[ sc_pb.ObserverAction( camera_move=sc_pb.ActionObserverCameraMove(world_pos=position.as_Point2D, distance=distance) ) ] ) ) async def move_camera_spatial(self, position: Point2 | Point3) -> None: """Moves camera to the target position using the spatial aciton interface :param position:""" assert isinstance(position, (Point2, Point3)) action = sc_pb.Action( action_render=spatial_pb.ActionSpatial( camera_move=spatial_pb.ActionSpatialCameraMove(center_minimap=position.as_PointI) ) ) await self._execute(action=sc_pb.RequestAction(actions=[action])) def debug_text_simple(self, text: str) -> None: """Draws a text in the top left corner of the screen (up to a max of 6 messages fit there).""" self._debug_texts.append(DrawItemScreenText(text=text, color=None, start_point=Point2((0, 0)), font_size=8)) def debug_text_screen( self, text: str, pos: Point2 | Point3 | tuple[float, float] | list[float], color: tuple[float, float, float] | list[float] | Point3 | None = None, size: int = 8, ) -> None: """ Draws a text on the screen (monitor / game window) with coordinates 0 <= x, y <= 1. :param text: :param pos: :param color: :param size: """ assert len(pos) >= 2 assert 0 <= pos[0] <= 1 assert 0 <= pos[1] <= 1 pos = Point2((pos[0], pos[1])) self._debug_texts.append(DrawItemScreenText(text=text, color=color, start_point=pos, font_size=size)) def debug_text_2d( self, text: str, pos: Point2 | Point3 | tuple[float, float] | list[float], color: tuple[float, float, float] | list[float] | Point3 | None = None, size: int = 8, ): return self.debug_text_screen(text, pos, color, size) def debug_text_world( self, text: str, pos: Unit | Point3, color: tuple[float, float, float] | list[float] | Point3 | None = None, size: int = 8, ) -> None: """ Draws a text at Point3 position in the game world. To grab a unit's 3d position, use unit.position3d Usually the Z value of a Point3 is between 8 and 14 (except for flying units). Use self.get_terrain_z_height() from bot_ai.py to get the Z value (height) of the terrain at a 2D position. :param text: :param color: :param size: """ if isinstance(pos, Unit): pos = pos.position3d assert isinstance(pos, Point3) self._debug_texts.append(DrawItemWorldText(text=text, color=color, start_point=pos, font_size=size)) def debug_text_3d( self, text: str, pos: Unit | Point3, color: tuple[float, float, float] | list[float] | Point3 | None = None, size: int = 8, ): return self.debug_text_world(text, pos, color, size) def debug_line_out( self, p0: Unit | Point3, p1: Unit | Point3, color: tuple[float, float, float] | list[float] | Point3 | None = None, ) -> None: """ Draws a line from p0 to p1. :param p0: :param p1: :param color: """ if isinstance(p0, Unit): p0 = p0.position3d assert isinstance(p0, Point3) if isinstance(p1, Unit): p1 = p1.position3d assert isinstance(p1, Point3) self._debug_lines.append(DrawItemLine(color=color, start_point=p0, end_point=p1)) def debug_box_out( self, p_min: Unit | Point3, p_max: Unit | Point3, color: tuple[float, float, float] | list[float] | Point3 | None = None, ) -> None: """ Draws a box with p_min and p_max as corners of the box. :param p_min: :param p_max: :param color: """ if isinstance(p_min, Unit): p_min = p_min.position3d assert isinstance(p_min, Point3) if isinstance(p_max, Unit): p_max = p_max.position3d assert isinstance(p_max, Point3) self._debug_boxes.append(DrawItemBox(start_point=p_min, end_point=p_max, color=color)) def debug_box2_out( self, pos: Unit | Point3, half_vertex_length: float = 0.25, color: tuple[float, float, float] | list[float] | Point3 | None = None, ) -> None: """ Draws a box center at a position 'pos', with box side lengths (vertices) of two times 'half_vertex_length'. :param pos: :param half_vertex_length: :param color: """ if isinstance(pos, Unit): pos = pos.position3d assert isinstance(pos, Point3) p0 = pos + Point3((-half_vertex_length, -half_vertex_length, -half_vertex_length)) p1 = pos + Point3((half_vertex_length, half_vertex_length, half_vertex_length)) self._debug_boxes.append(DrawItemBox(start_point=p0, end_point=p1, color=color)) def debug_sphere_out( self, p: Unit | Point3, r: float, color: tuple[float, float, float] | list[float] | Point3 | None = None, ) -> None: """ Draws a sphere at point p with radius r. :param p: :param r: :param color: """ if isinstance(p, Unit): p = p.position3d assert isinstance(p, Point3) self._debug_spheres.append(DrawItemSphere(start_point=p, radius=r, color=color)) async def _send_debug(self) -> None: """Sends the debug draw execution. This is run by main.py now automatically, if there is any items in the list. You do not need to run this manually any longer. Check examples/terran/ramp_wall.py for example drawing. Each draw request needs to be sent again in every single on_step iteration. """ debug_hash = ( sum(hash(item) for item in self._debug_texts), sum(hash(item) for item in self._debug_lines), sum(hash(item) for item in self._debug_boxes), sum(hash(item) for item in self._debug_spheres), ) if debug_hash != (0, 0, 0, 0): if debug_hash != self._debug_hash_tuple_last_iteration: # Something has changed, either more or less is to be drawn, or a position of a drawing changed (e.g. when drawing on a moving unit) self._debug_hash_tuple_last_iteration = debug_hash try: await self._execute( debug=sc_pb.RequestDebug( debug=[ debug_pb.DebugCommand( draw=debug_pb.DebugDraw( # pyrefly: ignore text=[text.to_proto() for text in self._debug_texts] if self._debug_texts else None, # pyrefly: ignore lines=[line.to_proto() for line in self._debug_lines] if self._debug_lines else None, # pyrefly: ignore boxes=[box.to_proto() for box in self._debug_boxes] if self._debug_boxes else None, # pyrefly: ignore spheres=[sphere.to_proto() for sphere in self._debug_spheres] if self._debug_spheres else None, ) ) ] ) ) except ProtocolError: return self._debug_draw_last_frame = True self._debug_texts.clear() self._debug_lines.clear() self._debug_boxes.clear() self._debug_spheres.clear() elif self._debug_draw_last_frame: # Clear drawing if we drew last frame but nothing to draw this frame self._debug_hash_tuple_last_iteration = (0, 0, 0, 0) await self._execute( debug=sc_pb.RequestDebug( debug=[ # pyrefly: ignore debug_pb.DebugCommand(draw=debug_pb.DebugDraw(text=None, lines=None, boxes=None, spheres=None)) ] ) ) self._debug_draw_last_frame = False async def debug_leave(self) -> None: await self._execute(debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(end_game=debug_pb.DebugEndGame())])) async def debug_set_unit_value( self, unit_tags: Iterable[int] | Units | Unit, unit_value: int, value: float ) -> None: """Sets a "unit value" (Energy, Life or Shields) of the given units to the given value. Can't set the life of a unit to 0, use "debug_kill_unit" for that. Also can't set the life above the unit's maximum. The following example sets the health of all your workers to 1: await self.debug_set_unit_value(self.workers, 2, value=1)""" if isinstance(unit_tags, Units): unit_tags = unit_tags.tags if isinstance(unit_tags, Unit): unit_tags = [unit_tags.tag] assert hasattr(unit_tags, "__iter__"), ( f"unit_tags argument needs to be an iterable (list, dict, set, Units), given argument is {type(unit_tags).__name__}" ) assert 1 <= unit_value <= 3, ( f"unit_value needs to be between 1 and 3 (1 for energy, 2 for life, 3 for shields), given argument is {unit_value}" ) assert all(tag > 0 for tag in unit_tags), f"Unit tags have invalid value: {unit_tags}" assert isinstance(value, (int, float)), "Value needs to be of type int or float" assert value >= 0, "Value can't be negative" await self._execute( debug=sc_pb.RequestDebug( debug=( debug_pb.DebugCommand( unit_value=debug_pb.DebugSetUnitValue( unit_value=unit_value, # pyrefly: ignore[bad-argument-type] value=float(value), unit_tag=unit_tag, ) ) for unit_tag in unit_tags ) ) ) async def debug_hang(self, delay_in_seconds: float) -> None: """Freezes the SC2 client. Not recommended to be used.""" delay_in_ms = int(round(delay_in_seconds * 1000)) await self._execute( debug=sc_pb.RequestDebug( debug=[ debug_pb.DebugCommand( test_process=debug_pb.DebugTestProcess( test=1, # pyrefly: ignore delay_ms=delay_in_ms, ) ) ] ) ) async def debug_show_map(self) -> None: """Reveals the whole map for the bot. Using it a second time disables it again.""" await self._execute( debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=1)]) # pyrefly: ignore[bad-argument-type] ) async def debug_control_enemy(self) -> None: """Allows control over enemy units and structures similar to team games control - does not allow the bot to spend the opponent's ressources. Using it a second time disables it again.""" await self._execute( debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=2)]) # pyrefly: ignore[bad-argument-type] ) async def debug_food(self) -> None: """Should disable food usage (does not seem to work?). Using it a second time disables it again.""" await self._execute( debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=3)]) # pyrefly: ignore[bad-argument-type] ) async def debug_free(self) -> None: """Units, structures and upgrades are free of mineral and gas cost. Using it a second time disables it again.""" await self._execute( debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=4)]) # pyrefly: ignore[bad-argument-type] ) async def debug_all_resources(self) -> None: """Gives 5000 minerals and 5000 vespene to the bot.""" await self._execute( debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=5)]) # pyrefly: ignore[bad-argument-type] ) async def debug_god(self) -> None: """Your units and structures no longer take any damage. Using it a second time disables it again.""" await self._execute( debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=6)]) # pyrefly: ignore[bad-argument-type] ) async def debug_minerals(self) -> None: """Gives 5000 minerals to the bot.""" await self._execute( debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=7)]) # pyrefly: ignore[bad-argument-type] ) async def debug_gas(self) -> None: """Gives 5000 vespene to the bot. This does not seem to be working.""" await self._execute( debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=8)]) # pyrefly: ignore[bad-argument-type] ) async def debug_cooldown(self) -> None: """Disables cooldowns of unit abilities for the bot. Using it a second time disables it again.""" await self._execute( debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=9)]) # pyrefly: ignore[bad-argument-type] ) async def debug_tech_tree(self) -> None: """Removes all tech requirements (e.g. can build a factory without having a barracks). Using it a second time disables it again.""" await self._execute( debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=10)]) # pyrefly: ignore[bad-argument-type] ) async def debug_upgrade(self) -> None: """Researches all currently available upgrades. E.g. using it once unlocks combat shield, stimpack and 1-1. Using it a second time unlocks 2-2 and all other upgrades stay researched.""" await self._execute( debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=11)]) # pyrefly: ignore[bad-argument-type] ) async def debug_fast_build(self) -> None: """Sets the build time of units and structures and upgrades to zero. Using it a second time disables it again.""" await self._execute( debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=12)]) # pyrefly: ignore[bad-argument-type] ) async def quick_save(self) -> None: """Saves the current game state to an in-memory bookmark. See: https://github.com/Blizzard/s2client-proto/blob/eeaf5efaea2259d7b70247211dff98da0a2685a2/s2clientprotocol/sc2api.proto#L93""" await self._execute(quick_save=sc_pb.RequestQuickSave()) async def quick_load(self) -> None: """Loads the game state from the previously stored in-memory bookmark. Caution: - The SC2 Client will crash if the game wasn't quicksaved - The bot step iteration counter will not reset - self.state.game_loop will be set to zero after the quickload, and self.time is dependant on it""" await self._execute(quick_load=sc_pb.RequestQuickLoad()) class DrawItem: @staticmethod def to_debug_color(color: tuple[float, float, float] | list[float] | Point3 | None = None) -> debug_pb.Color: """Helper function for color conversion""" if color is None: return debug_pb.Color(r=255, g=255, b=255) # Need to check if not of type Point3 because Point3 inherits from tuple if isinstance(color, (tuple, list)) or isinstance(color, Point3) and len(color) == 3: return debug_pb.Color(r=int(color[0]), g=int(color[1]), b=int(color[2])) # In case color is of type Point3 r = getattr(color, "r", getattr(color, "x", 255)) g = getattr(color, "g", getattr(color, "y", 255)) b = getattr(color, "b", getattr(color, "z", 255)) if max(r, g, b) <= 1: r *= 255 g *= 255 b *= 255 return debug_pb.Color(r=int(r), g=int(g), b=int(b)) class DrawItemScreenText(DrawItem): def __init__( self, start_point: Point2, color: tuple[float, float, float] | list[float] | Point3 | None = None, text: str = "", font_size: int = 8, ) -> None: self._start_point = start_point self._color = color self._text = text self._font_size = font_size def to_proto(self): return debug_pb.DebugText( color=self.to_debug_color(self._color), text=self._text, virtual_pos=self._start_point.to3.as_Point, # pyrefly: ignore world_pos=None, size=self._font_size, ) def __hash__(self) -> int: return hash((self._start_point, self._color, self._text, self._font_size)) class DrawItemWorldText(DrawItem): def __init__( self, start_point: Point3, color: tuple[float, float, float] | list[float] | Point3 | None, text: str = "", font_size: int = 8, ) -> None: self._start_point = start_point self._color = color self._text = text self._font_size = font_size def to_proto(self): return debug_pb.DebugText( color=self.to_debug_color(self._color), text=self._text, # pyrefly: ignore virtual_pos=None, world_pos=self._start_point.as_Point, size=self._font_size, ) def __hash__(self) -> int: return hash((self._start_point, self._text, self._font_size, self._color)) class DrawItemLine(DrawItem): def __init__( self, start_point: Point3, end_point: Point3, color: tuple[float, float, float] | list[float] | Point3 | None = None, ) -> None: self._start_point = start_point self._end_point = end_point self._color = color def to_proto(self): return debug_pb.DebugLine( line=debug_pb.Line(p0=self._start_point.as_Point, p1=self._end_point.as_Point), color=self.to_debug_color(self._color), ) def __hash__(self) -> int: return hash((self._start_point, self._end_point, self._color)) class DrawItemBox(DrawItem): def __init__( self, start_point: Point3, end_point: Point3, color: tuple[float, float, float] | list[float] | Point3 | None = None, ) -> None: self._start_point = start_point self._end_point = end_point self._color = color def to_proto(self): return debug_pb.DebugBox( min=self._start_point.as_Point, max=self._end_point.as_Point, color=self.to_debug_color(self._color), ) def __hash__(self) -> int: return hash((self._start_point, self._end_point, self._color)) class DrawItemSphere(DrawItem): def __init__( self, start_point: Point3, radius: float, color: tuple[float, float, float] | list[float] | Point3 | None = None, ) -> None: self._start_point = start_point self._radius = radius self._color = color def to_proto(self): return debug_pb.DebugSphere( p=self._start_point.as_Point, r=self._radius, color=self.to_debug_color(self._color) ) def __hash__(self) -> int: return hash((self._start_point, self._radius, self._color)) ================================================ FILE: sc2/constants.py ================================================ from __future__ import annotations from collections import defaultdict from typing import Any from sc2.data import Alliance, Attribute, CloakState, DisplayType, TargetType from sc2.ids.ability_id import AbilityId from sc2.ids.buff_id import BuffId from sc2.ids.unit_typeid import UnitTypeId from sc2.ids.upgrade_id import UpgradeId mineral_ids: set[int] = { UnitTypeId.RICHMINERALFIELD.value, UnitTypeId.RICHMINERALFIELD750.value, UnitTypeId.MINERALFIELD.value, UnitTypeId.MINERALFIELD450.value, UnitTypeId.MINERALFIELD750.value, UnitTypeId.LABMINERALFIELD.value, UnitTypeId.LABMINERALFIELD750.value, UnitTypeId.PURIFIERRICHMINERALFIELD.value, UnitTypeId.PURIFIERRICHMINERALFIELD750.value, UnitTypeId.PURIFIERMINERALFIELD.value, UnitTypeId.PURIFIERMINERALFIELD750.value, UnitTypeId.BATTLESTATIONMINERALFIELD.value, UnitTypeId.BATTLESTATIONMINERALFIELD750.value, UnitTypeId.MINERALFIELDOPAQUE.value, UnitTypeId.MINERALFIELDOPAQUE900.value, } geyser_ids: set[int] = { UnitTypeId.VESPENEGEYSER.value, UnitTypeId.SPACEPLATFORMGEYSER.value, UnitTypeId.RICHVESPENEGEYSER.value, UnitTypeId.PROTOSSVESPENEGEYSER.value, UnitTypeId.PURIFIERVESPENEGEYSER.value, UnitTypeId.SHAKURASVESPENEGEYSER.value, } transforming: dict[UnitTypeId, AbilityId] = { # Terran structures UnitTypeId.BARRACKS: AbilityId.LAND_BARRACKS, UnitTypeId.BARRACKSFLYING: AbilityId.LAND_BARRACKS, UnitTypeId.COMMANDCENTER: AbilityId.LAND_COMMANDCENTER, UnitTypeId.COMMANDCENTERFLYING: AbilityId.LAND_COMMANDCENTER, UnitTypeId.ORBITALCOMMAND: AbilityId.LAND_ORBITALCOMMAND, UnitTypeId.ORBITALCOMMANDFLYING: AbilityId.LAND_ORBITALCOMMAND, UnitTypeId.FACTORY: AbilityId.LAND_FACTORY, UnitTypeId.FACTORYFLYING: AbilityId.LAND_FACTORY, UnitTypeId.STARPORT: AbilityId.LAND_STARPORT, UnitTypeId.STARPORTFLYING: AbilityId.LAND_STARPORT, UnitTypeId.SUPPLYDEPOT: AbilityId.MORPH_SUPPLYDEPOT_RAISE, UnitTypeId.SUPPLYDEPOTLOWERED: AbilityId.MORPH_SUPPLYDEPOT_LOWER, # Terran units UnitTypeId.HELLION: AbilityId.MORPH_HELLION, UnitTypeId.HELLIONTANK: AbilityId.MORPH_HELLBAT, UnitTypeId.LIBERATOR: AbilityId.MORPH_LIBERATORAAMODE, UnitTypeId.LIBERATORAG: AbilityId.MORPH_LIBERATORAGMODE, UnitTypeId.SIEGETANK: AbilityId.UNSIEGE_UNSIEGE, UnitTypeId.SIEGETANKSIEGED: AbilityId.SIEGEMODE_SIEGEMODE, UnitTypeId.THOR: AbilityId.MORPH_THOREXPLOSIVEMODE, UnitTypeId.THORAP: AbilityId.MORPH_THORHIGHIMPACTMODE, UnitTypeId.VIKINGASSAULT: AbilityId.MORPH_VIKINGASSAULTMODE, UnitTypeId.VIKINGFIGHTER: AbilityId.MORPH_VIKINGFIGHTERMODE, UnitTypeId.WIDOWMINE: AbilityId.BURROWUP, UnitTypeId.WIDOWMINEBURROWED: AbilityId.BURROWDOWN, # Protoss structures UnitTypeId.GATEWAY: AbilityId.MORPH_GATEWAY, UnitTypeId.WARPGATE: AbilityId.MORPH_WARPGATE, # Protoss units UnitTypeId.OBSERVER: AbilityId.MORPH_OBSERVERMODE, UnitTypeId.OBSERVERSIEGEMODE: AbilityId.MORPH_SURVEILLANCEMODE, UnitTypeId.WARPPRISM: AbilityId.MORPH_WARPPRISMTRANSPORTMODE, UnitTypeId.WARPPRISMPHASING: AbilityId.MORPH_WARPPRISMPHASINGMODE, # Zerg structures UnitTypeId.SPINECRAWLER: AbilityId.SPINECRAWLERROOT_SPINECRAWLERROOT, UnitTypeId.SPINECRAWLERUPROOTED: AbilityId.SPINECRAWLERUPROOT_SPINECRAWLERUPROOT, UnitTypeId.SPORECRAWLER: AbilityId.SPORECRAWLERROOT_SPORECRAWLERROOT, UnitTypeId.SPORECRAWLERUPROOTED: AbilityId.SPORECRAWLERUPROOT_SPORECRAWLERUPROOT, # Zerg units UnitTypeId.BANELING: AbilityId.BURROWUP_BANELING, UnitTypeId.BANELINGBURROWED: AbilityId.BURROWDOWN_BANELING, UnitTypeId.DRONE: AbilityId.BURROWUP_DRONE, UnitTypeId.DRONEBURROWED: AbilityId.BURROWDOWN_DRONE, UnitTypeId.HYDRALISK: AbilityId.BURROWUP_HYDRALISK, UnitTypeId.HYDRALISKBURROWED: AbilityId.BURROWDOWN_HYDRALISK, UnitTypeId.INFESTOR: AbilityId.BURROWUP_INFESTOR, UnitTypeId.INFESTORBURROWED: AbilityId.BURROWDOWN_INFESTOR, UnitTypeId.INFESTORTERRAN: AbilityId.BURROWUP_INFESTORTERRAN, UnitTypeId.INFESTORTERRANBURROWED: AbilityId.BURROWDOWN_INFESTORTERRAN, UnitTypeId.LURKERMP: AbilityId.BURROWUP_LURKER, UnitTypeId.LURKERMPBURROWED: AbilityId.BURROWDOWN_LURKER, UnitTypeId.OVERSEER: AbilityId.MORPH_OVERSEERMODE, UnitTypeId.OVERSEERSIEGEMODE: AbilityId.MORPH_OVERSIGHTMODE, UnitTypeId.QUEEN: AbilityId.BURROWUP_QUEEN, UnitTypeId.QUEENBURROWED: AbilityId.BURROWDOWN_QUEEN, UnitTypeId.ROACH: AbilityId.BURROWUP_ROACH, UnitTypeId.ROACHBURROWED: AbilityId.BURROWDOWN_ROACH, UnitTypeId.SWARMHOSTBURROWEDMP: AbilityId.BURROWDOWN_SWARMHOST, UnitTypeId.SWARMHOSTMP: AbilityId.BURROWUP_SWARMHOST, UnitTypeId.ULTRALISK: AbilityId.BURROWUP_ULTRALISK, UnitTypeId.ULTRALISKBURROWED: AbilityId.BURROWDOWN_ULTRALISK, UnitTypeId.ZERGLING: AbilityId.BURROWUP_ZERGLING, UnitTypeId.ZERGLINGBURROWED: AbilityId.BURROWDOWN_ZERGLING, } # For now only contains units that cost supply abilityid_to_unittypeid: dict[AbilityId, UnitTypeId] = { # Protoss AbilityId.NEXUSTRAIN_PROBE: UnitTypeId.PROBE, AbilityId.GATEWAYTRAIN_ZEALOT: UnitTypeId.ZEALOT, AbilityId.WARPGATETRAIN_ZEALOT: UnitTypeId.ZEALOT, AbilityId.TRAIN_ADEPT: UnitTypeId.ADEPT, AbilityId.TRAINWARP_ADEPT: UnitTypeId.ADEPT, AbilityId.GATEWAYTRAIN_STALKER: UnitTypeId.STALKER, AbilityId.WARPGATETRAIN_STALKER: UnitTypeId.STALKER, AbilityId.GATEWAYTRAIN_SENTRY: UnitTypeId.SENTRY, AbilityId.WARPGATETRAIN_SENTRY: UnitTypeId.SENTRY, AbilityId.GATEWAYTRAIN_DARKTEMPLAR: UnitTypeId.DARKTEMPLAR, AbilityId.WARPGATETRAIN_DARKTEMPLAR: UnitTypeId.DARKTEMPLAR, AbilityId.GATEWAYTRAIN_HIGHTEMPLAR: UnitTypeId.HIGHTEMPLAR, AbilityId.WARPGATETRAIN_HIGHTEMPLAR: UnitTypeId.HIGHTEMPLAR, AbilityId.ROBOTICSFACILITYTRAIN_OBSERVER: UnitTypeId.OBSERVER, AbilityId.ROBOTICSFACILITYTRAIN_COLOSSUS: UnitTypeId.COLOSSUS, AbilityId.ROBOTICSFACILITYTRAIN_IMMORTAL: UnitTypeId.IMMORTAL, AbilityId.ROBOTICSFACILITYTRAIN_WARPPRISM: UnitTypeId.WARPPRISM, AbilityId.STARGATETRAIN_CARRIER: UnitTypeId.CARRIER, AbilityId.STARGATETRAIN_ORACLE: UnitTypeId.ORACLE, AbilityId.STARGATETRAIN_PHOENIX: UnitTypeId.PHOENIX, AbilityId.STARGATETRAIN_TEMPEST: UnitTypeId.TEMPEST, AbilityId.STARGATETRAIN_VOIDRAY: UnitTypeId.VOIDRAY, AbilityId.NEXUSTRAINMOTHERSHIP_MOTHERSHIP: UnitTypeId.MOTHERSHIP, # Terran AbilityId.COMMANDCENTERTRAIN_SCV: UnitTypeId.SCV, AbilityId.BARRACKSTRAIN_MARINE: UnitTypeId.MARINE, AbilityId.BARRACKSTRAIN_GHOST: UnitTypeId.GHOST, AbilityId.BARRACKSTRAIN_MARAUDER: UnitTypeId.MARAUDER, AbilityId.BARRACKSTRAIN_REAPER: UnitTypeId.REAPER, AbilityId.FACTORYTRAIN_HELLION: UnitTypeId.HELLION, AbilityId.FACTORYTRAIN_SIEGETANK: UnitTypeId.SIEGETANK, AbilityId.FACTORYTRAIN_THOR: UnitTypeId.THOR, AbilityId.FACTORYTRAIN_WIDOWMINE: UnitTypeId.WIDOWMINE, AbilityId.TRAIN_HELLBAT: UnitTypeId.HELLIONTANK, AbilityId.TRAIN_CYCLONE: UnitTypeId.CYCLONE, AbilityId.STARPORTTRAIN_RAVEN: UnitTypeId.RAVEN, AbilityId.STARPORTTRAIN_VIKINGFIGHTER: UnitTypeId.VIKINGFIGHTER, AbilityId.STARPORTTRAIN_MEDIVAC: UnitTypeId.MEDIVAC, AbilityId.STARPORTTRAIN_BATTLECRUISER: UnitTypeId.BATTLECRUISER, AbilityId.STARPORTTRAIN_BANSHEE: UnitTypeId.BANSHEE, AbilityId.STARPORTTRAIN_LIBERATOR: UnitTypeId.LIBERATOR, # Zerg AbilityId.LARVATRAIN_DRONE: UnitTypeId.DRONE, AbilityId.LARVATRAIN_OVERLORD: UnitTypeId.OVERLORD, AbilityId.LARVATRAIN_ZERGLING: UnitTypeId.ZERGLING, AbilityId.LARVATRAIN_ROACH: UnitTypeId.ROACH, AbilityId.LARVATRAIN_HYDRALISK: UnitTypeId.HYDRALISK, AbilityId.LARVATRAIN_MUTALISK: UnitTypeId.MUTALISK, AbilityId.LARVATRAIN_CORRUPTOR: UnitTypeId.CORRUPTOR, AbilityId.LARVATRAIN_ULTRALISK: UnitTypeId.ULTRALISK, AbilityId.LARVATRAIN_INFESTOR: UnitTypeId.INFESTOR, AbilityId.LARVATRAIN_VIPER: UnitTypeId.VIPER, AbilityId.LOCUSTTRAIN_SWARMHOST: UnitTypeId.SWARMHOSTMP, AbilityId.TRAINQUEEN_QUEEN: UnitTypeId.QUEEN, } IS_STRUCTURE: int = Attribute.Structure.value IS_LIGHT: int = Attribute.Light.value IS_ARMORED: int = Attribute.Armored.value IS_BIOLOGICAL: int = Attribute.Biological.value IS_MECHANICAL: int = Attribute.Mechanical.value IS_MASSIVE: int = Attribute.Massive.value IS_PSIONIC: int = Attribute.Psionic.value UNIT_BATTLECRUISER: UnitTypeId = UnitTypeId.BATTLECRUISER UNIT_ORACLE: UnitTypeId = UnitTypeId.ORACLE TARGET_GROUND: set[int] = {TargetType.Ground.value, TargetType.Any.value} TARGET_AIR: set[int] = {TargetType.Air.value, TargetType.Any.value} TARGET_BOTH: set[int] = TARGET_GROUND | TARGET_AIR IS_SNAPSHOT = DisplayType.Snapshot.value IS_VISIBLE = DisplayType.Visible.value IS_PLACEHOLDER = DisplayType.Placeholder.value IS_MINE = Alliance.Self.value IS_ENEMY = Alliance.Enemy.value IS_CLOAKED: set[int] = {CloakState.Cloaked.value, CloakState.CloakedDetected.value, CloakState.CloakedAllied.value} IS_REVEALED: int = CloakState.CloakedDetected.value CAN_BE_ATTACKED: set[int] = {CloakState.NotCloaked.value, CloakState.CloakedDetected.value} IS_CARRYING_MINERALS: set[BuffId] = {BuffId.CARRYMINERALFIELDMINERALS, BuffId.CARRYHIGHYIELDMINERALFIELDMINERALS} IS_CARRYING_VESPENE: set[BuffId] = { BuffId.CARRYHARVESTABLEVESPENEGEYSERGAS, BuffId.CARRYHARVESTABLEVESPENEGEYSERGASPROTOSS, BuffId.CARRYHARVESTABLEVESPENEGEYSERGASZERG, } IS_CARRYING_RESOURCES: set[BuffId] = IS_CARRYING_MINERALS | IS_CARRYING_VESPENE IS_ATTACKING: set[AbilityId] = { AbilityId.ATTACK, AbilityId.ATTACK_ATTACK, AbilityId.ATTACK_ATTACKTOWARDS, AbilityId.ATTACK_ATTACKBARRAGE, AbilityId.SCAN_MOVE, } IS_PATROLLING: AbilityId = AbilityId.PATROL_PATROL IS_GATHERING: AbilityId = AbilityId.HARVEST_GATHER IS_RETURNING: AbilityId = AbilityId.HARVEST_RETURN IS_COLLECTING: set[AbilityId] = {IS_GATHERING, IS_RETURNING} IS_CONSTRUCTING_SCV: set[AbilityId] = { AbilityId.TERRANBUILD_ARMORY, AbilityId.TERRANBUILD_BARRACKS, AbilityId.TERRANBUILD_BUNKER, AbilityId.TERRANBUILD_COMMANDCENTER, AbilityId.TERRANBUILD_ENGINEERINGBAY, AbilityId.TERRANBUILD_FACTORY, AbilityId.TERRANBUILD_FUSIONCORE, AbilityId.TERRANBUILD_GHOSTACADEMY, AbilityId.TERRANBUILD_MISSILETURRET, AbilityId.TERRANBUILD_REFINERY, AbilityId.TERRANBUILD_SENSORTOWER, AbilityId.TERRANBUILD_STARPORT, AbilityId.TERRANBUILD_SUPPLYDEPOT, } IS_REPAIRING: set[AbilityId] = {AbilityId.EFFECT_REPAIR, AbilityId.EFFECT_REPAIR_MULE, AbilityId.EFFECT_REPAIR_SCV} IS_DETECTOR: set[UnitTypeId] = { UnitTypeId.OBSERVER, UnitTypeId.OBSERVERSIEGEMODE, UnitTypeId.RAVEN, UnitTypeId.MISSILETURRET, UnitTypeId.OVERSEER, UnitTypeId.OVERSEERSIEGEMODE, UnitTypeId.SPORECRAWLER, } SPEED_UPGRADE_DICT: dict[UnitTypeId, UpgradeId] = { # Terran UnitTypeId.MEDIVAC: UpgradeId.MEDIVACRAPIDDEPLOYMENT, UnitTypeId.BANSHEE: UpgradeId.BANSHEESPEED, # Protoss UnitTypeId.ZEALOT: UpgradeId.CHARGE, UnitTypeId.OBSERVER: UpgradeId.OBSERVERGRAVITICBOOSTER, UnitTypeId.WARPPRISM: UpgradeId.GRAVITICDRIVE, UnitTypeId.VOIDRAY: UpgradeId.VOIDRAYSPEEDUPGRADE, # Zerg UnitTypeId.OVERLORD: UpgradeId.OVERLORDSPEED, UnitTypeId.OVERSEER: UpgradeId.OVERLORDSPEED, UnitTypeId.ZERGLING: UpgradeId.ZERGLINGMOVEMENTSPEED, UnitTypeId.BANELING: UpgradeId.CENTRIFICALHOOKS, UnitTypeId.ROACH: UpgradeId.GLIALRECONSTITUTION, UnitTypeId.LURKERMP: UpgradeId.DIGGINGCLAWS, } SPEED_INCREASE_DICT: dict[UnitTypeId, float] = { # Terran UnitTypeId.MEDIVAC: 1.18, UnitTypeId.BANSHEE: 1.3636, # Protoss UnitTypeId.ZEALOT: 1.5, UnitTypeId.OBSERVER: 2, UnitTypeId.WARPPRISM: 1.3, UnitTypeId.VOIDRAY: 1.328, # Zerg UnitTypeId.OVERLORD: 2.915, UnitTypeId.OVERSEER: 1.8015, UnitTypeId.ZERGLING: 1.6, UnitTypeId.BANELING: 1.18, UnitTypeId.ROACH: 1.3333333333, UnitTypeId.LURKERMP: 1.1, } temp1 = set(SPEED_UPGRADE_DICT) temp2 = set(SPEED_INCREASE_DICT) assert temp1 == temp2, f"{temp1.symmetric_difference(temp2)}" del temp1 del temp2 SPEED_INCREASE_ON_CREEP_DICT: dict[UnitTypeId, float] = { UnitTypeId.QUEEN: 2.67, UnitTypeId.ZERGLING: 1.3, UnitTypeId.BANELING: 1.3, UnitTypeId.ROACH: 1.3, UnitTypeId.RAVAGER: 1.3, UnitTypeId.HYDRALISK: 1.3, UnitTypeId.LURKERMP: 1.3, UnitTypeId.ULTRALISK: 1.3, UnitTypeId.INFESTOR: 1.3, UnitTypeId.INFESTORTERRAN: 1.3, UnitTypeId.SWARMHOSTMP: 1.3, UnitTypeId.LOCUSTMP: 1.4, UnitTypeId.SPINECRAWLER: 2.5, UnitTypeId.SPORECRAWLER: 2.5, } OFF_CREEP_SPEED_UPGRADE_DICT: dict[UnitTypeId, UpgradeId] = { UnitTypeId.HYDRALISK: UpgradeId.EVOLVEMUSCULARAUGMENTS, UnitTypeId.ULTRALISK: UpgradeId.ANABOLICSYNTHESIS, } OFF_CREEP_SPEED_INCREASE_DICT: dict[UnitTypeId, float] = { UnitTypeId.HYDRALISK: 1.25, UnitTypeId.ULTRALISK: 1.2, } temp1 = set(OFF_CREEP_SPEED_UPGRADE_DICT) temp2 = set(OFF_CREEP_SPEED_INCREASE_DICT) assert temp1 == temp2, f"{temp1.symmetric_difference(temp2)}" del temp1 del temp2 # Movement speed gets altered by this factor if it is affected by this buff SPEED_ALTERING_BUFFS: dict[BuffId, float] = { # Stimpack increases speed by 1.5 BuffId.STIMPACK: 1.5, BuffId.STIMPACKMARAUDER: 1.5, BuffId.CHARGEUP: 2.2, # x2.8 speed up in pre version 4.11 # Concussive shells of Marauder reduce speed by 50% BuffId.DUTCHMARAUDERSLOW: 0.5, # Time Warp of Mothership reduces speed by 50% BuffId.TIMEWARPPRODUCTION: 0.5, # Fungal Growth of Infestor reduces speed by 75% BuffId.FUNGALGROWTH: 0.25, # Inhibitor Zones reduce speed by 35% BuffId.INHIBITORZONETEMPORALFIELD: 0.65, # TODO there is a new zone coming (acceleration zone) which increase movement speed, ultralisk will be affected by this } UNIT_PHOTONCANNON: UnitTypeId = UnitTypeId.PHOTONCANNON UNIT_COLOSSUS: UnitTypeId = UnitTypeId.COLOSSUS # Used in unit_command.py and action.py to combine only certain abilities COMBINEABLE_ABILITIES: set[AbilityId] = { AbilityId.MOVE, AbilityId.ATTACK, AbilityId.SCAN_MOVE, AbilityId.STOP, AbilityId.HOLDPOSITION, AbilityId.PATROL, AbilityId.HARVEST_GATHER, AbilityId.HARVEST_RETURN, AbilityId.EFFECT_REPAIR, AbilityId.LIFT, AbilityId.BURROWDOWN, AbilityId.BURROWUP, AbilityId.SIEGEMODE_SIEGEMODE, AbilityId.UNSIEGE_UNSIEGE, AbilityId.MORPH_LIBERATORAAMODE, AbilityId.EFFECT_STIM, AbilityId.MORPH_UPROOT, AbilityId.EFFECT_BLINK, AbilityId.MORPH_ARCHON, } FakeEffectRadii: dict[int, float] = { UnitTypeId.KD8CHARGE.value: 2, UnitTypeId.PARASITICBOMBDUMMY.value: 3, UnitTypeId.FORCEFIELD.value: 1.5, } FakeEffectID: dict[int, str] = { UnitTypeId.KD8CHARGE.value: "KD8CHARGE", UnitTypeId.PARASITICBOMBDUMMY.value: "PARASITICBOMB", UnitTypeId.FORCEFIELD.value: "FORCEFIELD", } TERRAN_STRUCTURES_REQUIRE_SCV: set[UnitTypeId] = { UnitTypeId.ARMORY, UnitTypeId.BARRACKS, UnitTypeId.BUNKER, UnitTypeId.COMMANDCENTER, UnitTypeId.ENGINEERINGBAY, UnitTypeId.FACTORY, UnitTypeId.FUSIONCORE, UnitTypeId.GHOSTACADEMY, UnitTypeId.MISSILETURRET, UnitTypeId.REFINERY, UnitTypeId.REFINERYRICH, UnitTypeId.SENSORTOWER, UnitTypeId.STARPORT, UnitTypeId.SUPPLYDEPOT, } def return_NOTAUNIT() -> UnitTypeId: # NOTAUNIT = 0 return UnitTypeId.NOTAUNIT # Hotfix for structures and units as the API does not seem to return the correct values, e.g. ghost and thor have None in the requirements TERRAN_TECH_REQUIREMENT: dict[UnitTypeId, UnitTypeId] = defaultdict( return_NOTAUNIT, { UnitTypeId.MISSILETURRET: UnitTypeId.ENGINEERINGBAY, UnitTypeId.SENSORTOWER: UnitTypeId.ENGINEERINGBAY, UnitTypeId.PLANETARYFORTRESS: UnitTypeId.ENGINEERINGBAY, UnitTypeId.BARRACKS: UnitTypeId.SUPPLYDEPOT, UnitTypeId.ORBITALCOMMAND: UnitTypeId.BARRACKS, UnitTypeId.BUNKER: UnitTypeId.BARRACKS, UnitTypeId.GHOST: UnitTypeId.GHOSTACADEMY, UnitTypeId.GHOSTACADEMY: UnitTypeId.BARRACKS, UnitTypeId.FACTORY: UnitTypeId.BARRACKS, UnitTypeId.ARMORY: UnitTypeId.FACTORY, UnitTypeId.HELLIONTANK: UnitTypeId.ARMORY, UnitTypeId.THOR: UnitTypeId.ARMORY, UnitTypeId.STARPORT: UnitTypeId.FACTORY, UnitTypeId.FUSIONCORE: UnitTypeId.STARPORT, UnitTypeId.BATTLECRUISER: UnitTypeId.FUSIONCORE, }, ) PROTOSS_TECH_REQUIREMENT: dict[UnitTypeId, UnitTypeId] = defaultdict( return_NOTAUNIT, { UnitTypeId.PHOTONCANNON: UnitTypeId.FORGE, UnitTypeId.CYBERNETICSCORE: UnitTypeId.GATEWAY, UnitTypeId.SENTRY: UnitTypeId.CYBERNETICSCORE, UnitTypeId.STALKER: UnitTypeId.CYBERNETICSCORE, UnitTypeId.ADEPT: UnitTypeId.CYBERNETICSCORE, UnitTypeId.TWILIGHTCOUNCIL: UnitTypeId.CYBERNETICSCORE, UnitTypeId.SHIELDBATTERY: UnitTypeId.CYBERNETICSCORE, UnitTypeId.TEMPLARARCHIVE: UnitTypeId.TWILIGHTCOUNCIL, UnitTypeId.DARKSHRINE: UnitTypeId.TWILIGHTCOUNCIL, UnitTypeId.HIGHTEMPLAR: UnitTypeId.TEMPLARARCHIVE, UnitTypeId.DARKTEMPLAR: UnitTypeId.DARKSHRINE, UnitTypeId.STARGATE: UnitTypeId.CYBERNETICSCORE, UnitTypeId.TEMPEST: UnitTypeId.FLEETBEACON, UnitTypeId.CARRIER: UnitTypeId.FLEETBEACON, UnitTypeId.MOTHERSHIP: UnitTypeId.FLEETBEACON, UnitTypeId.ROBOTICSFACILITY: UnitTypeId.CYBERNETICSCORE, UnitTypeId.ROBOTICSBAY: UnitTypeId.ROBOTICSFACILITY, UnitTypeId.COLOSSUS: UnitTypeId.ROBOTICSBAY, UnitTypeId.DISRUPTOR: UnitTypeId.ROBOTICSBAY, UnitTypeId.FLEETBEACON: UnitTypeId.STARGATE, }, ) ZERG_TECH_REQUIREMENT: dict[UnitTypeId, UnitTypeId] = defaultdict( return_NOTAUNIT, { UnitTypeId.ZERGLING: UnitTypeId.SPAWNINGPOOL, UnitTypeId.QUEEN: UnitTypeId.SPAWNINGPOOL, UnitTypeId.ROACHWARREN: UnitTypeId.SPAWNINGPOOL, UnitTypeId.BANELINGNEST: UnitTypeId.SPAWNINGPOOL, UnitTypeId.SPINECRAWLER: UnitTypeId.SPAWNINGPOOL, UnitTypeId.SPORECRAWLER: UnitTypeId.SPAWNINGPOOL, UnitTypeId.ROACH: UnitTypeId.ROACHWARREN, UnitTypeId.BANELING: UnitTypeId.BANELINGNEST, UnitTypeId.LAIR: UnitTypeId.SPAWNINGPOOL, UnitTypeId.OVERSEER: UnitTypeId.LAIR, UnitTypeId.OVERLORDTRANSPORT: UnitTypeId.LAIR, UnitTypeId.INFESTATIONPIT: UnitTypeId.LAIR, UnitTypeId.INFESTOR: UnitTypeId.INFESTATIONPIT, UnitTypeId.SWARMHOSTMP: UnitTypeId.INFESTATIONPIT, UnitTypeId.HYDRALISKDEN: UnitTypeId.LAIR, UnitTypeId.HYDRALISK: UnitTypeId.HYDRALISKDEN, UnitTypeId.LURKERDENMP: UnitTypeId.HYDRALISKDEN, UnitTypeId.LURKERMP: UnitTypeId.LURKERDENMP, UnitTypeId.SPIRE: UnitTypeId.LAIR, UnitTypeId.MUTALISK: UnitTypeId.SPIRE, UnitTypeId.CORRUPTOR: UnitTypeId.SPIRE, UnitTypeId.NYDUSNETWORK: UnitTypeId.LAIR, UnitTypeId.HIVE: UnitTypeId.INFESTATIONPIT, UnitTypeId.VIPER: UnitTypeId.HIVE, UnitTypeId.ULTRALISKCAVERN: UnitTypeId.HIVE, UnitTypeId.GREATERSPIRE: UnitTypeId.HIVE, UnitTypeId.BROODLORD: UnitTypeId.GREATERSPIRE, }, ) # Required in 'tech_requirement_progress' bot_ai.py function EQUIVALENTS_FOR_TECH_PROGRESS: dict[UnitTypeId, set[UnitTypeId]] = { # Protoss UnitTypeId.GATEWAY: {UnitTypeId.WARPGATE}, UnitTypeId.WARPPRISM: {UnitTypeId.WARPPRISMPHASING}, UnitTypeId.OBSERVER: {UnitTypeId.OBSERVERSIEGEMODE}, # Terran UnitTypeId.SUPPLYDEPOT: {UnitTypeId.SUPPLYDEPOTLOWERED, UnitTypeId.SUPPLYDEPOTDROP}, UnitTypeId.BARRACKS: {UnitTypeId.BARRACKSFLYING}, UnitTypeId.FACTORY: {UnitTypeId.FACTORYFLYING}, UnitTypeId.STARPORT: {UnitTypeId.STARPORTFLYING}, UnitTypeId.COMMANDCENTER: { UnitTypeId.COMMANDCENTERFLYING, UnitTypeId.PLANETARYFORTRESS, UnitTypeId.ORBITALCOMMAND, UnitTypeId.ORBITALCOMMANDFLYING, }, UnitTypeId.ORBITALCOMMAND: {UnitTypeId.ORBITALCOMMANDFLYING}, UnitTypeId.HELLION: {UnitTypeId.HELLIONTANK}, UnitTypeId.WIDOWMINE: {UnitTypeId.WIDOWMINEBURROWED}, UnitTypeId.SIEGETANK: {UnitTypeId.SIEGETANKSIEGED}, UnitTypeId.THOR: {UnitTypeId.THORAP}, UnitTypeId.VIKINGFIGHTER: {UnitTypeId.VIKINGASSAULT}, UnitTypeId.LIBERATOR: {UnitTypeId.LIBERATORAG}, # Zerg UnitTypeId.LAIR: {UnitTypeId.HIVE}, UnitTypeId.HATCHERY: {UnitTypeId.LAIR, UnitTypeId.HIVE}, UnitTypeId.SPIRE: {UnitTypeId.GREATERSPIRE}, UnitTypeId.SPINECRAWLER: {UnitTypeId.SPINECRAWLERUPROOTED}, UnitTypeId.SPORECRAWLER: {UnitTypeId.SPORECRAWLERUPROOTED}, UnitTypeId.OVERLORD: {UnitTypeId.OVERLORDTRANSPORT}, UnitTypeId.OVERSEER: {UnitTypeId.OVERSEERSIEGEMODE}, UnitTypeId.DRONE: {UnitTypeId.DRONEBURROWED}, UnitTypeId.ZERGLING: {UnitTypeId.ZERGLINGBURROWED}, UnitTypeId.ROACH: {UnitTypeId.ROACHBURROWED}, UnitTypeId.RAVAGER: {UnitTypeId.RAVAGERBURROWED}, UnitTypeId.HYDRALISK: {UnitTypeId.HYDRALISKBURROWED}, UnitTypeId.LURKERMP: {UnitTypeId.LURKERMPBURROWED}, UnitTypeId.SWARMHOSTMP: {UnitTypeId.SWARMHOSTBURROWEDMP}, UnitTypeId.INFESTOR: {UnitTypeId.INFESTORBURROWED}, UnitTypeId.ULTRALISK: {UnitTypeId.ULTRALISKBURROWED}, # TODO What about morphing untis? E.g. roach to ravager, overlord to drop-overlord or overseer } ALL_GAS: set[UnitTypeId] = { UnitTypeId.ASSIMILATOR, UnitTypeId.ASSIMILATORRICH, UnitTypeId.REFINERY, UnitTypeId.REFINERYRICH, UnitTypeId.EXTRACTOR, UnitTypeId.EXTRACTORRICH, } DAMAGE_BONUS_PER_UPGRADE: dict[UnitTypeId, dict[int, Any]] = { # # Protoss # UnitTypeId.PROBE: {TargetType.Ground.value: {None: 0}}, # Gateway Units UnitTypeId.ADEPT: {TargetType.Ground.value: {IS_LIGHT: 1}}, UnitTypeId.STALKER: {TargetType.Any.value: {IS_ARMORED: 1}}, UnitTypeId.DARKTEMPLAR: {TargetType.Ground.value: {None: 5}}, UnitTypeId.ARCHON: {TargetType.Any.value: {None: 3, IS_BIOLOGICAL: 1}}, # Robo Units UnitTypeId.IMMORTAL: {TargetType.Ground.value: {None: 2, IS_ARMORED: 3}}, UnitTypeId.COLOSSUS: {TargetType.Ground.value: {IS_LIGHT: 1}}, # Stargate Units UnitTypeId.ORACLE: {TargetType.Ground.value: {None: 0}}, UnitTypeId.TEMPEST: {TargetType.Ground.value: {None: 4}, TargetType.Air.value: {None: 3, IS_MASSIVE: 2}}, # # Terran # UnitTypeId.SCV: {TargetType.Ground.value: {None: 0}}, # Barracks Units UnitTypeId.MARAUDER: {TargetType.Ground.value: {IS_ARMORED: 1}}, UnitTypeId.GHOST: {TargetType.Any.value: {IS_LIGHT: 1}}, # Factory Units UnitTypeId.HELLION: {TargetType.Ground.value: {IS_LIGHT: 1}}, UnitTypeId.HELLIONTANK: {TargetType.Ground.value: {None: 2, IS_LIGHT: 1}}, UnitTypeId.CYCLONE: {TargetType.Any.value: {None: 2}}, UnitTypeId.SIEGETANK: {TargetType.Ground.value: {None: 2, IS_ARMORED: 1}}, UnitTypeId.SIEGETANKSIEGED: {TargetType.Ground.value: {None: 4, IS_ARMORED: 1}}, UnitTypeId.THOR: {TargetType.Ground.value: {None: 3}, TargetType.Air.value: {IS_LIGHT: 1}}, UnitTypeId.THORAP: {TargetType.Ground.value: {None: 3}, TargetType.Air.value: {None: 3, IS_MASSIVE: 1}}, # Starport Units UnitTypeId.VIKINGASSAULT: {TargetType.Ground.value: {IS_MECHANICAL: 1}}, UnitTypeId.LIBERATORAG: {TargetType.Ground.value: {None: 5}}, # # Zerg # UnitTypeId.DRONE: {TargetType.Ground.value: {None: 0}}, # Hatch Tech Units (Queen, Ling, Bane, Roach, Ravager) UnitTypeId.BANELING: {TargetType.Ground.value: {None: 2, IS_LIGHT: 2, IS_STRUCTURE: 3}}, UnitTypeId.ROACH: {TargetType.Ground.value: {None: 2}}, UnitTypeId.RAVAGER: {TargetType.Ground.value: {None: 2}}, # Lair Tech Units (Hydra, Lurker, Ultra) UnitTypeId.LURKERMPBURROWED: {TargetType.Ground.value: {None: 2, IS_ARMORED: 1}}, UnitTypeId.ULTRALISK: {TargetType.Ground.value: {None: 3}}, # Spire Units (Muta, Corruptor, BL) UnitTypeId.CORRUPTOR: {TargetType.Air.value: {IS_MASSIVE: 1}}, UnitTypeId.BROODLORD: {TargetType.Ground.value: {None: 2}}, } TARGET_HELPER = { 1: "no target", 2: "Point2", 3: "Unit", 4: "Point2 or Unit", 5: "Point2 or no target", } CREATION_ABILITY_FIX: dict[UnitTypeId, AbilityId] = { UnitTypeId.ARCHON: AbilityId.ARCHON_WARP_TARGET, UnitTypeId.ASSIMILATORRICH: AbilityId.PROTOSSBUILD_ASSIMILATOR, UnitTypeId.BANELINGCOCOON: AbilityId.MORPHZERGLINGTOBANELING_BANELING, UnitTypeId.CHANGELING: AbilityId.SPAWNCHANGELING_SPAWNCHANGELING, UnitTypeId.EXTRACTORRICH: AbilityId.ZERGBUILD_EXTRACTOR, UnitTypeId.INTERCEPTOR: AbilityId.BUILD_INTERCEPTORS, UnitTypeId.LURKERMPEGG: AbilityId.MORPH_LURKER, UnitTypeId.MULE: AbilityId.CALLDOWNMULE_CALLDOWNMULE, UnitTypeId.RAVAGERCOCOON: AbilityId.MORPHTORAVAGER_RAVAGER, UnitTypeId.REFINERYRICH: AbilityId.TERRANBUILD_REFINERY, UnitTypeId.TECHLAB: AbilityId.BUILD_TECHLAB, } ================================================ FILE: sc2/controller.py ================================================ from __future__ import annotations import platform from pathlib import Path from typing import TYPE_CHECKING from aiohttp import ClientWebSocketResponse from loguru import logger from s2clientprotocol import sc2api_pb2 as sc_pb from sc2.player import Computer from sc2.protocol import Protocol if TYPE_CHECKING: from sc2.sc2process import SC2Process class Controller(Protocol): def __init__(self, ws: ClientWebSocketResponse, process: SC2Process) -> None: super().__init__(ws) self._process = process @property def running(self) -> bool: return self._process._process is not None async def create_game(self, game_map, players, realtime: bool, random_seed=None, disable_fog=None): req = sc_pb.RequestCreateGame( local_map=sc_pb.LocalMap(map_path=str(game_map.relative_path)), realtime=realtime, # pyrefly: ignore disable_fog=disable_fog, ) if random_seed is not None: req.random_seed = random_seed for player in players: # pyrefly: ignore p = req.player_setup.add() p.type = player.type.value if isinstance(player, Computer): # pyrefly: ignore[bad-assignment] p.race = player.race.value # pyrefly: ignore[bad-assignment] p.difficulty = player.difficulty.value if player.ai_build is not None: # pyrefly: ignore[bad-assignment] p.ai_build = player.ai_build.value logger.info("Creating new game") logger.info(f"Map: {game_map.name}") logger.info(f"Players: {', '.join(str(p) for p in players)}") result = await self._execute(create_game=req) return result async def request_available_maps(self): req = sc_pb.RequestAvailableMaps() result = await self._execute(available_maps=req) return result async def request_save_map(self, download_path: str): """Not working on linux.""" req = sc_pb.RequestSaveMap(map_path=download_path) result = await self._execute(save_map=req) return result async def request_replay_info(self, replay_path: str): """Not working on linux.""" req = sc_pb.RequestReplayInfo(replay_path=replay_path, download_data=False) result = await self._execute(replay_info=req) return result async def start_replay(self, replay_path: str, realtime: bool, observed_id: int = 0): ifopts = sc_pb.InterfaceOptions( raw=True, score=True, show_cloaked=True, raw_affects_selection=True, raw_crop_to_playable_area=False ) if platform.system() == "Linux": replay_name = Path(replay_path).name home_replay_folder = Path.home() / "Documents" / "StarCraft II" / "Replays" if str(home_replay_folder / replay_name) != replay_path: logger.warning( f"Linux detected, please put your replay in your home directory at {home_replay_folder}. It was detected at {replay_path}" ) raise FileNotFoundError replay_path = replay_name req = sc_pb.RequestStartReplay( replay_path=replay_path, observed_player_id=observed_id, realtime=realtime, options=ifopts ) result = await self._execute(start_replay=req) assert result.status == 4, f"{result.start_replay.error} - {result.start_replay.error_details}" return result ================================================ FILE: sc2/data.py ================================================ """For the list of enums, see here https://github.com/Blizzard/s2client-proto/tree/bff45dae1fc685e6acbaae084670afb7d1c0832c/s2clientprotocol """ from __future__ import annotations import enum from s2clientprotocol import common_pb2 as common_pb from s2clientprotocol import data_pb2 as data_pb from s2clientprotocol import error_pb2 as error_pb from s2clientprotocol import raw_pb2 as raw_pb from s2clientprotocol import sc2api_pb2 as sc_pb from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId CreateGameError = enum.Enum("CreateGameError", sc_pb.ResponseCreateGame.Error.items()) PlayerType = enum.Enum("PlayerType", sc_pb.PlayerType.items()) Difficulty = enum.Enum("Difficulty", sc_pb.Difficulty.items()) AIBuild = enum.Enum("AIBuild", sc_pb.AIBuild.items()) Status = enum.Enum("Status", sc_pb.Status.items()) Result = enum.Enum("Result", sc_pb.Result.items()) Alert = enum.Enum("Alert", sc_pb.Alert.items()) ChatChannel = enum.Enum("ChatChannel", sc_pb.ActionChat.Channel.items()) Race = enum.Enum("Race", common_pb.Race.items()) DisplayType = enum.Enum("DisplayType", raw_pb.DisplayType.items()) Alliance = enum.Enum("Alliance", raw_pb.Alliance.items()) CloakState = enum.Enum("CloakState", raw_pb.CloakState.items()) Attribute = enum.Enum("Attribute", data_pb.Attribute.items()) TargetType = enum.Enum("TargetType", data_pb.Weapon.TargetType.items()) Target = enum.Enum("Target", data_pb.AbilityData.Target.items()) ActionResult = enum.Enum("ActionResult", error_pb.ActionResult.items()) race_worker: dict[Race, UnitTypeId] = { Race.Protoss: UnitTypeId.PROBE, Race.Terran: UnitTypeId.SCV, Race.Zerg: UnitTypeId.DRONE, } race_townhalls: dict[Race, set[UnitTypeId]] = { Race.Protoss: {UnitTypeId.NEXUS}, Race.Terran: { UnitTypeId.COMMANDCENTER, UnitTypeId.ORBITALCOMMAND, UnitTypeId.PLANETARYFORTRESS, UnitTypeId.COMMANDCENTERFLYING, UnitTypeId.ORBITALCOMMANDFLYING, }, Race.Zerg: {UnitTypeId.HATCHERY, UnitTypeId.LAIR, UnitTypeId.HIVE}, Race.Random: { # Protoss UnitTypeId.NEXUS, # Terran UnitTypeId.COMMANDCENTER, UnitTypeId.ORBITALCOMMAND, UnitTypeId.PLANETARYFORTRESS, UnitTypeId.COMMANDCENTERFLYING, UnitTypeId.ORBITALCOMMANDFLYING, # Zerg UnitTypeId.HATCHERY, UnitTypeId.LAIR, UnitTypeId.HIVE, }, } warpgate_abilities: dict[AbilityId, AbilityId] = { AbilityId.GATEWAYTRAIN_ZEALOT: AbilityId.WARPGATETRAIN_ZEALOT, AbilityId.GATEWAYTRAIN_STALKER: AbilityId.WARPGATETRAIN_STALKER, AbilityId.GATEWAYTRAIN_HIGHTEMPLAR: AbilityId.WARPGATETRAIN_HIGHTEMPLAR, AbilityId.GATEWAYTRAIN_DARKTEMPLAR: AbilityId.WARPGATETRAIN_DARKTEMPLAR, AbilityId.GATEWAYTRAIN_SENTRY: AbilityId.WARPGATETRAIN_SENTRY, AbilityId.TRAIN_ADEPT: AbilityId.TRAINWARP_ADEPT, } race_gas: dict[Race, UnitTypeId] = { Race.Protoss: UnitTypeId.ASSIMILATOR, Race.Terran: UnitTypeId.REFINERY, Race.Zerg: UnitTypeId.EXTRACTOR, } ================================================ FILE: sc2/data.pyi ================================================ """Type stubs for sc2.data module This stub provides static type information for dynamically generated enums. The enums in sc2.data are created at runtime using enum.Enum() with protobuf enum descriptors, which makes them invisible to static type checkers. This stub file (PEP 561 compliant) allows type checkers like Pylance, Pyright, and mypy to understand the structure and members of these enums. """ from __future__ import annotations from enum import Enum from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId class CreateGameError(Enum): MissingMap = 1 InvalidMapPath = 2 InvalidMapData = 3 InvalidMapName = 4 InvalidMapHandle = 5 MissingPlayerSetup = 6 InvalidPlayerSetup = 7 MultiplayerUnsupported = 8 class PlayerType(Enum): Participant = 1 Computer = 2 Observer = 3 class Difficulty(Enum): VeryEasy = 1 Easy = 2 Medium = 3 MediumHard = 4 Hard = 5 Harder = 6 VeryHard = 7 CheatVision = 8 CheatMoney = 9 CheatInsane = 10 class AIBuild(Enum): RandomBuild = 1 Rush = 2 Timing = 3 Power = 4 Macro = 5 Air = 6 class Status(Enum): launched = 1 init_game = 2 in_game = 3 in_replay = 4 ended = 5 quit = 6 unknown = 7 class Result(Enum): Victory = 1 Defeat = 2 Tie = 3 Undecided = 4 class Alert(Enum): AlertError = 1 AddOnComplete = 2 BuildingComplete = 3 BuildingUnderAttack = 4 LarvaHatched = 5 MergeComplete = 6 MineralsExhausted = 7 MorphComplete = 8 MothershipComplete = 9 MULEExpired = 10 NuclearLaunchDetected = 11 NukeComplete = 12 NydusWormDetected = 13 ResearchComplete = 14 TrainError = 15 TrainUnitComplete = 16 TrainWorkerComplete = 17 TransformationComplete = 18 UnitUnderAttack = 19 UpgradeComplete = 20 VespeneExhausted = 21 WarpInComplete = 22 class ChatChannel(Enum): Broadcast = 1 Team = 2 class Race(Enum): """StarCraft II race enum. Members: NoRace: No race specified Terran: Terran race Zerg: Zerg race Protoss: Protoss race Random: Random race selection """ NoRace = 0 Terran = 1 Zerg = 2 Protoss = 3 Random = 4 # Enums created from raw_pb2 class DisplayType(Enum): Visible = 1 Snapshot = 2 Hidden = 3 Placeholder = 4 class Alliance(Enum): Self = 1 Ally = 2 Neutral = 3 Enemy = 4 class CloakState(Enum): CloakedUnknown = 1 Cloaked = 2 CloakedDetected = 3 NotCloaked = 4 CloakedAllied = 5 class Attribute(Enum): Light = 1 Armored = 2 Biological = 3 Mechanical = 4 Robotic = 5 Psionic = 6 Massive = 7 Structure = 8 Hover = 9 Heroic = 10 Summoned = 11 class TargetType(Enum): Ground = 1 Air = 2 Any = 3 Invalid = 4 class Target(Enum): # Note: The protobuf enum member 'None' is a Python keyword, # so at runtime it may need special handling Point = 1 Unit = 2 PointOrUnit = 3 PointOrNone = 4 class ActionResult(Enum): """Action result codes from game engine. This enum contains a large number of members (~200+) representing various action results and error conditions. """ Success = 1 NotSupported = 2 Error = 3 CantQueueThatOrder = 4 Retry = 5 Cooldown = 6 QueueIsFull = 7 RallyQueueIsFull = 8 NotEnoughMinerals = 9 NotEnoughVespene = 10 NotEnoughTerrazine = 11 NotEnoughCustom = 12 NotEnoughFood = 13 FoodUsageImpossible = 14 NotEnoughLife = 15 NotEnoughShields = 16 NotEnoughEnergy = 17 LifeSuppressed = 18 ShieldsSuppressed = 19 EnergySuppressed = 20 NotEnoughCharges = 21 CantAddMoreCharges = 22 TooMuchMinerals = 23 TooMuchVespene = 24 TooMuchTerrazine = 25 TooMuchCustom = 26 TooMuchFood = 27 TooMuchLife = 28 TooMuchShields = 29 TooMuchEnergy = 30 MustTargetUnitWithLife = 31 MustTargetUnitWithShields = 32 MustTargetUnitWithEnergy = 33 CantTrade = 34 CantSpend = 35 CantTargetThatUnit = 36 CouldntAllocateUnit = 37 UnitCantMove = 38 TransportIsHoldingPosition = 39 BuildTechRequirementsNotMet = 40 CantFindPlacementLocation = 41 CantBuildOnThat = 42 CantBuildTooCloseToDropOff = 43 CantBuildLocationInvalid = 44 CantSeeBuildLocation = 45 CantBuildTooCloseToCreepSource = 46 CantBuildTooCloseToResources = 47 CantBuildTooFarFromWater = 48 CantBuildTooFarFromCreepSource = 49 CantBuildTooFarFromBuildPowerSource = 50 CantBuildOnDenseTerrain = 51 CantTrainTooFarFromTrainPowerSource = 52 CantLandLocationInvalid = 53 CantSeeLandLocation = 54 CantLandTooCloseToCreepSource = 55 CantLandTooCloseToResources = 56 CantLandTooFarFromWater = 57 CantLandTooFarFromCreepSource = 58 CantLandTooFarFromBuildPowerSource = 59 CantLandTooFarFromTrainPowerSource = 60 CantLandOnDenseTerrain = 61 AddOnTooFarFromBuilding = 62 MustBuildRefineryFirst = 63 BuildingIsUnderConstruction = 64 CantFindDropOff = 65 CantLoadOtherPlayersUnits = 66 NotEnoughRoomToLoadUnit = 67 CantUnloadUnitsThere = 68 CantWarpInUnitsThere = 69 CantLoadImmobileUnits = 70 CantRechargeImmobileUnits = 71 CantRechargeUnderConstructionUnits = 72 CantLoadThatUnit = 73 NoCargoToUnload = 74 LoadAllNoTargetsFound = 75 NotWhileOccupied = 76 CantAttackWithoutAmmo = 77 CantHoldAnyMoreAmmo = 78 TechRequirementsNotMet = 79 MustLockdownUnitFirst = 80 MustTargetUnit = 81 MustTargetInventory = 82 MustTargetVisibleUnit = 83 MustTargetVisibleLocation = 84 MustTargetWalkableLocation = 85 MustTargetPawnableUnit = 86 YouCantControlThatUnit = 87 YouCantIssueCommandsToThatUnit = 88 MustTargetResources = 89 RequiresHealTarget = 90 RequiresRepairTarget = 91 NoItemsToDrop = 92 CantHoldAnyMoreItems = 93 CantHoldThat = 94 TargetHasNoInventory = 95 CantDropThisItem = 96 CantMoveThisItem = 97 CantPawnThisUnit = 98 MustTargetCaster = 99 CantTargetCaster = 100 MustTargetOuter = 101 CantTargetOuter = 102 MustTargetYourOwnUnits = 103 CantTargetYourOwnUnits = 104 MustTargetFriendlyUnits = 105 CantTargetFriendlyUnits = 106 MustTargetNeutralUnits = 107 CantTargetNeutralUnits = 108 MustTargetEnemyUnits = 109 CantTargetEnemyUnits = 110 MustTargetAirUnits = 111 CantTargetAirUnits = 112 MustTargetGroundUnits = 113 CantTargetGroundUnits = 114 MustTargetStructures = 115 CantTargetStructures = 116 MustTargetLightUnits = 117 CantTargetLightUnits = 118 MustTargetArmoredUnits = 119 CantTargetArmoredUnits = 120 MustTargetBiologicalUnits = 121 CantTargetBiologicalUnits = 122 MustTargetHeroicUnits = 123 CantTargetHeroicUnits = 124 MustTargetRoboticUnits = 125 CantTargetRoboticUnits = 126 MustTargetMechanicalUnits = 127 CantTargetMechanicalUnits = 128 MustTargetPsionicUnits = 129 CantTargetPsionicUnits = 130 MustTargetMassiveUnits = 131 CantTargetMassiveUnits = 132 MustTargetMissile = 133 CantTargetMissile = 134 MustTargetWorkerUnits = 135 CantTargetWorkerUnits = 136 MustTargetEnergyCapableUnits = 137 CantTargetEnergyCapableUnits = 138 MustTargetShieldCapableUnits = 139 CantTargetShieldCapableUnits = 140 MustTargetFlyers = 141 CantTargetFlyers = 142 MustTargetBuriedUnits = 143 CantTargetBuriedUnits = 144 MustTargetCloakedUnits = 145 CantTargetCloakedUnits = 146 MustTargetUnitsInAStasisField = 147 CantTargetUnitsInAStasisField = 148 MustTargetUnderConstructionUnits = 149 CantTargetUnderConstructionUnits = 150 MustTargetDeadUnits = 151 CantTargetDeadUnits = 152 MustTargetRevivableUnits = 153 CantTargetRevivableUnits = 154 MustTargetHiddenUnits = 155 CantTargetHiddenUnits = 156 CantRechargeOtherPlayersUnits = 157 MustTargetHallucinations = 158 CantTargetHallucinations = 159 MustTargetInvulnerableUnits = 160 CantTargetInvulnerableUnits = 161 MustTargetDetectedUnits = 162 CantTargetDetectedUnits = 163 CantTargetUnitWithEnergy = 164 CantTargetUnitWithShields = 165 MustTargetUncommandableUnits = 166 CantTargetUncommandableUnits = 167 MustTargetPreventDefeatUnits = 168 CantTargetPreventDefeatUnits = 169 MustTargetPreventRevealUnits = 170 CantTargetPreventRevealUnits = 171 MustTargetPassiveUnits = 172 CantTargetPassiveUnits = 173 MustTargetStunnedUnits = 174 CantTargetStunnedUnits = 175 MustTargetSummonedUnits = 176 CantTargetSummonedUnits = 177 MustTargetUser1 = 178 CantTargetUser1 = 179 MustTargetUnstoppableUnits = 180 CantTargetUnstoppableUnits = 181 MustTargetResistantUnits = 182 CantTargetResistantUnits = 183 MustTargetDazedUnits = 184 CantTargetDazedUnits = 185 CantLockdown = 186 CantMindControl = 187 MustTargetDestructibles = 188 CantTargetDestructibles = 189 MustTargetItems = 190 CantTargetItems = 191 NoCalldownAvailable = 192 WaypointListFull = 193 MustTargetRace = 194 CantTargetRace = 195 MustTargetSimilarUnits = 196 CantTargetSimilarUnits = 197 CantFindEnoughTargets = 198 AlreadySpawningLarva = 199 CantTargetExhaustedResources = 200 CantUseMinimap = 201 CantUseInfoPanel = 202 OrderQueueIsFull = 203 CantHarvestThatResource = 204 HarvestersNotRequired = 205 AlreadyTargeted = 206 CantAttackWeaponsDisabled = 207 CouldntReachTarget = 208 TargetIsOutOfRange = 209 TargetIsTooClose = 210 TargetIsOutOfArc = 211 CantFindTeleportLocation = 212 InvalidItemClass = 213 CantFindCancelOrder = 214 # Module-level dictionaries race_worker: dict[Race, UnitTypeId] race_townhalls: dict[Race, set[UnitTypeId]] warpgate_abilities: dict[AbilityId, AbilityId] race_gas: dict[Race, UnitTypeId] ================================================ FILE: sc2/dicts/__init__.py ================================================ # DO NOT EDIT! # This file was automatically generated by "generate_dicts_from_data_json.py" __all__ = [ "generic_redirect_abilities", "unit_abilities", "unit_research_abilities", "unit_tech_alias", "unit_train_build_abilities", "unit_trained_from", "unit_unit_alias", "upgrade_researched_from", ] ================================================ FILE: sc2/dicts/generic_redirect_abilities.py ================================================ # THIS FILE WAS AUTOMATICALLY GENERATED BY "generate_dicts_from_data_json.py" DO NOT CHANGE MANUALLY! # ANY CHANGE WILL BE OVERWRITTEN from sc2.ids.ability_id import AbilityId # from sc2.ids.buff_id import BuffId # from sc2.ids.effect_id import EffectId GENERIC_REDIRECT_ABILITIES: dict[AbilityId, AbilityId] = { AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL1: AbilityId.RESEARCH_TERRANSHIPWEAPONS, AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL2: AbilityId.RESEARCH_TERRANSHIPWEAPONS, AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL3: AbilityId.RESEARCH_TERRANSHIPWEAPONS, AbilityId.ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL1: AbilityId.RESEARCH_TERRANVEHICLEANDSHIPPLATING, AbilityId.ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL2: AbilityId.RESEARCH_TERRANVEHICLEANDSHIPPLATING, AbilityId.ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL3: AbilityId.RESEARCH_TERRANVEHICLEANDSHIPPLATING, AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL1: AbilityId.RESEARCH_TERRANVEHICLEWEAPONS, AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL2: AbilityId.RESEARCH_TERRANVEHICLEWEAPONS, AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL3: AbilityId.RESEARCH_TERRANVEHICLEWEAPONS, AbilityId.ATTACKPROTOSSBUILDING_ATTACKBUILDING: AbilityId.ATTACK, AbilityId.ATTACK_ATTACK: AbilityId.ATTACK, AbilityId.ATTACK_BATTLECRUISER: AbilityId.ATTACK, AbilityId.ATTACK_REDIRECT: AbilityId.ATTACK, AbilityId.BEHAVIOR_CLOAKOFF_BANSHEE: AbilityId.BEHAVIOR_CLOAKOFF, AbilityId.BEHAVIOR_CLOAKOFF_GHOST: AbilityId.BEHAVIOR_CLOAKOFF, AbilityId.BEHAVIOR_CLOAKON_BANSHEE: AbilityId.BEHAVIOR_CLOAKON, AbilityId.BEHAVIOR_CLOAKON_GHOST: AbilityId.BEHAVIOR_CLOAKON, AbilityId.BEHAVIOR_HOLDFIREOFF_GHOST: AbilityId.BEHAVIOR_HOLDFIREOFF, AbilityId.BEHAVIOR_HOLDFIREOFF_LURKER: AbilityId.BEHAVIOR_HOLDFIREOFF, AbilityId.BEHAVIOR_HOLDFIREON_GHOST: AbilityId.BEHAVIOR_HOLDFIREON, AbilityId.BEHAVIOR_HOLDFIREON_LURKER: AbilityId.BEHAVIOR_HOLDFIREON, AbilityId.BROODLORDQUEUE2_CANCEL: AbilityId.CANCEL_LAST, AbilityId.BROODLORDQUEUE2_CANCELSLOT: AbilityId.CANCEL_SLOT, AbilityId.BUILDINPROGRESSNYDUSCANAL_CANCEL: AbilityId.CANCEL, AbilityId.BUILDNYDUSCANAL_CANCEL: AbilityId.HALT, AbilityId.BUILD_CREEPTUMOR_QUEEN: AbilityId.BUILD_CREEPTUMOR, AbilityId.BUILD_CREEPTUMOR_TUMOR: AbilityId.BUILD_CREEPTUMOR, AbilityId.BUILD_REACTOR_BARRACKS: AbilityId.BUILD_REACTOR, AbilityId.BUILD_REACTOR_FACTORY: AbilityId.BUILD_REACTOR, AbilityId.BUILD_REACTOR_STARPORT: AbilityId.BUILD_REACTOR, AbilityId.BUILD_TECHLAB_BARRACKS: AbilityId.BUILD_TECHLAB, AbilityId.BUILD_TECHLAB_FACTORY: AbilityId.BUILD_TECHLAB, AbilityId.BUILD_TECHLAB_STARPORT: AbilityId.BUILD_TECHLAB, AbilityId.BURROWBANELINGDOWN_CANCEL: AbilityId.CANCEL, AbilityId.BURROWCREEPTUMORDOWN_BURROWDOWN: AbilityId.BURROWDOWN, AbilityId.BURROWDOWN_BANELING: AbilityId.BURROWDOWN, AbilityId.BURROWDOWN_DRONE: AbilityId.BURROWDOWN, AbilityId.BURROWDOWN_HYDRALISK: AbilityId.BURROWDOWN, AbilityId.BURROWDOWN_INFESTOR: AbilityId.BURROWDOWN, AbilityId.BURROWDOWN_INFESTORTERRAN: AbilityId.BURROWDOWN, AbilityId.BURROWDOWN_LURKER: AbilityId.BURROWDOWN, AbilityId.BURROWDOWN_QUEEN: AbilityId.BURROWDOWN, AbilityId.BURROWDOWN_RAVAGER: AbilityId.BURROWDOWN, AbilityId.BURROWDOWN_ROACH: AbilityId.BURROWDOWN, AbilityId.BURROWDOWN_SWARMHOST: AbilityId.BURROWDOWN, AbilityId.BURROWDOWN_ULTRALISK: AbilityId.BURROWDOWN, AbilityId.BURROWDOWN_WIDOWMINE: AbilityId.BURROWDOWN, AbilityId.BURROWDOWN_ZERGLING: AbilityId.BURROWDOWN, AbilityId.BURROWDRONEDOWN_CANCEL: AbilityId.CANCEL, AbilityId.BURROWHYDRALISKDOWN_CANCEL: AbilityId.CANCEL, AbilityId.BURROWINFESTORDOWN_CANCEL: AbilityId.CANCEL, AbilityId.BURROWLURKERMPDOWN_CANCEL: AbilityId.CANCEL, AbilityId.BURROWQUEENDOWN_CANCEL: AbilityId.CANCEL, AbilityId.BURROWRAVAGERDOWN_CANCEL: AbilityId.CANCEL, AbilityId.BURROWROACHDOWN_CANCEL: AbilityId.CANCEL, AbilityId.BURROWUP_BANELING: AbilityId.BURROWUP, AbilityId.BURROWUP_DRONE: AbilityId.BURROWUP, AbilityId.BURROWUP_HYDRALISK: AbilityId.BURROWUP, AbilityId.BURROWUP_INFESTOR: AbilityId.BURROWUP, AbilityId.BURROWUP_INFESTORTERRAN: AbilityId.BURROWUP, AbilityId.BURROWUP_LURKER: AbilityId.BURROWUP, AbilityId.BURROWUP_QUEEN: AbilityId.BURROWUP, AbilityId.BURROWUP_RAVAGER: AbilityId.BURROWUP, AbilityId.BURROWUP_ROACH: AbilityId.BURROWUP, AbilityId.BURROWUP_SWARMHOST: AbilityId.BURROWUP, AbilityId.BURROWUP_ULTRALISK: AbilityId.BURROWUP, AbilityId.BURROWUP_WIDOWMINE: AbilityId.BURROWUP, AbilityId.BURROWUP_ZERGLING: AbilityId.BURROWUP, AbilityId.BURROWZERGLINGDOWN_CANCEL: AbilityId.CANCEL, AbilityId.CANCELSLOT_ADDON: AbilityId.CANCEL_SLOT, AbilityId.CANCELSLOT_HANGARQUEUE5: AbilityId.CANCEL_SLOT, AbilityId.CANCELSLOT_QUEUE1: AbilityId.CANCEL_SLOT, AbilityId.CANCELSLOT_QUEUE5: AbilityId.CANCEL_SLOT, AbilityId.CANCELSLOT_QUEUECANCELTOSELECTION: AbilityId.CANCEL_SLOT, AbilityId.CANCELSLOT_QUEUEPASSIVE: AbilityId.CANCEL_SLOT, AbilityId.CANCELSLOT_QUEUEPASSIVECANCELTOSELECTION: AbilityId.CANCEL_SLOT, AbilityId.CANCEL_ADEPTPHASESHIFT: AbilityId.CANCEL, AbilityId.CANCEL_ADEPTSHADEPHASESHIFT: AbilityId.CANCEL, AbilityId.CANCEL_BARRACKSADDON: AbilityId.CANCEL, AbilityId.CANCEL_BUILDINPROGRESS: AbilityId.CANCEL, AbilityId.CANCEL_CREEPTUMOR: AbilityId.CANCEL, AbilityId.CANCEL_FACTORYADDON: AbilityId.CANCEL, AbilityId.CANCEL_GRAVITONBEAM: AbilityId.CANCEL, AbilityId.CANCEL_HANGARQUEUE5: AbilityId.CANCEL_LAST, AbilityId.CANCEL_LOCKON: AbilityId.CANCEL, AbilityId.CANCEL_MORPHBROODLORD: AbilityId.CANCEL, AbilityId.CANCEL_MORPHGREATERSPIRE: AbilityId.CANCEL, AbilityId.CANCEL_MORPHHIVE: AbilityId.CANCEL, AbilityId.CANCEL_MORPHLAIR: AbilityId.CANCEL, AbilityId.CANCEL_MORPHLURKER: AbilityId.CANCEL, AbilityId.CANCEL_MORPHMOTHERSHIP: AbilityId.CANCEL, AbilityId.CANCEL_MORPHORBITAL: AbilityId.CANCEL, AbilityId.CANCEL_MORPHOVERLORDTRANSPORT: AbilityId.CANCEL, AbilityId.CANCEL_MORPHOVERSEER: AbilityId.CANCEL, AbilityId.CANCEL_MORPHPLANETARYFORTRESS: AbilityId.CANCEL, AbilityId.CANCEL_MORPHRAVAGER: AbilityId.CANCEL, AbilityId.CANCEL_MORPHTHOREXPLOSIVEMODE: AbilityId.CANCEL, AbilityId.CANCEL_NEURALPARASITE: AbilityId.CANCEL, AbilityId.CANCEL_NUKE: AbilityId.CANCEL, AbilityId.CANCEL_QUEUE1: AbilityId.CANCEL_LAST, AbilityId.CANCEL_QUEUE5: AbilityId.CANCEL_LAST, AbilityId.CANCEL_QUEUEADDON: AbilityId.CANCEL_LAST, AbilityId.CANCEL_QUEUECANCELTOSELECTION: AbilityId.CANCEL_LAST, AbilityId.CANCEL_QUEUEPASIVE: AbilityId.CANCEL_LAST, AbilityId.CANCEL_QUEUEPASSIVECANCELTOSELECTION: AbilityId.CANCEL_LAST, AbilityId.CANCEL_SPINECRAWLERROOT: AbilityId.CANCEL, AbilityId.CANCEL_SPORECRAWLERROOT: AbilityId.CANCEL, AbilityId.CANCEL_STARPORTADDON: AbilityId.CANCEL, AbilityId.CANCEL_STASISTRAP: AbilityId.CANCEL, AbilityId.CANCEL_VOIDRAYPRISMATICALIGNMENT: AbilityId.CANCEL, AbilityId.CHANNELSNIPE_CANCEL: AbilityId.CANCEL, AbilityId.COMMANDCENTERTRANSPORT_COMMANDCENTERTRANSPORT: AbilityId.LOAD, AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL1: AbilityId.RESEARCH_PROTOSSAIRARMOR, AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL2: AbilityId.RESEARCH_PROTOSSAIRARMOR, AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL3: AbilityId.RESEARCH_PROTOSSAIRARMOR, AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL1: AbilityId.RESEARCH_PROTOSSAIRWEAPONS, AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL2: AbilityId.RESEARCH_PROTOSSAIRWEAPONS, AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL3: AbilityId.RESEARCH_PROTOSSAIRWEAPONS, AbilityId.DEFILERMPBURROW_BURROWDOWN: AbilityId.BURROWDOWN, AbilityId.DEFILERMPBURROW_CANCEL: AbilityId.CANCEL, AbilityId.DEFILERMPUNBURROW_BURROWUP: AbilityId.BURROWUP, AbilityId.EFFECT_BLINK_STALKER: AbilityId.EFFECT_BLINK, AbilityId.EFFECT_MASSRECALL_MOTHERSHIPCORE: AbilityId.EFFECT_MASSRECALL, AbilityId.EFFECT_MASSRECALL_NEXUS: AbilityId.EFFECT_MASSRECALL, AbilityId.EFFECT_MASSRECALL_STRATEGICRECALL: AbilityId.EFFECT_MASSRECALL, AbilityId.EFFECT_REPAIR_MULE: AbilityId.EFFECT_REPAIR, AbilityId.EFFECT_REPAIR_REPAIRDRONE: AbilityId.EFFECT_REPAIR, AbilityId.EFFECT_REPAIR_SCV: AbilityId.EFFECT_REPAIR, AbilityId.EFFECT_SHADOWSTRIDE: AbilityId.EFFECT_BLINK, AbilityId.EFFECT_SPRAY_PROTOSS: AbilityId.EFFECT_SPRAY, AbilityId.EFFECT_SPRAY_TERRAN: AbilityId.EFFECT_SPRAY, AbilityId.EFFECT_SPRAY_ZERG: AbilityId.EFFECT_SPRAY, AbilityId.EFFECT_STIM_MARAUDER: AbilityId.EFFECT_STIM, AbilityId.EFFECT_STIM_MARAUDER_REDIRECT: AbilityId.EFFECT_STIM, AbilityId.EFFECT_STIM_MARINE: AbilityId.EFFECT_STIM, AbilityId.EFFECT_STIM_MARINE_REDIRECT: AbilityId.EFFECT_STIM, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL1: AbilityId.RESEARCH_TERRANINFANTRYARMOR, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL2: AbilityId.RESEARCH_TERRANINFANTRYARMOR, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL3: AbilityId.RESEARCH_TERRANINFANTRYARMOR, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL1: AbilityId.RESEARCH_TERRANINFANTRYWEAPONS, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL2: AbilityId.RESEARCH_TERRANINFANTRYWEAPONS, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL3: AbilityId.RESEARCH_TERRANINFANTRYWEAPONS, AbilityId.FORCEFIELD_CANCEL: AbilityId.CANCEL, AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL1: AbilityId.RESEARCH_PROTOSSGROUNDARMOR, AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL2: AbilityId.RESEARCH_PROTOSSGROUNDARMOR, AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL3: AbilityId.RESEARCH_PROTOSSGROUNDARMOR, AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL1: AbilityId.RESEARCH_PROTOSSGROUNDWEAPONS, AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL2: AbilityId.RESEARCH_PROTOSSGROUNDWEAPONS, AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL3: AbilityId.RESEARCH_PROTOSSGROUNDWEAPONS, AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL1: AbilityId.RESEARCH_PROTOSSSHIELDS, AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL2: AbilityId.RESEARCH_PROTOSSSHIELDS, AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL3: AbilityId.RESEARCH_PROTOSSSHIELDS, AbilityId.HALT_BUILDING: AbilityId.HALT, AbilityId.HALT_TERRANBUILD: AbilityId.HALT, AbilityId.HARVEST_GATHER_DRONE: AbilityId.HARVEST_GATHER, AbilityId.HARVEST_GATHER_MULE: AbilityId.HARVEST_GATHER, AbilityId.HARVEST_GATHER_PROBE: AbilityId.HARVEST_GATHER, AbilityId.HARVEST_GATHER_SCV: AbilityId.HARVEST_GATHER, AbilityId.HARVEST_RETURN_DRONE: AbilityId.HARVEST_RETURN, AbilityId.HARVEST_RETURN_MULE: AbilityId.HARVEST_RETURN, AbilityId.HARVEST_RETURN_PROBE: AbilityId.HARVEST_RETURN, AbilityId.HARVEST_RETURN_SCV: AbilityId.HARVEST_RETURN, AbilityId.HOLDPOSITION_BATTLECRUISER: AbilityId.HOLDPOSITION, AbilityId.HOLDPOSITION_HOLD: AbilityId.HOLDPOSITION, AbilityId.LAND_BARRACKS: AbilityId.LAND, AbilityId.LAND_COMMANDCENTER: AbilityId.LAND, AbilityId.LAND_FACTORY: AbilityId.LAND, AbilityId.LAND_ORBITALCOMMAND: AbilityId.LAND, AbilityId.LAND_STARPORT: AbilityId.LAND, AbilityId.LIFT_BARRACKS: AbilityId.LIFT, AbilityId.LIFT_COMMANDCENTER: AbilityId.LIFT, AbilityId.LIFT_FACTORY: AbilityId.LIFT, AbilityId.LIFT_ORBITALCOMMAND: AbilityId.LIFT, AbilityId.LIFT_STARPORT: AbilityId.LIFT, AbilityId.LOADALL_COMMANDCENTER: AbilityId.LOADALL, AbilityId.LOAD_BUNKER: AbilityId.LOAD, AbilityId.LOAD_MEDIVAC: AbilityId.LOAD, AbilityId.LOAD_NYDUSNETWORK: AbilityId.LOAD, AbilityId.LOAD_NYDUSWORM: AbilityId.LOAD, AbilityId.LOAD_OVERLORD: AbilityId.LOAD, AbilityId.LOAD_WARPPRISM: AbilityId.LOAD, AbilityId.MERGEABLE_CANCEL: AbilityId.CANCEL, AbilityId.MORPHBACKTOGATEWAY_CANCEL: AbilityId.CANCEL, AbilityId.MORPHTOBANELING_CANCEL: AbilityId.CANCEL, AbilityId.MORPHTOCOLLAPSIBLEPURIFIERTOWERDEBRIS_CANCEL: AbilityId.CANCEL, AbilityId.MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPLEFTGREEN_CANCEL: AbilityId.CANCEL, AbilityId.MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPLEFT_CANCEL: AbilityId.CANCEL, AbilityId.MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPRIGHTGREEN_CANCEL: AbilityId.CANCEL, AbilityId.MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPRIGHT_CANCEL: AbilityId.CANCEL, AbilityId.MORPHTOCOLLAPSIBLEROCKTOWERDEBRIS_CANCEL: AbilityId.CANCEL, AbilityId.MORPHTOCOLLAPSIBLETERRANTOWERDEBRISRAMPLEFT_CANCEL: AbilityId.CANCEL, AbilityId.MORPHTOCOLLAPSIBLETERRANTOWERDEBRISRAMPRIGHT_CANCEL: AbilityId.CANCEL, AbilityId.MORPHTOCOLLAPSIBLETERRANTOWERDEBRIS_CANCEL: AbilityId.CANCEL, AbilityId.MORPHTODEVOURERMP_CANCEL: AbilityId.CANCEL, AbilityId.MORPHTOGUARDIANMP_CANCEL: AbilityId.CANCEL, AbilityId.MORPHTOSWARMHOSTBURROWEDMP_CANCEL: AbilityId.CANCEL, AbilityId.MOVE_BATTLECRUISER: AbilityId.MOVE, AbilityId.MOVE_MOVE: AbilityId.MOVE, AbilityId.PATROL_BATTLECRUISER: AbilityId.PATROL, AbilityId.PATROL_PATROL: AbilityId.PATROL, AbilityId.PHASINGMODE_CANCEL: AbilityId.CANCEL, AbilityId.PROTOSSBUILD_CANCEL: AbilityId.HALT, AbilityId.QUEENBUILD_CANCEL: AbilityId.HALT, AbilityId.RALLY_BUILDING: AbilityId.RALLY_UNITS, AbilityId.RALLY_COMMANDCENTER: AbilityId.RALLY_WORKERS, AbilityId.RALLY_HATCHERY_UNITS: AbilityId.RALLY_UNITS, AbilityId.RALLY_HATCHERY_WORKERS: AbilityId.RALLY_WORKERS, AbilityId.RALLY_MORPHING_UNIT: AbilityId.RALLY_UNITS, AbilityId.RALLY_NEXUS: AbilityId.RALLY_WORKERS, AbilityId.RESEARCH_ZERGFLYERARMORLEVEL1: AbilityId.RESEARCH_ZERGFLYERARMOR, AbilityId.RESEARCH_ZERGFLYERARMORLEVEL2: AbilityId.RESEARCH_ZERGFLYERARMOR, AbilityId.RESEARCH_ZERGFLYERARMORLEVEL3: AbilityId.RESEARCH_ZERGFLYERARMOR, AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL1: AbilityId.RESEARCH_ZERGFLYERATTACK, AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL2: AbilityId.RESEARCH_ZERGFLYERATTACK, AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL3: AbilityId.RESEARCH_ZERGFLYERATTACK, AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL1: AbilityId.RESEARCH_ZERGGROUNDARMOR, AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL2: AbilityId.RESEARCH_ZERGGROUNDARMOR, AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL3: AbilityId.RESEARCH_ZERGGROUNDARMOR, AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL1: AbilityId.RESEARCH_ZERGMELEEWEAPONS, AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL2: AbilityId.RESEARCH_ZERGMELEEWEAPONS, AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL3: AbilityId.RESEARCH_ZERGMELEEWEAPONS, AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL1: AbilityId.RESEARCH_ZERGMISSILEWEAPONS, AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL2: AbilityId.RESEARCH_ZERGMISSILEWEAPONS, AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL3: AbilityId.RESEARCH_ZERGMISSILEWEAPONS, AbilityId.SCAN_MOVE: AbilityId.ATTACK, AbilityId.SHIELDBATTERYRECHARGEEX5_STOP: AbilityId.CANCEL, AbilityId.SPINECRAWLERROOT_SPINECRAWLERROOT: AbilityId.MORPH_ROOT, AbilityId.SPINECRAWLERUPROOT_CANCEL: AbilityId.CANCEL, AbilityId.SPINECRAWLERUPROOT_SPINECRAWLERUPROOT: AbilityId.MORPH_UPROOT, AbilityId.SPORECRAWLERROOT_SPORECRAWLERROOT: AbilityId.MORPH_ROOT, AbilityId.SPORECRAWLERUPROOT_CANCEL: AbilityId.CANCEL, AbilityId.SPORECRAWLERUPROOT_SPORECRAWLERUPROOT: AbilityId.MORPH_UPROOT, AbilityId.STOP_BATTLECRUISER: AbilityId.STOP, AbilityId.STOP_BUILDING: AbilityId.STOP, AbilityId.STOP_CHEER: AbilityId.STOP, AbilityId.STOP_DANCE: AbilityId.STOP, AbilityId.STOP_HOLDFIRESPECIAL: AbilityId.STOP, AbilityId.STOP_REDIRECT: AbilityId.STOP, AbilityId.STOP_STOP: AbilityId.STOP, AbilityId.TESTZERG_CANCEL: AbilityId.CANCEL, AbilityId.THORAPMODE_CANCEL: AbilityId.CANCEL, AbilityId.TRANSPORTMODE_CANCEL: AbilityId.CANCEL, AbilityId.UNLOADALLAT_MEDIVAC: AbilityId.UNLOADALLAT, AbilityId.UNLOADALLAT_OVERLORD: AbilityId.UNLOADALLAT, AbilityId.UNLOADALLAT_WARPPRISM: AbilityId.UNLOADALLAT, AbilityId.UNLOADALL_BUNKER: AbilityId.UNLOADALL, AbilityId.UNLOADALL_COMMANDCENTER: AbilityId.UNLOADALL, AbilityId.UNLOADALL_NYDASNETWORK: AbilityId.UNLOADALL, AbilityId.UNLOADALL_NYDUSWORM: AbilityId.UNLOADALL, AbilityId.UNLOADALL_WARPPRISM: AbilityId.UNLOADALL, AbilityId.UNLOADUNIT_BUNKER: AbilityId.UNLOADUNIT, AbilityId.UNLOADUNIT_COMMANDCENTER: AbilityId.UNLOADUNIT, AbilityId.UNLOADUNIT_MEDIVAC: AbilityId.UNLOADUNIT, AbilityId.UNLOADUNIT_NYDASNETWORK: AbilityId.UNLOADUNIT, AbilityId.UNLOADUNIT_OVERLORD: AbilityId.UNLOADUNIT, AbilityId.UNLOADUNIT_WARPPRISM: AbilityId.UNLOADUNIT, AbilityId.UPGRADETOWARPGATE_CANCEL: AbilityId.CANCEL, AbilityId.WARPABLE_CANCEL: AbilityId.CANCEL, AbilityId.WIDOWMINEBURROW_CANCEL: AbilityId.CANCEL, AbilityId.ZERGBUILD_CANCEL: AbilityId.HALT, } ================================================ FILE: sc2/dicts/unit_abilities.py ================================================ # THIS FILE WAS AUTOMATICALLY GENERATED BY "generate_dicts_from_data_json.py" DO NOT CHANGE MANUALLY! # ANY CHANGE WILL BE OVERWRITTEN from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId # from sc2.ids.buff_id import BuffId # from sc2.ids.effect_id import EffectId UNIT_ABILITIES: dict[UnitTypeId, set[AbilityId]] = { UnitTypeId.ADEPT: { AbilityId.ADEPTPHASESHIFT_ADEPTPHASESHIFT, AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.ADEPTPHASESHIFT: { AbilityId.ATTACK_ATTACK, AbilityId.CANCEL_ADEPTSHADEPHASESHIFT, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.ARBITERMP: { AbilityId.ARBITERMPRECALL_ARBITERMPRECALL, AbilityId.ARBITERMPSTASISFIELD_ARBITERMPSTASISFIELD, AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.ARCHON: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.ARMORY: { AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL1, AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL2, AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL3, AbilityId.ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL1, AbilityId.ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL2, AbilityId.ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL3, AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL1, AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL2, AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL3, }, UnitTypeId.AUTOTURRET: {AbilityId.ATTACK_ATTACK, AbilityId.SMART, AbilityId.STOP_STOP}, UnitTypeId.BANELING: { AbilityId.ATTACK_ATTACK, AbilityId.BEHAVIOR_BUILDINGATTACKON, AbilityId.BURROWDOWN_BANELING, AbilityId.EXPLODE_EXPLODE, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.BANELINGBURROWED: {AbilityId.BURROWUP_BANELING, AbilityId.EXPLODE_EXPLODE}, UnitTypeId.BANELINGCOCOON: {AbilityId.RALLY_BUILDING, AbilityId.SMART}, UnitTypeId.BANELINGNEST: {AbilityId.RESEARCH_CENTRIFUGALHOOKS}, UnitTypeId.BANSHEE: { AbilityId.ATTACK_ATTACK, AbilityId.BEHAVIOR_CLOAKON_BANSHEE, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.BARRACKS: { AbilityId.BARRACKSTRAIN_GHOST, AbilityId.BARRACKSTRAIN_MARAUDER, AbilityId.BARRACKSTRAIN_MARINE, AbilityId.BARRACKSTRAIN_REAPER, AbilityId.BUILD_REACTOR_BARRACKS, AbilityId.BUILD_TECHLAB_BARRACKS, AbilityId.LIFT_BARRACKS, AbilityId.RALLY_BUILDING, AbilityId.SMART, }, UnitTypeId.BARRACKSFLYING: { AbilityId.BUILD_REACTOR_BARRACKS, AbilityId.BUILD_TECHLAB_BARRACKS, AbilityId.HOLDPOSITION_HOLD, AbilityId.LAND_BARRACKS, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.BARRACKSTECHLAB: { AbilityId.BARRACKSTECHLABRESEARCH_STIMPACK, AbilityId.RESEARCH_COMBATSHIELD, AbilityId.RESEARCH_CONCUSSIVESHELLS, }, UnitTypeId.BATTLECRUISER: { AbilityId.ATTACK_BATTLECRUISER, AbilityId.EFFECT_TACTICALJUMP, AbilityId.HOLDPOSITION_BATTLECRUISER, AbilityId.MOVE_BATTLECRUISER, AbilityId.PATROL_BATTLECRUISER, AbilityId.SMART, AbilityId.STOP_BATTLECRUISER, AbilityId.YAMATO_YAMATOGUN, }, UnitTypeId.BROODLING: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.BROODLORD: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.BUNKER: { AbilityId.LOAD_BUNKER, AbilityId.RALLY_BUILDING, AbilityId.SALVAGEEFFECT_SALVAGE, AbilityId.SMART, }, UnitTypeId.BYPASSARMORDRONE: {AbilityId.ATTACK_ATTACK, AbilityId.MOVE_MOVE, AbilityId.SMART, AbilityId.STOP_STOP}, UnitTypeId.CARRIER: { AbilityId.ATTACK_ATTACK, AbilityId.BUILD_INTERCEPTORS, AbilityId.CANCEL_HANGARQUEUE5, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.CHANGELING: { AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.CHANGELINGMARINE: { AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.CHANGELINGMARINESHIELD: { AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.CHANGELINGZEALOT: { AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.CHANGELINGZERGLING: { AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.CHANGELINGZERGLINGWINGS: { AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.COLOSSUS: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.COMMANDCENTER: { AbilityId.COMMANDCENTERTRAIN_SCV, AbilityId.LIFT_COMMANDCENTER, AbilityId.LOADALL_COMMANDCENTER, AbilityId.RALLY_COMMANDCENTER, AbilityId.SMART, AbilityId.UPGRADETOORBITAL_ORBITALCOMMAND, AbilityId.UPGRADETOPLANETARYFORTRESS_PLANETARYFORTRESS, }, UnitTypeId.COMMANDCENTERFLYING: { AbilityId.HOLDPOSITION_HOLD, AbilityId.LAND_COMMANDCENTER, AbilityId.LOADALL_COMMANDCENTER, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.CORRUPTOR: { AbilityId.ATTACK_ATTACK, AbilityId.CAUSTICSPRAY_CAUSTICSPRAY, AbilityId.HOLDPOSITION_HOLD, AbilityId.MORPHTOBROODLORD_BROODLORD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.CORSAIRMP: { AbilityId.ATTACK_ATTACK, AbilityId.CORSAIRMPDISRUPTIONWEB_CORSAIRMPDISRUPTIONWEB, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.CREEPTUMORBURROWED: {AbilityId.BUILD_CREEPTUMOR, AbilityId.BUILD_CREEPTUMOR_TUMOR, AbilityId.SMART}, UnitTypeId.CYBERNETICSCORE: { AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL1, AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL2, AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL3, AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL1, AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL2, AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL3, AbilityId.RESEARCH_WARPGATE, }, UnitTypeId.CYCLONE: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.LOCKON_LOCKON, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.DARKSHRINE: {AbilityId.RESEARCH_SHADOWSTRIKE}, UnitTypeId.DARKTEMPLAR: { AbilityId.ATTACK_ATTACK, AbilityId.EFFECT_SHADOWSTRIDE, AbilityId.HOLDPOSITION_HOLD, AbilityId.MORPH_ARCHON, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.DEFILERMP: { AbilityId.DEFILERMPBURROW_BURROWDOWN, AbilityId.DEFILERMPCONSUME_DEFILERMPCONSUME, AbilityId.DEFILERMPDARKSWARM_DEFILERMPDARKSWARM, AbilityId.DEFILERMPPLAGUE_DEFILERMPPLAGUE, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.DEFILERMPBURROWED: {AbilityId.DEFILERMPUNBURROW_BURROWUP}, UnitTypeId.DEVOURERMP: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.DISRUPTOR: { AbilityId.EFFECT_PURIFICATIONNOVA, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.DISRUPTORPHASED: { AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.DRONE: { AbilityId.ATTACK_ATTACK, AbilityId.BUILD_LURKERDEN, AbilityId.BURROWDOWN_DRONE, AbilityId.EFFECT_SPRAY_ZERG, AbilityId.HARVEST_GATHER_DRONE, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, AbilityId.ZERGBUILD_BANELINGNEST, AbilityId.ZERGBUILD_EVOLUTIONCHAMBER, AbilityId.ZERGBUILD_EXTRACTOR, AbilityId.ZERGBUILD_HATCHERY, AbilityId.ZERGBUILD_HYDRALISKDEN, AbilityId.ZERGBUILD_INFESTATIONPIT, AbilityId.ZERGBUILD_NYDUSNETWORK, AbilityId.ZERGBUILD_ROACHWARREN, AbilityId.ZERGBUILD_SPAWNINGPOOL, AbilityId.ZERGBUILD_SPINECRAWLER, AbilityId.ZERGBUILD_SPIRE, AbilityId.ZERGBUILD_SPORECRAWLER, AbilityId.ZERGBUILD_ULTRALISKCAVERN, }, UnitTypeId.DRONEBURROWED: {AbilityId.BURROWUP_DRONE}, UnitTypeId.EGG: {AbilityId.RALLY_BUILDING, AbilityId.SMART}, UnitTypeId.ELSECARO_COLONIST_HUT: {AbilityId.RALLY_BUILDING, AbilityId.SMART}, UnitTypeId.ENGINEERINGBAY: { AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL1, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL2, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL3, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL1, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL2, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL3, AbilityId.RESEARCH_HISECAUTOTRACKING, AbilityId.RESEARCH_TERRANSTRUCTUREARMORUPGRADE, }, UnitTypeId.EVOLUTIONCHAMBER: { AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL1, AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL2, AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL3, AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL1, AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL2, AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL3, AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL1, AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL2, AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL3, }, UnitTypeId.FACTORY: { AbilityId.BUILD_REACTOR_FACTORY, AbilityId.BUILD_TECHLAB_FACTORY, AbilityId.FACTORYTRAIN_HELLION, AbilityId.FACTORYTRAIN_SIEGETANK, AbilityId.FACTORYTRAIN_THOR, AbilityId.FACTORYTRAIN_WIDOWMINE, AbilityId.LIFT_FACTORY, AbilityId.RALLY_BUILDING, AbilityId.SMART, AbilityId.TRAIN_CYCLONE, AbilityId.TRAIN_HELLBAT, }, UnitTypeId.FACTORYFLYING: { AbilityId.BUILD_REACTOR_FACTORY, AbilityId.BUILD_TECHLAB_FACTORY, AbilityId.HOLDPOSITION_HOLD, AbilityId.LAND_FACTORY, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.FACTORYTECHLAB: { AbilityId.RESEARCH_CYCLONELOCKONDAMAGE, AbilityId.RESEARCH_DRILLINGCLAWS, AbilityId.RESEARCH_INFERNALPREIGNITER, AbilityId.RESEARCH_SMARTSERVOS, }, UnitTypeId.FLEETBEACON: { AbilityId.FLEETBEACONRESEARCH_RESEARCHVOIDRAYSPEEDUPGRADE, AbilityId.FLEETBEACONRESEARCH_TEMPESTRESEARCHGROUNDATTACKUPGRADE, AbilityId.RESEARCH_PHOENIXANIONPULSECRYSTALS, }, UnitTypeId.FORGE: { AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL1, AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL2, AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL3, AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL1, AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL2, AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL3, AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL1, AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL2, AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL3, }, UnitTypeId.FUSIONCORE: { AbilityId.FUSIONCORERESEARCH_RESEARCHBALLISTICRANGE, AbilityId.FUSIONCORERESEARCH_RESEARCHMEDIVACENERGYUPGRADE, AbilityId.RESEARCH_BATTLECRUISERWEAPONREFIT, }, UnitTypeId.GATEWAY: { AbilityId.GATEWAYTRAIN_DARKTEMPLAR, AbilityId.GATEWAYTRAIN_HIGHTEMPLAR, AbilityId.GATEWAYTRAIN_SENTRY, AbilityId.GATEWAYTRAIN_STALKER, AbilityId.GATEWAYTRAIN_ZEALOT, AbilityId.MORPH_WARPGATE, AbilityId.RALLY_BUILDING, AbilityId.SMART, AbilityId.TRAIN_ADEPT, }, UnitTypeId.GHOST: { AbilityId.ATTACK_ATTACK, AbilityId.BEHAVIOR_CLOAKON_GHOST, AbilityId.BEHAVIOR_HOLDFIREON_GHOST, AbilityId.EFFECT_GHOSTSNIPE, AbilityId.EMP_EMP, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.GHOSTACADEMY: {AbilityId.BUILD_NUKE, AbilityId.RESEARCH_PERSONALCLOAKING}, UnitTypeId.GHOSTNOVA: { AbilityId.ATTACK_ATTACK, AbilityId.BEHAVIOR_CLOAKON_GHOST, AbilityId.BEHAVIOR_HOLDFIREON_GHOST, AbilityId.EFFECT_GHOSTSNIPE, AbilityId.EMP_EMP, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.GREATERSPIRE: { AbilityId.RESEARCH_ZERGFLYERARMORLEVEL1, AbilityId.RESEARCH_ZERGFLYERARMORLEVEL2, AbilityId.RESEARCH_ZERGFLYERARMORLEVEL3, AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL1, AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL2, AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL3, }, UnitTypeId.GUARDIANMP: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.HATCHERY: { AbilityId.RALLY_HATCHERY_UNITS, AbilityId.RALLY_HATCHERY_WORKERS, AbilityId.RESEARCH_BURROW, AbilityId.RESEARCH_PNEUMATIZEDCARAPACE, AbilityId.SMART, AbilityId.TRAINQUEEN_QUEEN, AbilityId.UPGRADETOLAIR_LAIR, }, UnitTypeId.HELLION: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MORPH_HELLBAT, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.HELLIONTANK: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MORPH_HELLION, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.HERC: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.HERCPLACEMENT: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.HIGHTEMPLAR: { AbilityId.ATTACK_ATTACK, AbilityId.FEEDBACK_FEEDBACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MORPH_ARCHON, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.PSISTORM_PSISTORM, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.HIVE: { AbilityId.RALLY_HATCHERY_UNITS, AbilityId.RALLY_HATCHERY_WORKERS, AbilityId.RESEARCH_BURROW, AbilityId.RESEARCH_PNEUMATIZEDCARAPACE, AbilityId.SMART, AbilityId.TRAINQUEEN_QUEEN, }, UnitTypeId.HYDRALISK: { AbilityId.ATTACK_ATTACK, AbilityId.BURROWDOWN_HYDRALISK, AbilityId.HOLDPOSITION_HOLD, AbilityId.HYDRALISKFRENZY_HYDRALISKFRENZY, AbilityId.MORPH_LURKER, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.HYDRALISKBURROWED: {AbilityId.BURROWUP_HYDRALISK}, UnitTypeId.HYDRALISKDEN: { AbilityId.HYDRALISKDENRESEARCH_RESEARCHFRENZY, AbilityId.RESEARCH_GROOVEDSPINES, AbilityId.RESEARCH_MUSCULARAUGMENTS, }, UnitTypeId.IMMORTAL: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.INFESTATIONPIT: {AbilityId.RESEARCH_NEURALPARASITE}, UnitTypeId.INFESTOR: { AbilityId.AMORPHOUSARMORCLOUD_AMORPHOUSARMORCLOUD, AbilityId.BURROWDOWN_INFESTOR, AbilityId.BURROWDOWN_INFESTORTERRAN, AbilityId.FUNGALGROWTH_FUNGALGROWTH, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.NEURALPARASITE_NEURALPARASITE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.INFESTORBURROWED: { AbilityId.BURROWUP_INFESTOR, AbilityId.BURROWUP_INFESTORTERRAN, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.NEURALPARASITE_NEURALPARASITE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.INFESTORTERRAN: { AbilityId.ATTACK_ATTACK, AbilityId.BURROWDOWN_INFESTORTERRAN, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.INFESTORTERRANBURROWED: {AbilityId.BURROWUP_INFESTORTERRAN}, UnitTypeId.INTERCEPTOR: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.LAIR: { AbilityId.RALLY_HATCHERY_UNITS, AbilityId.RALLY_HATCHERY_WORKERS, AbilityId.RESEARCH_BURROW, AbilityId.RESEARCH_PNEUMATIZEDCARAPACE, AbilityId.SMART, AbilityId.TRAINQUEEN_QUEEN, AbilityId.UPGRADETOHIVE_HIVE, }, UnitTypeId.LARVA: { AbilityId.LARVATRAIN_CORRUPTOR, AbilityId.LARVATRAIN_DRONE, AbilityId.LARVATRAIN_HYDRALISK, AbilityId.LARVATRAIN_INFESTOR, AbilityId.LARVATRAIN_MUTALISK, AbilityId.LARVATRAIN_OVERLORD, AbilityId.LARVATRAIN_ROACH, AbilityId.LARVATRAIN_ULTRALISK, AbilityId.LARVATRAIN_VIPER, AbilityId.LARVATRAIN_ZERGLING, AbilityId.TRAIN_SWARMHOST, }, UnitTypeId.LIBERATOR: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MORPH_LIBERATORAGMODE, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.LIBERATORAG: { AbilityId.ATTACK_ATTACK, AbilityId.MORPH_LIBERATORAAMODE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.LOCUSTMP: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.LOCUSTMPFLYING: { AbilityId.ATTACK_ATTACK, AbilityId.EFFECT_LOCUSTSWOOP, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.LURKERDENMP: {AbilityId.LURKERDENRESEARCH_RESEARCHLURKERRANGE, AbilityId.RESEARCH_ADAPTIVETALONS}, UnitTypeId.LURKERMP: { AbilityId.ATTACK_ATTACK, AbilityId.BURROWDOWN_LURKER, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.LURKERMPBURROWED: { AbilityId.ATTACK_ATTACK, AbilityId.BEHAVIOR_HOLDFIREON_LURKER, AbilityId.BURROWUP_LURKER, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.LURKERMPEGG: {AbilityId.RALLY_BUILDING, AbilityId.SMART}, UnitTypeId.MARAUDER: { AbilityId.ATTACK_ATTACK, AbilityId.EFFECT_STIM_MARAUDER, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.MARINE: { AbilityId.ATTACK_ATTACK, AbilityId.EFFECT_STIM_MARINE, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.MEDIVAC: { AbilityId.EFFECT_MEDIVACIGNITEAFTERBURNERS, AbilityId.HOLDPOSITION_HOLD, AbilityId.LOAD_MEDIVAC, AbilityId.MEDIVACHEAL_HEAL, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.MISSILETURRET: {AbilityId.ATTACK_ATTACK, AbilityId.SMART, AbilityId.STOP_STOP}, UnitTypeId.MOTHERSHIP: { AbilityId.ATTACK_ATTACK, AbilityId.EFFECT_MASSRECALL_STRATEGICRECALL, AbilityId.EFFECT_TIMEWARP, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOTHERSHIPCLOAK_ORACLECLOAKFIELD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.MOTHERSHIPCORE: { AbilityId.ATTACK_ATTACK, AbilityId.EFFECT_MASSRECALL_MOTHERSHIPCORE, AbilityId.EFFECT_PHOTONOVERCHARGE, AbilityId.EFFECT_TIMEWARP, AbilityId.HOLDPOSITION_HOLD, AbilityId.MORPH_MOTHERSHIP, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.MULE: { AbilityId.EFFECT_REPAIR_MULE, AbilityId.HARVEST_GATHER_MULE, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.MUTALISK: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.NEXUS: { AbilityId.EFFECT_CHRONOBOOSTENERGYCOST, AbilityId.EFFECT_MASSRECALL_NEXUS, AbilityId.ENERGYRECHARGE_ENERGYRECHARGE, AbilityId.NEXUSTRAINMOTHERSHIP_MOTHERSHIP, AbilityId.NEXUSTRAIN_PROBE, AbilityId.RALLY_NEXUS, AbilityId.SMART, }, UnitTypeId.NYDUSCANAL: {AbilityId.LOAD_NYDUSWORM, AbilityId.RALLY_BUILDING, AbilityId.SMART, AbilityId.STOP_STOP}, UnitTypeId.NYDUSCANALATTACKER: {AbilityId.ATTACK_ATTACK, AbilityId.SMART, AbilityId.STOP_STOP}, UnitTypeId.NYDUSCANALCREEPER: { AbilityId.ATTACK_ATTACK, AbilityId.DIGESTERCREEPSPRAY_DIGESTERCREEPSPRAY, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.NYDUSNETWORK: { AbilityId.BUILD_NYDUSWORM, AbilityId.LOAD_NYDUSNETWORK, AbilityId.RALLY_BUILDING, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.OBSERVER: { AbilityId.HOLDPOSITION_HOLD, AbilityId.MORPH_SURVEILLANCEMODE, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.OBSERVERSIEGEMODE: {AbilityId.MORPH_OBSERVERMODE, AbilityId.STOP_STOP}, UnitTypeId.ORACLE: { AbilityId.ATTACK_ATTACK, AbilityId.BEHAVIOR_PULSARBEAMON, AbilityId.BUILD_STASISTRAP, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.ORACLEREVELATION_ORACLEREVELATION, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.ORBITALCOMMAND: { AbilityId.CALLDOWNMULE_CALLDOWNMULE, AbilityId.COMMANDCENTERTRAIN_SCV, AbilityId.LIFT_ORBITALCOMMAND, AbilityId.RALLY_COMMANDCENTER, AbilityId.SCANNERSWEEP_SCAN, AbilityId.SMART, AbilityId.SUPPLYDROP_SUPPLYDROP, }, UnitTypeId.ORBITALCOMMANDFLYING: { AbilityId.HOLDPOSITION_HOLD, AbilityId.LAND_ORBITALCOMMAND, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.OVERLORD: { AbilityId.BEHAVIOR_GENERATECREEPON, AbilityId.HOLDPOSITION_HOLD, AbilityId.MORPH_OVERLORDTRANSPORT, AbilityId.MORPH_OVERSEER, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.OVERLORDTRANSPORT: { AbilityId.BEHAVIOR_GENERATECREEPON, AbilityId.HOLDPOSITION_HOLD, AbilityId.LOAD_OVERLORD, AbilityId.MORPH_OVERSEER, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.OVERSEER: { AbilityId.CONTAMINATE_CONTAMINATE, AbilityId.HOLDPOSITION_HOLD, AbilityId.MORPH_OVERSIGHTMODE, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.SPAWNCHANGELING_SPAWNCHANGELING, AbilityId.STOP_STOP, }, UnitTypeId.OVERSEERSIEGEMODE: { AbilityId.CONTAMINATE_CONTAMINATE, AbilityId.MORPH_OVERSEERMODE, AbilityId.SMART, AbilityId.SPAWNCHANGELING_SPAWNCHANGELING, AbilityId.STOP_STOP, }, UnitTypeId.PHOENIX: { AbilityId.ATTACK_ATTACK, AbilityId.GRAVITONBEAM_GRAVITONBEAM, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.PHOTONCANNON: {AbilityId.ATTACK_ATTACK, AbilityId.SMART, AbilityId.STOP_STOP}, UnitTypeId.PLANETARYFORTRESS: { AbilityId.ATTACK_ATTACK, AbilityId.COMMANDCENTERTRAIN_SCV, AbilityId.LOADALL_COMMANDCENTER, AbilityId.RALLY_COMMANDCENTER, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.PROBE: { AbilityId.ATTACK_ATTACK, AbilityId.BUILD_SHIELDBATTERY, AbilityId.EFFECT_SPRAY_PROTOSS, AbilityId.HARVEST_GATHER_PROBE, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.PROTOSSBUILD_ASSIMILATOR, AbilityId.PROTOSSBUILD_CYBERNETICSCORE, AbilityId.PROTOSSBUILD_DARKSHRINE, AbilityId.PROTOSSBUILD_FLEETBEACON, AbilityId.PROTOSSBUILD_FORGE, AbilityId.PROTOSSBUILD_GATEWAY, AbilityId.PROTOSSBUILD_NEXUS, AbilityId.PROTOSSBUILD_PHOTONCANNON, AbilityId.PROTOSSBUILD_PYLON, AbilityId.PROTOSSBUILD_ROBOTICSBAY, AbilityId.PROTOSSBUILD_ROBOTICSFACILITY, AbilityId.PROTOSSBUILD_STARGATE, AbilityId.PROTOSSBUILD_TEMPLARARCHIVE, AbilityId.PROTOSSBUILD_TWILIGHTCOUNCIL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.QUEEN: { AbilityId.ATTACK_ATTACK, AbilityId.BUILD_CREEPTUMOR, AbilityId.BUILD_CREEPTUMOR_QUEEN, AbilityId.BURROWDOWN_QUEEN, AbilityId.EFFECT_INJECTLARVA, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, AbilityId.TRANSFUSION_TRANSFUSION, }, UnitTypeId.QUEENBURROWED: {AbilityId.BURROWUP_QUEEN}, UnitTypeId.QUEENMP: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.QUEENMPENSNARE_QUEENMPENSNARE, AbilityId.QUEENMPINFESTCOMMANDCENTER_QUEENMPINFESTCOMMANDCENTER, AbilityId.QUEENMPSPAWNBROODLINGS_QUEENMPSPAWNBROODLINGS, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.RAVAGER: { AbilityId.ATTACK_ATTACK, AbilityId.BURROWDOWN_RAVAGER, AbilityId.EFFECT_CORROSIVEBILE, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.RAVAGERBURROWED: {AbilityId.BURROWUP_RAVAGER}, UnitTypeId.RAVAGERCOCOON: {AbilityId.RALLY_BUILDING, AbilityId.SMART}, UnitTypeId.RAVEN: { AbilityId.BUILDAUTOTURRET_AUTOTURRET, AbilityId.EFFECT_ANTIARMORMISSILE, AbilityId.EFFECT_INTERFERENCEMATRIX, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.RAVENREPAIRDRONE: {AbilityId.EFFECT_REPAIR_REPAIRDRONE, AbilityId.SMART, AbilityId.STOP_STOP}, UnitTypeId.REAPER: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.KD8CHARGE_KD8CHARGE, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.REPLICANT: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.ROACH: { AbilityId.ATTACK_ATTACK, AbilityId.BURROWDOWN_ROACH, AbilityId.HOLDPOSITION_HOLD, AbilityId.MORPHTORAVAGER_RAVAGER, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.ROACHBURROWED: { AbilityId.BURROWUP_ROACH, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.ROACHWARREN: {AbilityId.RESEARCH_GLIALREGENERATION, AbilityId.RESEARCH_TUNNELINGCLAWS}, UnitTypeId.ROBOTICSBAY: { AbilityId.RESEARCH_EXTENDEDTHERMALLANCE, AbilityId.RESEARCH_GRAVITICBOOSTER, AbilityId.RESEARCH_GRAVITICDRIVE, }, UnitTypeId.ROBOTICSFACILITY: { AbilityId.RALLY_BUILDING, AbilityId.ROBOTICSFACILITYTRAIN_COLOSSUS, AbilityId.ROBOTICSFACILITYTRAIN_IMMORTAL, AbilityId.ROBOTICSFACILITYTRAIN_OBSERVER, AbilityId.ROBOTICSFACILITYTRAIN_WARPPRISM, AbilityId.SMART, AbilityId.TRAIN_DISRUPTOR, }, UnitTypeId.SCOURGEMP: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.SCOUTMP: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.SCV: { AbilityId.ATTACK_ATTACK, AbilityId.EFFECT_REPAIR_SCV, AbilityId.EFFECT_SPRAY_TERRAN, AbilityId.HARVEST_GATHER_SCV, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, AbilityId.TERRANBUILD_ARMORY, AbilityId.TERRANBUILD_BARRACKS, AbilityId.TERRANBUILD_BUNKER, AbilityId.TERRANBUILD_COMMANDCENTER, AbilityId.TERRANBUILD_ENGINEERINGBAY, AbilityId.TERRANBUILD_FACTORY, AbilityId.TERRANBUILD_FUSIONCORE, AbilityId.TERRANBUILD_GHOSTACADEMY, AbilityId.TERRANBUILD_MISSILETURRET, AbilityId.TERRANBUILD_REFINERY, AbilityId.TERRANBUILD_SENSORTOWER, AbilityId.TERRANBUILD_STARPORT, AbilityId.TERRANBUILD_SUPPLYDEPOT, }, UnitTypeId.SENSORTOWER: {AbilityId.SALVAGEEFFECT_SALVAGE}, UnitTypeId.SENTRY: { AbilityId.ATTACK_ATTACK, AbilityId.FORCEFIELD_FORCEFIELD, AbilityId.GUARDIANSHIELD_GUARDIANSHIELD, AbilityId.HALLUCINATION_ADEPT, AbilityId.HALLUCINATION_ARCHON, AbilityId.HALLUCINATION_COLOSSUS, AbilityId.HALLUCINATION_DISRUPTOR, AbilityId.HALLUCINATION_HIGHTEMPLAR, AbilityId.HALLUCINATION_IMMORTAL, AbilityId.HALLUCINATION_ORACLE, AbilityId.HALLUCINATION_PHOENIX, AbilityId.HALLUCINATION_PROBE, AbilityId.HALLUCINATION_STALKER, AbilityId.HALLUCINATION_VOIDRAY, AbilityId.HALLUCINATION_WARPPRISM, AbilityId.HALLUCINATION_ZEALOT, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.SHIELDBATTERY: {AbilityId.SHIELDBATTERYRECHARGEEX5_SHIELDBATTERYRECHARGE, AbilityId.SMART}, UnitTypeId.SIEGETANK: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SIEGEMODE_SIEGEMODE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.SIEGETANKSIEGED: { AbilityId.ATTACK_ATTACK, AbilityId.SMART, AbilityId.STOP_STOP, AbilityId.UNSIEGE_UNSIEGE, }, UnitTypeId.SPAWNINGPOOL: {AbilityId.RESEARCH_ZERGLINGADRENALGLANDS, AbilityId.RESEARCH_ZERGLINGMETABOLICBOOST}, UnitTypeId.SPINECRAWLER: { AbilityId.ATTACK_ATTACK, AbilityId.SMART, AbilityId.SPINECRAWLERUPROOT_SPINECRAWLERUPROOT, AbilityId.STOP_STOP, }, UnitTypeId.SPINECRAWLERUPROOTED: { AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.SPINECRAWLERROOT_SPINECRAWLERROOT, AbilityId.STOP_STOP, }, UnitTypeId.SPIRE: { AbilityId.RESEARCH_ZERGFLYERARMORLEVEL1, AbilityId.RESEARCH_ZERGFLYERARMORLEVEL2, AbilityId.RESEARCH_ZERGFLYERARMORLEVEL3, AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL1, AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL2, AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL3, AbilityId.UPGRADETOGREATERSPIRE_GREATERSPIRE, }, UnitTypeId.SPORECRAWLER: { AbilityId.ATTACK_ATTACK, AbilityId.SMART, AbilityId.SPORECRAWLERUPROOT_SPORECRAWLERUPROOT, AbilityId.STOP_STOP, }, UnitTypeId.SPORECRAWLERUPROOTED: { AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.SPORECRAWLERROOT_SPORECRAWLERROOT, AbilityId.STOP_STOP, }, UnitTypeId.STALKER: { AbilityId.ATTACK_ATTACK, AbilityId.EFFECT_BLINK_STALKER, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.STARGATE: { AbilityId.RALLY_BUILDING, AbilityId.SMART, AbilityId.STARGATETRAIN_CARRIER, AbilityId.STARGATETRAIN_ORACLE, AbilityId.STARGATETRAIN_PHOENIX, AbilityId.STARGATETRAIN_TEMPEST, AbilityId.STARGATETRAIN_VOIDRAY, }, UnitTypeId.STARPORT: { AbilityId.BUILD_REACTOR_STARPORT, AbilityId.BUILD_TECHLAB_STARPORT, AbilityId.LIFT_STARPORT, AbilityId.RALLY_BUILDING, AbilityId.SMART, AbilityId.STARPORTTRAIN_BANSHEE, AbilityId.STARPORTTRAIN_BATTLECRUISER, AbilityId.STARPORTTRAIN_LIBERATOR, AbilityId.STARPORTTRAIN_MEDIVAC, AbilityId.STARPORTTRAIN_RAVEN, AbilityId.STARPORTTRAIN_VIKINGFIGHTER, }, UnitTypeId.STARPORTFLYING: { AbilityId.BUILD_REACTOR_STARPORT, AbilityId.BUILD_TECHLAB_STARPORT, AbilityId.HOLDPOSITION_HOLD, AbilityId.LAND_STARPORT, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.STARPORTTECHLAB: { AbilityId.RESEARCH_BANSHEECLOAKINGFIELD, AbilityId.RESEARCH_BANSHEEHYPERFLIGHTROTORS, AbilityId.STARPORTTECHLABRESEARCH_RESEARCHRAVENINTERFERENCEMATRIX, }, UnitTypeId.SUPPLYDEPOT: {AbilityId.MORPH_SUPPLYDEPOT_LOWER}, UnitTypeId.SUPPLYDEPOTLOWERED: {AbilityId.MORPH_SUPPLYDEPOT_RAISE}, UnitTypeId.SWARMHOSTBURROWEDMP: {AbilityId.EFFECT_SPAWNLOCUSTS, AbilityId.SMART}, UnitTypeId.SWARMHOSTMP: { AbilityId.BURROWDOWN_SWARMHOST, AbilityId.EFFECT_SPAWNLOCUSTS, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.TECHLAB: { AbilityId.BARRACKSTECHLABRESEARCH_STIMPACK, AbilityId.RESEARCH_BANSHEECLOAKINGFIELD, AbilityId.RESEARCH_COMBATSHIELD, AbilityId.RESEARCH_CONCUSSIVESHELLS, AbilityId.RESEARCH_DRILLINGCLAWS, AbilityId.RESEARCH_INFERNALPREIGNITER, AbilityId.RESEARCH_RAVENCORVIDREACTOR, }, UnitTypeId.TEMPEST: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.TEMPLARARCHIVE: {AbilityId.RESEARCH_PSISTORM}, UnitTypeId.THOR: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MORPH_THORHIGHIMPACTMODE, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.THORAP: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MORPH_THOREXPLOSIVEMODE, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.TWILIGHTCOUNCIL: { AbilityId.RESEARCH_ADEPTRESONATINGGLAIVES, AbilityId.RESEARCH_BLINK, AbilityId.RESEARCH_CHARGE, }, UnitTypeId.ULTRALISK: { AbilityId.ATTACK_ATTACK, AbilityId.BURROWDOWN_ULTRALISK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.ULTRALISKBURROWED: {AbilityId.BURROWUP_ULTRALISK}, UnitTypeId.ULTRALISKCAVERN: {AbilityId.RESEARCH_ANABOLICSYNTHESIS, AbilityId.RESEARCH_CHITINOUSPLATING}, UnitTypeId.VIKINGASSAULT: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MORPH_VIKINGFIGHTERMODE, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.VIKINGFIGHTER: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MORPH_VIKINGASSAULTMODE, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.VIPER: { AbilityId.BLINDINGCLOUD_BLINDINGCLOUD, AbilityId.EFFECT_ABDUCT, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PARASITICBOMB_PARASITICBOMB, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, AbilityId.VIPERCONSUMESTRUCTURE_VIPERCONSUME, }, UnitTypeId.VOIDMPIMMORTALREVIVECORPSE: { AbilityId.RALLY_BUILDING, AbilityId.SMART, AbilityId.VOIDMPIMMORTALREVIVEREBUILD_IMMORTAL, }, UnitTypeId.VOIDRAY: { AbilityId.ATTACK_ATTACK, AbilityId.EFFECT_VOIDRAYPRISMATICALIGNMENT, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.WARHOUND: { AbilityId.ATTACK_ATTACK, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, AbilityId.TORNADOMISSILE_TORNADOMISSILE, }, UnitTypeId.WARPGATE: { AbilityId.MORPH_GATEWAY, AbilityId.SMART, AbilityId.TRAINWARP_ADEPT, AbilityId.WARPGATETRAIN_DARKTEMPLAR, AbilityId.WARPGATETRAIN_HIGHTEMPLAR, AbilityId.WARPGATETRAIN_SENTRY, AbilityId.WARPGATETRAIN_STALKER, AbilityId.WARPGATETRAIN_ZEALOT, }, UnitTypeId.WARPPRISM: { AbilityId.HOLDPOSITION_HOLD, AbilityId.LOAD_WARPPRISM, AbilityId.MORPH_WARPPRISMPHASINGMODE, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.WARPPRISMPHASING: { AbilityId.LOAD_WARPPRISM, AbilityId.MORPH_WARPPRISMTRANSPORTMODE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.WIDOWMINE: { AbilityId.BURROWDOWN_WIDOWMINE, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SCAN_MOVE, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.WIDOWMINEBURROWED: { AbilityId.BURROWUP_WIDOWMINE, AbilityId.SMART, AbilityId.WIDOWMINEATTACK_WIDOWMINEATTACK, }, UnitTypeId.ZEALOT: { AbilityId.ATTACK_ATTACK, AbilityId.EFFECT_CHARGE, AbilityId.HOLDPOSITION_HOLD, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.ZERGLING: { AbilityId.ATTACK_ATTACK, AbilityId.BURROWDOWN_ZERGLING, AbilityId.HOLDPOSITION_HOLD, AbilityId.MORPHTOBANELING_BANELING, AbilityId.MOVE_MOVE, AbilityId.PATROL_PATROL, AbilityId.SMART, AbilityId.STOP_STOP, }, UnitTypeId.ZERGLINGBURROWED: {AbilityId.BURROWUP_ZERGLING}, } ================================================ FILE: sc2/dicts/unit_research_abilities.py ================================================ # THIS FILE WAS AUTOMATICALLY GENERATED BY "generate_dicts_from_data_json.py" DO NOT CHANGE MANUALLY! # ANY CHANGE WILL BE OVERWRITTEN # from sc2.ids.buff_id import BuffId # from sc2.ids.effect_id import EffectId from typing import Union from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId from sc2.ids.upgrade_id import UpgradeId RESEARCH_INFO: dict[UnitTypeId, dict[UpgradeId, dict[str, Union[AbilityId, bool, UnitTypeId, UpgradeId]]]] = { UnitTypeId.ARMORY: { UpgradeId.TERRANSHIPWEAPONSLEVEL1: {"ability": AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL1}, UpgradeId.TERRANSHIPWEAPONSLEVEL2: { "ability": AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL2, "required_upgrade": UpgradeId.TERRANSHIPWEAPONSLEVEL1, }, UpgradeId.TERRANSHIPWEAPONSLEVEL3: { "ability": AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL3, "required_upgrade": UpgradeId.TERRANSHIPWEAPONSLEVEL2, }, UpgradeId.TERRANVEHICLEANDSHIPARMORSLEVEL1: { "ability": AbilityId.ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL1 }, UpgradeId.TERRANVEHICLEANDSHIPARMORSLEVEL2: { "ability": AbilityId.ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL2, "required_upgrade": UpgradeId.TERRANVEHICLEANDSHIPARMORSLEVEL1, }, UpgradeId.TERRANVEHICLEANDSHIPARMORSLEVEL3: { "ability": AbilityId.ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL3, "required_upgrade": UpgradeId.TERRANVEHICLEANDSHIPARMORSLEVEL2, }, UpgradeId.TERRANVEHICLEWEAPONSLEVEL1: {"ability": AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL1}, UpgradeId.TERRANVEHICLEWEAPONSLEVEL2: { "ability": AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL2, "required_upgrade": UpgradeId.TERRANVEHICLEWEAPONSLEVEL1, }, UpgradeId.TERRANVEHICLEWEAPONSLEVEL3: { "ability": AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL3, "required_upgrade": UpgradeId.TERRANVEHICLEWEAPONSLEVEL2, }, }, UnitTypeId.BANELINGNEST: { UpgradeId.CENTRIFICALHOOKS: { "ability": AbilityId.RESEARCH_CENTRIFUGALHOOKS, "required_building": UnitTypeId.LAIR, } }, UnitTypeId.BARRACKSTECHLAB: { UpgradeId.PUNISHERGRENADES: {"ability": AbilityId.RESEARCH_CONCUSSIVESHELLS}, UpgradeId.SHIELDWALL: {"ability": AbilityId.RESEARCH_COMBATSHIELD}, UpgradeId.STIMPACK: {"ability": AbilityId.BARRACKSTECHLABRESEARCH_STIMPACK}, }, UnitTypeId.CYBERNETICSCORE: { UpgradeId.PROTOSSAIRARMORSLEVEL1: { "ability": AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL1, "requires_power": True, }, UpgradeId.PROTOSSAIRARMORSLEVEL2: { "ability": AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL2, "required_building": UnitTypeId.FLEETBEACON, "required_upgrade": UpgradeId.PROTOSSAIRARMORSLEVEL1, "requires_power": True, }, UpgradeId.PROTOSSAIRARMORSLEVEL3: { "ability": AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL3, "required_building": UnitTypeId.FLEETBEACON, "required_upgrade": UpgradeId.PROTOSSAIRARMORSLEVEL2, "requires_power": True, }, UpgradeId.PROTOSSAIRWEAPONSLEVEL1: { "ability": AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL1, "requires_power": True, }, UpgradeId.PROTOSSAIRWEAPONSLEVEL2: { "ability": AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL2, "required_building": UnitTypeId.FLEETBEACON, "required_upgrade": UpgradeId.PROTOSSAIRWEAPONSLEVEL1, "requires_power": True, }, UpgradeId.PROTOSSAIRWEAPONSLEVEL3: { "ability": AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL3, "required_building": UnitTypeId.FLEETBEACON, "required_upgrade": UpgradeId.PROTOSSAIRWEAPONSLEVEL2, "requires_power": True, }, UpgradeId.WARPGATERESEARCH: {"ability": AbilityId.RESEARCH_WARPGATE, "requires_power": True}, }, UnitTypeId.DARKSHRINE: { UpgradeId.DARKTEMPLARBLINKUPGRADE: {"ability": AbilityId.RESEARCH_SHADOWSTRIKE, "requires_power": True} }, UnitTypeId.ENGINEERINGBAY: { UpgradeId.HISECAUTOTRACKING: {"ability": AbilityId.RESEARCH_HISECAUTOTRACKING}, UpgradeId.TERRANBUILDINGARMOR: {"ability": AbilityId.RESEARCH_TERRANSTRUCTUREARMORUPGRADE}, UpgradeId.TERRANINFANTRYARMORSLEVEL1: {"ability": AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL1}, UpgradeId.TERRANINFANTRYARMORSLEVEL2: { "ability": AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL2, "required_building": UnitTypeId.ARMORY, "required_upgrade": UpgradeId.TERRANINFANTRYARMORSLEVEL1, }, UpgradeId.TERRANINFANTRYARMORSLEVEL3: { "ability": AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL3, "required_building": UnitTypeId.ARMORY, "required_upgrade": UpgradeId.TERRANINFANTRYARMORSLEVEL2, }, UpgradeId.TERRANINFANTRYWEAPONSLEVEL1: { "ability": AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL1 }, UpgradeId.TERRANINFANTRYWEAPONSLEVEL2: { "ability": AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL2, "required_building": UnitTypeId.ARMORY, "required_upgrade": UpgradeId.TERRANINFANTRYWEAPONSLEVEL1, }, UpgradeId.TERRANINFANTRYWEAPONSLEVEL3: { "ability": AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL3, "required_building": UnitTypeId.ARMORY, "required_upgrade": UpgradeId.TERRANINFANTRYWEAPONSLEVEL2, }, }, UnitTypeId.EVOLUTIONCHAMBER: { UpgradeId.ZERGGROUNDARMORSLEVEL1: {"ability": AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL1}, UpgradeId.ZERGGROUNDARMORSLEVEL2: { "ability": AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL2, "required_building": UnitTypeId.LAIR, "required_upgrade": UpgradeId.ZERGGROUNDARMORSLEVEL1, }, UpgradeId.ZERGGROUNDARMORSLEVEL3: { "ability": AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL3, "required_building": UnitTypeId.HIVE, "required_upgrade": UpgradeId.ZERGGROUNDARMORSLEVEL2, }, UpgradeId.ZERGMELEEWEAPONSLEVEL1: {"ability": AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL1}, UpgradeId.ZERGMELEEWEAPONSLEVEL2: { "ability": AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL2, "required_building": UnitTypeId.LAIR, "required_upgrade": UpgradeId.ZERGMELEEWEAPONSLEVEL1, }, UpgradeId.ZERGMELEEWEAPONSLEVEL3: { "ability": AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL3, "required_building": UnitTypeId.HIVE, "required_upgrade": UpgradeId.ZERGMELEEWEAPONSLEVEL2, }, UpgradeId.ZERGMISSILEWEAPONSLEVEL1: {"ability": AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL1}, UpgradeId.ZERGMISSILEWEAPONSLEVEL2: { "ability": AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL2, "required_building": UnitTypeId.LAIR, "required_upgrade": UpgradeId.ZERGMISSILEWEAPONSLEVEL1, }, UpgradeId.ZERGMISSILEWEAPONSLEVEL3: { "ability": AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL3, "required_building": UnitTypeId.HIVE, "required_upgrade": UpgradeId.ZERGMISSILEWEAPONSLEVEL2, }, }, UnitTypeId.FACTORYTECHLAB: { UpgradeId.CYCLONELOCKONDAMAGEUPGRADE: {"ability": AbilityId.RESEARCH_CYCLONELOCKONDAMAGE}, UpgradeId.DRILLCLAWS: {"ability": AbilityId.RESEARCH_DRILLINGCLAWS, "required_building": UnitTypeId.ARMORY}, UpgradeId.HIGHCAPACITYBARRELS: {"ability": AbilityId.RESEARCH_INFERNALPREIGNITER}, UpgradeId.SMARTSERVOS: {"ability": AbilityId.RESEARCH_SMARTSERVOS, "required_building": UnitTypeId.ARMORY}, }, UnitTypeId.FLEETBEACON: { UpgradeId.PHOENIXRANGEUPGRADE: { "ability": AbilityId.RESEARCH_PHOENIXANIONPULSECRYSTALS, "requires_power": True, }, UpgradeId.TEMPESTGROUNDATTACKUPGRADE: { "ability": AbilityId.FLEETBEACONRESEARCH_TEMPESTRESEARCHGROUNDATTACKUPGRADE, "requires_power": True, }, UpgradeId.VOIDRAYSPEEDUPGRADE: { "ability": AbilityId.FLEETBEACONRESEARCH_RESEARCHVOIDRAYSPEEDUPGRADE, "requires_power": True, }, }, UnitTypeId.FORGE: { UpgradeId.PROTOSSGROUNDARMORSLEVEL1: { "ability": AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL1, "requires_power": True, }, UpgradeId.PROTOSSGROUNDARMORSLEVEL2: { "ability": AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL2, "required_building": UnitTypeId.TWILIGHTCOUNCIL, "required_upgrade": UpgradeId.PROTOSSGROUNDARMORSLEVEL1, "requires_power": True, }, UpgradeId.PROTOSSGROUNDARMORSLEVEL3: { "ability": AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL3, "required_building": UnitTypeId.TWILIGHTCOUNCIL, "required_upgrade": UpgradeId.PROTOSSGROUNDARMORSLEVEL2, "requires_power": True, }, UpgradeId.PROTOSSGROUNDWEAPONSLEVEL1: { "ability": AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL1, "requires_power": True, }, UpgradeId.PROTOSSGROUNDWEAPONSLEVEL2: { "ability": AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL2, "required_building": UnitTypeId.TWILIGHTCOUNCIL, "required_upgrade": UpgradeId.PROTOSSGROUNDWEAPONSLEVEL1, "requires_power": True, }, UpgradeId.PROTOSSGROUNDWEAPONSLEVEL3: { "ability": AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL3, "required_building": UnitTypeId.TWILIGHTCOUNCIL, "required_upgrade": UpgradeId.PROTOSSGROUNDWEAPONSLEVEL2, "requires_power": True, }, UpgradeId.PROTOSSSHIELDSLEVEL1: { "ability": AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL1, "requires_power": True, }, UpgradeId.PROTOSSSHIELDSLEVEL2: { "ability": AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL2, "required_building": UnitTypeId.TWILIGHTCOUNCIL, "required_upgrade": UpgradeId.PROTOSSSHIELDSLEVEL1, "requires_power": True, }, UpgradeId.PROTOSSSHIELDSLEVEL3: { "ability": AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL3, "required_building": UnitTypeId.TWILIGHTCOUNCIL, "required_upgrade": UpgradeId.PROTOSSSHIELDSLEVEL2, "requires_power": True, }, }, UnitTypeId.FUSIONCORE: { UpgradeId.BATTLECRUISERENABLESPECIALIZATIONS: {"ability": AbilityId.RESEARCH_BATTLECRUISERWEAPONREFIT}, UpgradeId.LIBERATORAGRANGEUPGRADE: {"ability": AbilityId.FUSIONCORERESEARCH_RESEARCHBALLISTICRANGE}, UpgradeId.MEDIVACCADUCEUSREACTOR: {"ability": AbilityId.FUSIONCORERESEARCH_RESEARCHMEDIVACENERGYUPGRADE}, }, UnitTypeId.GHOSTACADEMY: {UpgradeId.PERSONALCLOAKING: {"ability": AbilityId.RESEARCH_PERSONALCLOAKING}}, UnitTypeId.GREATERSPIRE: { UpgradeId.ZERGFLYERARMORSLEVEL1: {"ability": AbilityId.RESEARCH_ZERGFLYERARMORLEVEL1}, UpgradeId.ZERGFLYERARMORSLEVEL2: { "ability": AbilityId.RESEARCH_ZERGFLYERARMORLEVEL2, "required_building": UnitTypeId.LAIR, "required_upgrade": UpgradeId.ZERGFLYERARMORSLEVEL1, }, UpgradeId.ZERGFLYERARMORSLEVEL3: { "ability": AbilityId.RESEARCH_ZERGFLYERARMORLEVEL3, "required_building": UnitTypeId.HIVE, "required_upgrade": UpgradeId.ZERGFLYERARMORSLEVEL2, }, UpgradeId.ZERGFLYERWEAPONSLEVEL1: {"ability": AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL1}, UpgradeId.ZERGFLYERWEAPONSLEVEL2: { "ability": AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL2, "required_building": UnitTypeId.LAIR, "required_upgrade": UpgradeId.ZERGFLYERWEAPONSLEVEL1, }, UpgradeId.ZERGFLYERWEAPONSLEVEL3: { "ability": AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL3, "required_building": UnitTypeId.HIVE, "required_upgrade": UpgradeId.ZERGFLYERWEAPONSLEVEL2, }, }, UnitTypeId.HATCHERY: { UpgradeId.BURROW: {"ability": AbilityId.RESEARCH_BURROW}, UpgradeId.OVERLORDSPEED: {"ability": AbilityId.RESEARCH_PNEUMATIZEDCARAPACE}, }, UnitTypeId.HIVE: { UpgradeId.BURROW: {"ability": AbilityId.RESEARCH_BURROW}, UpgradeId.OVERLORDSPEED: {"ability": AbilityId.RESEARCH_PNEUMATIZEDCARAPACE}, }, UnitTypeId.HYDRALISKDEN: { UpgradeId.EVOLVEGROOVEDSPINES: {"ability": AbilityId.RESEARCH_GROOVEDSPINES}, UpgradeId.EVOLVEMUSCULARAUGMENTS: {"ability": AbilityId.RESEARCH_MUSCULARAUGMENTS}, UpgradeId.FRENZY: { "ability": AbilityId.HYDRALISKDENRESEARCH_RESEARCHFRENZY, "required_building": UnitTypeId.HIVE, }, }, UnitTypeId.INFESTATIONPIT: {UpgradeId.NEURALPARASITE: {"ability": AbilityId.RESEARCH_NEURALPARASITE}}, UnitTypeId.LAIR: { UpgradeId.BURROW: {"ability": AbilityId.RESEARCH_BURROW}, UpgradeId.OVERLORDSPEED: {"ability": AbilityId.RESEARCH_PNEUMATIZEDCARAPACE}, }, UnitTypeId.LURKERDENMP: { UpgradeId.DIGGINGCLAWS: {"ability": AbilityId.RESEARCH_ADAPTIVETALONS, "required_building": UnitTypeId.HIVE}, UpgradeId.LURKERRANGE: { "ability": AbilityId.LURKERDENRESEARCH_RESEARCHLURKERRANGE, "required_building": UnitTypeId.HIVE, }, }, UnitTypeId.ROACHWARREN: { UpgradeId.GLIALRECONSTITUTION: { "ability": AbilityId.RESEARCH_GLIALREGENERATION, "required_building": UnitTypeId.LAIR, }, UpgradeId.TUNNELINGCLAWS: {"ability": AbilityId.RESEARCH_TUNNELINGCLAWS, "required_building": UnitTypeId.LAIR}, }, UnitTypeId.ROBOTICSBAY: { UpgradeId.EXTENDEDTHERMALLANCE: {"ability": AbilityId.RESEARCH_EXTENDEDTHERMALLANCE, "requires_power": True}, UpgradeId.GRAVITICDRIVE: {"ability": AbilityId.RESEARCH_GRAVITICDRIVE, "requires_power": True}, UpgradeId.OBSERVERGRAVITICBOOSTER: {"ability": AbilityId.RESEARCH_GRAVITICBOOSTER, "requires_power": True}, }, UnitTypeId.SPAWNINGPOOL: { UpgradeId.ZERGLINGATTACKSPEED: { "ability": AbilityId.RESEARCH_ZERGLINGADRENALGLANDS, "required_building": UnitTypeId.HIVE, }, UpgradeId.ZERGLINGMOVEMENTSPEED: {"ability": AbilityId.RESEARCH_ZERGLINGMETABOLICBOOST}, }, UnitTypeId.SPIRE: { UpgradeId.ZERGFLYERARMORSLEVEL1: {"ability": AbilityId.RESEARCH_ZERGFLYERARMORLEVEL1}, UpgradeId.ZERGFLYERARMORSLEVEL2: { "ability": AbilityId.RESEARCH_ZERGFLYERARMORLEVEL2, "required_building": UnitTypeId.LAIR, "required_upgrade": UpgradeId.ZERGFLYERARMORSLEVEL1, }, UpgradeId.ZERGFLYERARMORSLEVEL3: { "ability": AbilityId.RESEARCH_ZERGFLYERARMORLEVEL3, "required_building": UnitTypeId.HIVE, "required_upgrade": UpgradeId.ZERGFLYERARMORSLEVEL2, }, UpgradeId.ZERGFLYERWEAPONSLEVEL1: {"ability": AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL1}, UpgradeId.ZERGFLYERWEAPONSLEVEL2: { "ability": AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL2, "required_building": UnitTypeId.LAIR, "required_upgrade": UpgradeId.ZERGFLYERWEAPONSLEVEL1, }, UpgradeId.ZERGFLYERWEAPONSLEVEL3: { "ability": AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL3, "required_building": UnitTypeId.HIVE, "required_upgrade": UpgradeId.ZERGFLYERWEAPONSLEVEL2, }, }, UnitTypeId.STARPORTTECHLAB: { UpgradeId.BANSHEECLOAK: {"ability": AbilityId.RESEARCH_BANSHEECLOAKINGFIELD}, UpgradeId.BANSHEESPEED: {"ability": AbilityId.RESEARCH_BANSHEEHYPERFLIGHTROTORS}, UpgradeId.INTERFERENCEMATRIX: {"ability": AbilityId.STARPORTTECHLABRESEARCH_RESEARCHRAVENINTERFERENCEMATRIX}, }, UnitTypeId.TEMPLARARCHIVE: { UpgradeId.PSISTORMTECH: {"ability": AbilityId.RESEARCH_PSISTORM, "requires_power": True} }, UnitTypeId.TWILIGHTCOUNCIL: { UpgradeId.ADEPTPIERCINGATTACK: {"ability": AbilityId.RESEARCH_ADEPTRESONATINGGLAIVES, "requires_power": True}, UpgradeId.BLINKTECH: {"ability": AbilityId.RESEARCH_BLINK, "requires_power": True}, UpgradeId.CHARGE: {"ability": AbilityId.RESEARCH_CHARGE, "requires_power": True}, }, UnitTypeId.ULTRALISKCAVERN: { UpgradeId.ANABOLICSYNTHESIS: {"ability": AbilityId.RESEARCH_ANABOLICSYNTHESIS}, UpgradeId.CHITINOUSPLATING: {"ability": AbilityId.RESEARCH_CHITINOUSPLATING}, }, } ================================================ FILE: sc2/dicts/unit_tech_alias.py ================================================ # THIS FILE WAS AUTOMATICALLY GENERATED BY "generate_dicts_from_data_json.py" DO NOT CHANGE MANUALLY! # ANY CHANGE WILL BE OVERWRITTEN from sc2.ids.unit_typeid import UnitTypeId # from sc2.ids.buff_id import BuffId # from sc2.ids.effect_id import EffectId UNIT_TECH_ALIAS: dict[UnitTypeId, set[UnitTypeId]] = { UnitTypeId.BARRACKSFLYING: {UnitTypeId.BARRACKS}, UnitTypeId.BARRACKSREACTOR: {UnitTypeId.REACTOR}, UnitTypeId.BARRACKSTECHLAB: {UnitTypeId.TECHLAB}, UnitTypeId.COMMANDCENTERFLYING: {UnitTypeId.COMMANDCENTER}, UnitTypeId.CREEPTUMORBURROWED: {UnitTypeId.CREEPTUMOR}, UnitTypeId.CREEPTUMORQUEEN: {UnitTypeId.CREEPTUMOR}, UnitTypeId.FACTORYFLYING: {UnitTypeId.FACTORY}, UnitTypeId.FACTORYREACTOR: {UnitTypeId.REACTOR}, UnitTypeId.FACTORYTECHLAB: {UnitTypeId.TECHLAB}, UnitTypeId.GREATERSPIRE: {UnitTypeId.SPIRE}, UnitTypeId.HIVE: {UnitTypeId.HATCHERY, UnitTypeId.LAIR}, UnitTypeId.LAIR: {UnitTypeId.HATCHERY}, UnitTypeId.LIBERATORAG: {UnitTypeId.LIBERATOR}, UnitTypeId.ORBITALCOMMAND: {UnitTypeId.COMMANDCENTER}, UnitTypeId.ORBITALCOMMANDFLYING: {UnitTypeId.COMMANDCENTER}, UnitTypeId.OVERLORDTRANSPORT: {UnitTypeId.OVERLORD}, UnitTypeId.OVERSEER: {UnitTypeId.OVERLORD}, UnitTypeId.OVERSEERSIEGEMODE: {UnitTypeId.OVERLORD}, UnitTypeId.PLANETARYFORTRESS: {UnitTypeId.COMMANDCENTER}, UnitTypeId.PYLONOVERCHARGED: {UnitTypeId.PYLON}, UnitTypeId.QUEENBURROWED: {UnitTypeId.QUEEN}, UnitTypeId.SIEGETANKSIEGED: {UnitTypeId.SIEGETANK}, UnitTypeId.STARPORTFLYING: {UnitTypeId.STARPORT}, UnitTypeId.STARPORTREACTOR: {UnitTypeId.REACTOR}, UnitTypeId.STARPORTTECHLAB: {UnitTypeId.TECHLAB}, UnitTypeId.SUPPLYDEPOTLOWERED: {UnitTypeId.SUPPLYDEPOT}, UnitTypeId.THORAP: {UnitTypeId.THOR}, UnitTypeId.VIKINGASSAULT: {UnitTypeId.VIKING}, UnitTypeId.VIKINGFIGHTER: {UnitTypeId.VIKING}, UnitTypeId.WARPGATE: {UnitTypeId.GATEWAY}, UnitTypeId.WARPPRISMPHASING: {UnitTypeId.WARPPRISM}, UnitTypeId.WIDOWMINEBURROWED: {UnitTypeId.WIDOWMINE}, } ================================================ FILE: sc2/dicts/unit_train_build_abilities.py ================================================ # THIS FILE WAS AUTOMATICALLY GENERATED BY "generate_dicts_from_data_json.py" DO NOT CHANGE MANUALLY! # ANY CHANGE WILL BE OVERWRITTEN # from sc2.ids.buff_id import BuffId # from sc2.ids.effect_id import EffectId from typing import Union from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId TRAIN_INFO: dict[UnitTypeId, dict[UnitTypeId, dict[str, Union[AbilityId, bool, UnitTypeId]]]] = { UnitTypeId.BARRACKS: { UnitTypeId.GHOST: { "ability": AbilityId.BARRACKSTRAIN_GHOST, "requires_techlab": True, "required_building": UnitTypeId.GHOSTACADEMY, }, UnitTypeId.MARAUDER: {"ability": AbilityId.BARRACKSTRAIN_MARAUDER, "requires_techlab": True}, UnitTypeId.MARINE: {"ability": AbilityId.BARRACKSTRAIN_MARINE}, UnitTypeId.REAPER: {"ability": AbilityId.BARRACKSTRAIN_REAPER}, }, UnitTypeId.COMMANDCENTER: { UnitTypeId.ORBITALCOMMAND: { "ability": AbilityId.UPGRADETOORBITAL_ORBITALCOMMAND, "required_building": UnitTypeId.BARRACKS, }, UnitTypeId.PLANETARYFORTRESS: { "ability": AbilityId.UPGRADETOPLANETARYFORTRESS_PLANETARYFORTRESS, "required_building": UnitTypeId.ENGINEERINGBAY, }, UnitTypeId.SCV: {"ability": AbilityId.COMMANDCENTERTRAIN_SCV}, }, UnitTypeId.CORRUPTOR: { UnitTypeId.BROODLORD: { "ability": AbilityId.MORPHTOBROODLORD_BROODLORD, "required_building": UnitTypeId.GREATERSPIRE, } }, UnitTypeId.CREEPTUMOR: { UnitTypeId.CREEPTUMOR: {"ability": AbilityId.BUILD_CREEPTUMOR_TUMOR, "requires_placement_position": True} }, UnitTypeId.CREEPTUMORBURROWED: { UnitTypeId.CREEPTUMOR: {"ability": AbilityId.BUILD_CREEPTUMOR, "requires_placement_position": True} }, UnitTypeId.DRONE: { UnitTypeId.BANELINGNEST: { "ability": AbilityId.ZERGBUILD_BANELINGNEST, "required_building": UnitTypeId.SPAWNINGPOOL, "requires_placement_position": True, }, UnitTypeId.EVOLUTIONCHAMBER: { "ability": AbilityId.ZERGBUILD_EVOLUTIONCHAMBER, "required_building": UnitTypeId.HATCHERY, "requires_placement_position": True, }, UnitTypeId.EXTRACTOR: {"ability": AbilityId.ZERGBUILD_EXTRACTOR}, UnitTypeId.HATCHERY: {"ability": AbilityId.ZERGBUILD_HATCHERY, "requires_placement_position": True}, UnitTypeId.HYDRALISKDEN: { "ability": AbilityId.ZERGBUILD_HYDRALISKDEN, "required_building": UnitTypeId.LAIR, "requires_placement_position": True, }, UnitTypeId.INFESTATIONPIT: { "ability": AbilityId.ZERGBUILD_INFESTATIONPIT, "required_building": UnitTypeId.LAIR, "requires_placement_position": True, }, UnitTypeId.LURKERDENMP: { "ability": AbilityId.BUILD_LURKERDEN, "required_building": UnitTypeId.HYDRALISKDEN, "requires_placement_position": True, }, UnitTypeId.NYDUSNETWORK: { "ability": AbilityId.ZERGBUILD_NYDUSNETWORK, "required_building": UnitTypeId.LAIR, "requires_placement_position": True, }, UnitTypeId.ROACHWARREN: { "ability": AbilityId.ZERGBUILD_ROACHWARREN, "required_building": UnitTypeId.SPAWNINGPOOL, "requires_placement_position": True, }, UnitTypeId.SPAWNINGPOOL: { "ability": AbilityId.ZERGBUILD_SPAWNINGPOOL, "required_building": UnitTypeId.HATCHERY, "requires_placement_position": True, }, UnitTypeId.SPINECRAWLER: { "ability": AbilityId.ZERGBUILD_SPINECRAWLER, "required_building": UnitTypeId.SPAWNINGPOOL, "requires_placement_position": True, }, UnitTypeId.SPIRE: { "ability": AbilityId.ZERGBUILD_SPIRE, "required_building": UnitTypeId.LAIR, "requires_placement_position": True, }, UnitTypeId.SPORECRAWLER: { "ability": AbilityId.ZERGBUILD_SPORECRAWLER, "required_building": UnitTypeId.SPAWNINGPOOL, "requires_placement_position": True, }, UnitTypeId.ULTRALISKCAVERN: { "ability": AbilityId.ZERGBUILD_ULTRALISKCAVERN, "required_building": UnitTypeId.HIVE, "requires_placement_position": True, }, }, UnitTypeId.FACTORY: { UnitTypeId.CYCLONE: {"ability": AbilityId.TRAIN_CYCLONE, "requires_techlab": True}, UnitTypeId.HELLION: {"ability": AbilityId.FACTORYTRAIN_HELLION}, UnitTypeId.HELLIONTANK: {"ability": AbilityId.TRAIN_HELLBAT, "required_building": UnitTypeId.ARMORY}, UnitTypeId.SIEGETANK: {"ability": AbilityId.FACTORYTRAIN_SIEGETANK, "requires_techlab": True}, UnitTypeId.THOR: { "ability": AbilityId.FACTORYTRAIN_THOR, "requires_techlab": True, "required_building": UnitTypeId.ARMORY, }, UnitTypeId.WIDOWMINE: {"ability": AbilityId.FACTORYTRAIN_WIDOWMINE}, }, UnitTypeId.GATEWAY: { UnitTypeId.ADEPT: { "ability": AbilityId.TRAIN_ADEPT, "required_building": UnitTypeId.CYBERNETICSCORE, "requires_power": True, }, UnitTypeId.DARKTEMPLAR: { "ability": AbilityId.GATEWAYTRAIN_DARKTEMPLAR, "required_building": UnitTypeId.DARKSHRINE, "requires_power": True, }, UnitTypeId.HIGHTEMPLAR: { "ability": AbilityId.GATEWAYTRAIN_HIGHTEMPLAR, "required_building": UnitTypeId.TEMPLARARCHIVE, "requires_power": True, }, UnitTypeId.SENTRY: { "ability": AbilityId.GATEWAYTRAIN_SENTRY, "required_building": UnitTypeId.CYBERNETICSCORE, "requires_power": True, }, UnitTypeId.STALKER: { "ability": AbilityId.GATEWAYTRAIN_STALKER, "required_building": UnitTypeId.CYBERNETICSCORE, "requires_power": True, }, UnitTypeId.ZEALOT: {"ability": AbilityId.GATEWAYTRAIN_ZEALOT, "requires_power": True}, }, UnitTypeId.HATCHERY: { UnitTypeId.LAIR: {"ability": AbilityId.UPGRADETOLAIR_LAIR, "required_building": UnitTypeId.SPAWNINGPOOL}, UnitTypeId.QUEEN: {"ability": AbilityId.TRAINQUEEN_QUEEN, "required_building": UnitTypeId.SPAWNINGPOOL}, }, UnitTypeId.HIVE: { UnitTypeId.QUEEN: {"ability": AbilityId.TRAINQUEEN_QUEEN, "required_building": UnitTypeId.SPAWNINGPOOL} }, UnitTypeId.HYDRALISK: { UnitTypeId.LURKERMP: {"ability": AbilityId.MORPH_LURKER, "required_building": UnitTypeId.LURKERDENMP} }, UnitTypeId.LAIR: { UnitTypeId.HIVE: {"ability": AbilityId.UPGRADETOHIVE_HIVE, "required_building": UnitTypeId.INFESTATIONPIT}, UnitTypeId.QUEEN: {"ability": AbilityId.TRAINQUEEN_QUEEN, "required_building": UnitTypeId.SPAWNINGPOOL}, }, UnitTypeId.LARVA: { UnitTypeId.CORRUPTOR: {"ability": AbilityId.LARVATRAIN_CORRUPTOR, "required_building": UnitTypeId.SPIRE}, UnitTypeId.DRONE: {"ability": AbilityId.LARVATRAIN_DRONE}, UnitTypeId.HYDRALISK: {"ability": AbilityId.LARVATRAIN_HYDRALISK, "required_building": UnitTypeId.HYDRALISKDEN}, UnitTypeId.INFESTOR: {"ability": AbilityId.LARVATRAIN_INFESTOR, "required_building": UnitTypeId.INFESTATIONPIT}, UnitTypeId.MUTALISK: {"ability": AbilityId.LARVATRAIN_MUTALISK, "required_building": UnitTypeId.SPIRE}, UnitTypeId.OVERLORD: {"ability": AbilityId.LARVATRAIN_OVERLORD}, UnitTypeId.ROACH: {"ability": AbilityId.LARVATRAIN_ROACH, "required_building": UnitTypeId.ROACHWARREN}, UnitTypeId.SWARMHOSTMP: {"ability": AbilityId.TRAIN_SWARMHOST, "required_building": UnitTypeId.INFESTATIONPIT}, UnitTypeId.ULTRALISK: { "ability": AbilityId.LARVATRAIN_ULTRALISK, "required_building": UnitTypeId.ULTRALISKCAVERN, }, UnitTypeId.VIPER: {"ability": AbilityId.LARVATRAIN_VIPER, "required_building": UnitTypeId.HIVE}, UnitTypeId.ZERGLING: {"ability": AbilityId.LARVATRAIN_ZERGLING, "required_building": UnitTypeId.SPAWNINGPOOL}, }, UnitTypeId.NEXUS: { UnitTypeId.MOTHERSHIP: { "ability": AbilityId.NEXUSTRAINMOTHERSHIP_MOTHERSHIP, "required_building": UnitTypeId.FLEETBEACON, }, UnitTypeId.PROBE: {"ability": AbilityId.NEXUSTRAIN_PROBE}, }, UnitTypeId.NYDUSNETWORK: { UnitTypeId.NYDUSCANAL: {"ability": AbilityId.BUILD_NYDUSWORM, "requires_placement_position": True} }, UnitTypeId.ORACLE: { UnitTypeId.ORACLESTASISTRAP: {"ability": AbilityId.BUILD_STASISTRAP, "requires_placement_position": True} }, UnitTypeId.ORBITALCOMMAND: {UnitTypeId.SCV: {"ability": AbilityId.COMMANDCENTERTRAIN_SCV}}, UnitTypeId.OVERLORD: { UnitTypeId.OVERLORDTRANSPORT: { "ability": AbilityId.MORPH_OVERLORDTRANSPORT, "required_building": UnitTypeId.LAIR, }, UnitTypeId.OVERSEER: {"ability": AbilityId.MORPH_OVERSEER, "required_building": UnitTypeId.LAIR}, }, UnitTypeId.OVERLORDTRANSPORT: { UnitTypeId.OVERSEER: {"ability": AbilityId.MORPH_OVERSEER, "required_building": UnitTypeId.LAIR} }, UnitTypeId.OVERSEER: {UnitTypeId.CHANGELING: {"ability": AbilityId.SPAWNCHANGELING_SPAWNCHANGELING}}, UnitTypeId.OVERSEERSIEGEMODE: {UnitTypeId.CHANGELING: {"ability": AbilityId.SPAWNCHANGELING_SPAWNCHANGELING}}, UnitTypeId.PLANETARYFORTRESS: {UnitTypeId.SCV: {"ability": AbilityId.COMMANDCENTERTRAIN_SCV}}, UnitTypeId.PROBE: { UnitTypeId.ASSIMILATOR: {"ability": AbilityId.PROTOSSBUILD_ASSIMILATOR}, UnitTypeId.CYBERNETICSCORE: { "ability": AbilityId.PROTOSSBUILD_CYBERNETICSCORE, "required_building": UnitTypeId.GATEWAY, "requires_placement_position": True, }, UnitTypeId.DARKSHRINE: { "ability": AbilityId.PROTOSSBUILD_DARKSHRINE, "required_building": UnitTypeId.TWILIGHTCOUNCIL, "requires_placement_position": True, }, UnitTypeId.FLEETBEACON: { "ability": AbilityId.PROTOSSBUILD_FLEETBEACON, "required_building": UnitTypeId.STARGATE, "requires_placement_position": True, }, UnitTypeId.FORGE: { "ability": AbilityId.PROTOSSBUILD_FORGE, "required_building": UnitTypeId.PYLON, "requires_placement_position": True, }, UnitTypeId.GATEWAY: { "ability": AbilityId.PROTOSSBUILD_GATEWAY, "required_building": UnitTypeId.PYLON, "requires_placement_position": True, }, UnitTypeId.NEXUS: {"ability": AbilityId.PROTOSSBUILD_NEXUS, "requires_placement_position": True}, UnitTypeId.PHOTONCANNON: { "ability": AbilityId.PROTOSSBUILD_PHOTONCANNON, "required_building": UnitTypeId.FORGE, "requires_placement_position": True, }, UnitTypeId.PYLON: {"ability": AbilityId.PROTOSSBUILD_PYLON, "requires_placement_position": True}, UnitTypeId.ROBOTICSBAY: { "ability": AbilityId.PROTOSSBUILD_ROBOTICSBAY, "required_building": UnitTypeId.ROBOTICSFACILITY, "requires_placement_position": True, }, UnitTypeId.ROBOTICSFACILITY: { "ability": AbilityId.PROTOSSBUILD_ROBOTICSFACILITY, "required_building": UnitTypeId.CYBERNETICSCORE, "requires_placement_position": True, }, UnitTypeId.SHIELDBATTERY: { "ability": AbilityId.BUILD_SHIELDBATTERY, "required_building": UnitTypeId.CYBERNETICSCORE, "requires_placement_position": True, }, UnitTypeId.STARGATE: { "ability": AbilityId.PROTOSSBUILD_STARGATE, "required_building": UnitTypeId.CYBERNETICSCORE, "requires_placement_position": True, }, UnitTypeId.TEMPLARARCHIVE: { "ability": AbilityId.PROTOSSBUILD_TEMPLARARCHIVE, "required_building": UnitTypeId.TWILIGHTCOUNCIL, "requires_placement_position": True, }, UnitTypeId.TWILIGHTCOUNCIL: { "ability": AbilityId.PROTOSSBUILD_TWILIGHTCOUNCIL, "required_building": UnitTypeId.CYBERNETICSCORE, "requires_placement_position": True, }, }, UnitTypeId.QUEEN: { UnitTypeId.CREEPTUMOR: {"ability": AbilityId.BUILD_CREEPTUMOR, "requires_placement_position": True}, UnitTypeId.CREEPTUMORQUEEN: {"ability": AbilityId.BUILD_CREEPTUMOR_QUEEN, "requires_placement_position": True}, }, UnitTypeId.RAVEN: {UnitTypeId.AUTOTURRET: {"ability": AbilityId.BUILDAUTOTURRET_AUTOTURRET}}, UnitTypeId.ROACH: { UnitTypeId.RAVAGER: {"ability": AbilityId.MORPHTORAVAGER_RAVAGER, "required_building": UnitTypeId.HATCHERY} }, UnitTypeId.ROBOTICSFACILITY: { UnitTypeId.COLOSSUS: { "ability": AbilityId.ROBOTICSFACILITYTRAIN_COLOSSUS, "required_building": UnitTypeId.ROBOTICSBAY, "requires_power": True, }, UnitTypeId.DISRUPTOR: { "ability": AbilityId.TRAIN_DISRUPTOR, "required_building": UnitTypeId.ROBOTICSBAY, "requires_power": True, }, UnitTypeId.IMMORTAL: {"ability": AbilityId.ROBOTICSFACILITYTRAIN_IMMORTAL, "requires_power": True}, UnitTypeId.OBSERVER: {"ability": AbilityId.ROBOTICSFACILITYTRAIN_OBSERVER, "requires_power": True}, UnitTypeId.WARPPRISM: {"ability": AbilityId.ROBOTICSFACILITYTRAIN_WARPPRISM, "requires_power": True}, }, UnitTypeId.SCV: { UnitTypeId.ARMORY: { "ability": AbilityId.TERRANBUILD_ARMORY, "required_building": UnitTypeId.FACTORY, "requires_placement_position": True, }, UnitTypeId.BARRACKS: { "ability": AbilityId.TERRANBUILD_BARRACKS, "required_building": UnitTypeId.SUPPLYDEPOT, "requires_placement_position": True, }, UnitTypeId.BUNKER: { "ability": AbilityId.TERRANBUILD_BUNKER, "required_building": UnitTypeId.BARRACKS, "requires_placement_position": True, }, UnitTypeId.COMMANDCENTER: {"ability": AbilityId.TERRANBUILD_COMMANDCENTER, "requires_placement_position": True}, UnitTypeId.ENGINEERINGBAY: { "ability": AbilityId.TERRANBUILD_ENGINEERINGBAY, "required_building": UnitTypeId.COMMANDCENTER, "requires_placement_position": True, }, UnitTypeId.FACTORY: { "ability": AbilityId.TERRANBUILD_FACTORY, "required_building": UnitTypeId.BARRACKS, "requires_placement_position": True, }, UnitTypeId.FUSIONCORE: { "ability": AbilityId.TERRANBUILD_FUSIONCORE, "required_building": UnitTypeId.STARPORT, "requires_placement_position": True, }, UnitTypeId.GHOSTACADEMY: { "ability": AbilityId.TERRANBUILD_GHOSTACADEMY, "required_building": UnitTypeId.BARRACKS, "requires_placement_position": True, }, UnitTypeId.MISSILETURRET: { "ability": AbilityId.TERRANBUILD_MISSILETURRET, "required_building": UnitTypeId.ENGINEERINGBAY, "requires_placement_position": True, }, UnitTypeId.REFINERY: {"ability": AbilityId.TERRANBUILD_REFINERY}, UnitTypeId.SENSORTOWER: { "ability": AbilityId.TERRANBUILD_SENSORTOWER, "required_building": UnitTypeId.ENGINEERINGBAY, "requires_placement_position": True, }, UnitTypeId.STARPORT: { "ability": AbilityId.TERRANBUILD_STARPORT, "required_building": UnitTypeId.FACTORY, "requires_placement_position": True, }, UnitTypeId.SUPPLYDEPOT: {"ability": AbilityId.TERRANBUILD_SUPPLYDEPOT, "requires_placement_position": True}, }, UnitTypeId.SPIRE: { UnitTypeId.GREATERSPIRE: { "ability": AbilityId.UPGRADETOGREATERSPIRE_GREATERSPIRE, "required_building": UnitTypeId.HIVE, } }, UnitTypeId.STARGATE: { UnitTypeId.CARRIER: { "ability": AbilityId.STARGATETRAIN_CARRIER, "required_building": UnitTypeId.FLEETBEACON, "requires_power": True, }, UnitTypeId.ORACLE: {"ability": AbilityId.STARGATETRAIN_ORACLE, "requires_power": True}, UnitTypeId.PHOENIX: {"ability": AbilityId.STARGATETRAIN_PHOENIX, "requires_power": True}, UnitTypeId.TEMPEST: { "ability": AbilityId.STARGATETRAIN_TEMPEST, "required_building": UnitTypeId.FLEETBEACON, "requires_power": True, }, UnitTypeId.VOIDRAY: {"ability": AbilityId.STARGATETRAIN_VOIDRAY, "requires_power": True}, }, UnitTypeId.STARPORT: { UnitTypeId.BANSHEE: {"ability": AbilityId.STARPORTTRAIN_BANSHEE, "requires_techlab": True}, UnitTypeId.BATTLECRUISER: { "ability": AbilityId.STARPORTTRAIN_BATTLECRUISER, "requires_techlab": True, "required_building": UnitTypeId.FUSIONCORE, }, UnitTypeId.LIBERATOR: {"ability": AbilityId.STARPORTTRAIN_LIBERATOR}, UnitTypeId.MEDIVAC: {"ability": AbilityId.STARPORTTRAIN_MEDIVAC}, UnitTypeId.RAVEN: {"ability": AbilityId.STARPORTTRAIN_RAVEN, "requires_techlab": True}, UnitTypeId.VIKINGFIGHTER: {"ability": AbilityId.STARPORTTRAIN_VIKINGFIGHTER}, }, UnitTypeId.SWARMHOSTBURROWEDMP: {UnitTypeId.LOCUSTMPFLYING: {"ability": AbilityId.EFFECT_SPAWNLOCUSTS}}, UnitTypeId.SWARMHOSTMP: {UnitTypeId.LOCUSTMPFLYING: {"ability": AbilityId.EFFECT_SPAWNLOCUSTS}}, UnitTypeId.WARPGATE: { UnitTypeId.ADEPT: { "ability": AbilityId.TRAINWARP_ADEPT, "required_building": UnitTypeId.CYBERNETICSCORE, "requires_placement_position": True, "requires_power": True, }, UnitTypeId.DARKTEMPLAR: { "ability": AbilityId.WARPGATETRAIN_DARKTEMPLAR, "required_building": UnitTypeId.DARKSHRINE, "requires_placement_position": True, "requires_power": True, }, UnitTypeId.HIGHTEMPLAR: { "ability": AbilityId.WARPGATETRAIN_HIGHTEMPLAR, "required_building": UnitTypeId.TEMPLARARCHIVE, "requires_placement_position": True, "requires_power": True, }, UnitTypeId.SENTRY: { "ability": AbilityId.WARPGATETRAIN_SENTRY, "required_building": UnitTypeId.CYBERNETICSCORE, "requires_placement_position": True, "requires_power": True, }, UnitTypeId.STALKER: { "ability": AbilityId.WARPGATETRAIN_STALKER, "required_building": UnitTypeId.CYBERNETICSCORE, "requires_placement_position": True, "requires_power": True, }, UnitTypeId.ZEALOT: { "ability": AbilityId.WARPGATETRAIN_ZEALOT, "requires_placement_position": True, "requires_power": True, }, }, UnitTypeId.ZERGLING: { UnitTypeId.BANELING: { "ability": AbilityId.MORPHTOBANELING_BANELING, "required_building": UnitTypeId.BANELINGNEST, } }, } ================================================ FILE: sc2/dicts/unit_trained_from.py ================================================ # THIS FILE WAS AUTOMATICALLY GENERATED BY "generate_dicts_from_data_json.py" DO NOT CHANGE MANUALLY! # ANY CHANGE WILL BE OVERWRITTEN from sc2.ids.unit_typeid import UnitTypeId # from sc2.ids.buff_id import BuffId # from sc2.ids.effect_id import EffectId UNIT_TRAINED_FROM: dict[UnitTypeId, set[UnitTypeId]] = { UnitTypeId.ADEPT: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE}, UnitTypeId.ARMORY: {UnitTypeId.SCV}, UnitTypeId.ASSIMILATOR: {UnitTypeId.PROBE}, UnitTypeId.AUTOTURRET: {UnitTypeId.RAVEN}, UnitTypeId.BANELING: {UnitTypeId.ZERGLING}, UnitTypeId.BANELINGNEST: {UnitTypeId.DRONE}, UnitTypeId.BANSHEE: {UnitTypeId.STARPORT}, UnitTypeId.BARRACKS: {UnitTypeId.SCV}, UnitTypeId.BATTLECRUISER: {UnitTypeId.STARPORT}, UnitTypeId.BROODLORD: {UnitTypeId.CORRUPTOR}, UnitTypeId.BUNKER: {UnitTypeId.SCV}, UnitTypeId.CARRIER: {UnitTypeId.STARGATE}, UnitTypeId.CHANGELING: {UnitTypeId.OVERSEER, UnitTypeId.OVERSEERSIEGEMODE}, UnitTypeId.COLOSSUS: {UnitTypeId.ROBOTICSFACILITY}, UnitTypeId.COMMANDCENTER: {UnitTypeId.SCV}, UnitTypeId.CORRUPTOR: {UnitTypeId.LARVA}, UnitTypeId.CREEPTUMOR: {UnitTypeId.CREEPTUMORBURROWED, UnitTypeId.QUEEN}, UnitTypeId.CREEPTUMORQUEEN: {UnitTypeId.QUEEN}, UnitTypeId.CYBERNETICSCORE: {UnitTypeId.PROBE}, UnitTypeId.CYCLONE: {UnitTypeId.FACTORY}, UnitTypeId.DARKSHRINE: {UnitTypeId.PROBE}, UnitTypeId.DARKTEMPLAR: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE}, UnitTypeId.DISRUPTOR: {UnitTypeId.ROBOTICSFACILITY}, UnitTypeId.DRONE: {UnitTypeId.LARVA}, UnitTypeId.ENGINEERINGBAY: {UnitTypeId.SCV}, UnitTypeId.EVOLUTIONCHAMBER: {UnitTypeId.DRONE}, UnitTypeId.EXTRACTOR: {UnitTypeId.DRONE}, UnitTypeId.FACTORY: {UnitTypeId.SCV}, UnitTypeId.FLEETBEACON: {UnitTypeId.PROBE}, UnitTypeId.FORGE: {UnitTypeId.PROBE}, UnitTypeId.FUSIONCORE: {UnitTypeId.SCV}, UnitTypeId.GATEWAY: {UnitTypeId.PROBE}, UnitTypeId.GHOST: {UnitTypeId.BARRACKS}, UnitTypeId.GHOSTACADEMY: {UnitTypeId.SCV}, UnitTypeId.GREATERSPIRE: {UnitTypeId.SPIRE}, UnitTypeId.HATCHERY: {UnitTypeId.DRONE}, UnitTypeId.HELLION: {UnitTypeId.FACTORY}, UnitTypeId.HELLIONTANK: {UnitTypeId.FACTORY}, UnitTypeId.HIGHTEMPLAR: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE}, UnitTypeId.HIVE: {UnitTypeId.LAIR}, UnitTypeId.HYDRALISK: {UnitTypeId.LARVA}, UnitTypeId.HYDRALISKDEN: {UnitTypeId.DRONE}, UnitTypeId.IMMORTAL: {UnitTypeId.ROBOTICSFACILITY}, UnitTypeId.INFESTATIONPIT: {UnitTypeId.DRONE}, UnitTypeId.INFESTOR: {UnitTypeId.LARVA}, UnitTypeId.LAIR: {UnitTypeId.HATCHERY}, UnitTypeId.LIBERATOR: {UnitTypeId.STARPORT}, UnitTypeId.LOCUSTMPFLYING: {UnitTypeId.SWARMHOSTBURROWEDMP, UnitTypeId.SWARMHOSTMP}, UnitTypeId.LURKERDENMP: {UnitTypeId.DRONE}, UnitTypeId.LURKERMP: {UnitTypeId.HYDRALISK}, UnitTypeId.MARAUDER: {UnitTypeId.BARRACKS}, UnitTypeId.MARINE: {UnitTypeId.BARRACKS}, UnitTypeId.MEDIVAC: {UnitTypeId.STARPORT}, UnitTypeId.MISSILETURRET: {UnitTypeId.SCV}, UnitTypeId.MOTHERSHIP: {UnitTypeId.NEXUS}, UnitTypeId.MUTALISK: {UnitTypeId.LARVA}, UnitTypeId.NEXUS: {UnitTypeId.PROBE}, UnitTypeId.NYDUSCANAL: {UnitTypeId.NYDUSNETWORK}, UnitTypeId.NYDUSNETWORK: {UnitTypeId.DRONE}, UnitTypeId.OBSERVER: {UnitTypeId.ROBOTICSFACILITY}, UnitTypeId.ORACLE: {UnitTypeId.STARGATE}, UnitTypeId.ORACLESTASISTRAP: {UnitTypeId.ORACLE}, UnitTypeId.ORBITALCOMMAND: {UnitTypeId.COMMANDCENTER}, UnitTypeId.OVERLORD: {UnitTypeId.LARVA}, UnitTypeId.OVERLORDTRANSPORT: {UnitTypeId.OVERLORD}, UnitTypeId.OVERSEER: {UnitTypeId.OVERLORD, UnitTypeId.OVERLORDTRANSPORT}, UnitTypeId.PHOENIX: {UnitTypeId.STARGATE}, UnitTypeId.PHOTONCANNON: {UnitTypeId.PROBE}, UnitTypeId.PLANETARYFORTRESS: {UnitTypeId.COMMANDCENTER}, UnitTypeId.PROBE: {UnitTypeId.NEXUS}, UnitTypeId.PYLON: {UnitTypeId.PROBE}, UnitTypeId.QUEEN: {UnitTypeId.HATCHERY, UnitTypeId.HIVE, UnitTypeId.LAIR}, UnitTypeId.RAVAGER: {UnitTypeId.ROACH}, UnitTypeId.RAVEN: {UnitTypeId.STARPORT}, UnitTypeId.REAPER: {UnitTypeId.BARRACKS}, UnitTypeId.REFINERY: {UnitTypeId.SCV}, UnitTypeId.ROACH: {UnitTypeId.LARVA}, UnitTypeId.ROACHWARREN: {UnitTypeId.DRONE}, UnitTypeId.ROBOTICSBAY: {UnitTypeId.PROBE}, UnitTypeId.ROBOTICSFACILITY: {UnitTypeId.PROBE}, UnitTypeId.SCV: {UnitTypeId.COMMANDCENTER, UnitTypeId.ORBITALCOMMAND, UnitTypeId.PLANETARYFORTRESS}, UnitTypeId.SENSORTOWER: {UnitTypeId.SCV}, UnitTypeId.SENTRY: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE}, UnitTypeId.SHIELDBATTERY: {UnitTypeId.PROBE}, UnitTypeId.SIEGETANK: {UnitTypeId.FACTORY}, UnitTypeId.SPAWNINGPOOL: {UnitTypeId.DRONE}, UnitTypeId.SPINECRAWLER: {UnitTypeId.DRONE}, UnitTypeId.SPIRE: {UnitTypeId.DRONE}, UnitTypeId.SPORECRAWLER: {UnitTypeId.DRONE}, UnitTypeId.STALKER: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE}, UnitTypeId.STARGATE: {UnitTypeId.PROBE}, UnitTypeId.STARPORT: {UnitTypeId.SCV}, UnitTypeId.SUPPLYDEPOT: {UnitTypeId.SCV}, UnitTypeId.SWARMHOSTMP: {UnitTypeId.LARVA}, UnitTypeId.TEMPEST: {UnitTypeId.STARGATE}, UnitTypeId.TEMPLARARCHIVE: {UnitTypeId.PROBE}, UnitTypeId.THOR: {UnitTypeId.FACTORY}, UnitTypeId.TWILIGHTCOUNCIL: {UnitTypeId.PROBE}, UnitTypeId.ULTRALISK: {UnitTypeId.LARVA}, UnitTypeId.ULTRALISKCAVERN: {UnitTypeId.DRONE}, UnitTypeId.VIKINGFIGHTER: {UnitTypeId.STARPORT}, UnitTypeId.VIPER: {UnitTypeId.LARVA}, UnitTypeId.VOIDRAY: {UnitTypeId.STARGATE}, UnitTypeId.WARPPRISM: {UnitTypeId.ROBOTICSFACILITY}, UnitTypeId.WIDOWMINE: {UnitTypeId.FACTORY}, UnitTypeId.ZEALOT: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE}, UnitTypeId.ZERGLING: {UnitTypeId.LARVA}, } ================================================ FILE: sc2/dicts/unit_unit_alias.py ================================================ # THIS FILE WAS AUTOMATICALLY GENERATED BY "generate_dicts_from_data_json.py" DO NOT CHANGE MANUALLY! # ANY CHANGE WILL BE OVERWRITTEN from sc2.ids.unit_typeid import UnitTypeId # from sc2.ids.buff_id import BuffId # from sc2.ids.effect_id import EffectId UNIT_UNIT_ALIAS: dict[UnitTypeId, UnitTypeId] = { UnitTypeId.ADEPTPHASESHIFT: UnitTypeId.ADEPT, UnitTypeId.BANELINGBURROWED: UnitTypeId.BANELING, UnitTypeId.BARRACKSFLYING: UnitTypeId.BARRACKS, UnitTypeId.CHANGELINGMARINE: UnitTypeId.CHANGELING, UnitTypeId.CHANGELINGMARINESHIELD: UnitTypeId.CHANGELING, UnitTypeId.CHANGELINGZEALOT: UnitTypeId.CHANGELING, UnitTypeId.CHANGELINGZERGLING: UnitTypeId.CHANGELING, UnitTypeId.CHANGELINGZERGLINGWINGS: UnitTypeId.CHANGELING, UnitTypeId.COMMANDCENTERFLYING: UnitTypeId.COMMANDCENTER, UnitTypeId.CREEPTUMORBURROWED: UnitTypeId.CREEPTUMOR, UnitTypeId.CREEPTUMORQUEEN: UnitTypeId.CREEPTUMOR, UnitTypeId.DRONEBURROWED: UnitTypeId.DRONE, UnitTypeId.FACTORYFLYING: UnitTypeId.FACTORY, UnitTypeId.GHOSTNOVA: UnitTypeId.GHOST, UnitTypeId.HERCPLACEMENT: UnitTypeId.HERC, UnitTypeId.HYDRALISKBURROWED: UnitTypeId.HYDRALISK, UnitTypeId.INFESTORBURROWED: UnitTypeId.INFESTOR, UnitTypeId.INFESTORTERRANBURROWED: UnitTypeId.INFESTORTERRAN, UnitTypeId.LIBERATORAG: UnitTypeId.LIBERATOR, UnitTypeId.LOCUSTMPFLYING: UnitTypeId.LOCUSTMP, UnitTypeId.LURKERMPBURROWED: UnitTypeId.LURKERMP, UnitTypeId.OBSERVERSIEGEMODE: UnitTypeId.OBSERVER, UnitTypeId.ORBITALCOMMANDFLYING: UnitTypeId.ORBITALCOMMAND, UnitTypeId.OVERSEERSIEGEMODE: UnitTypeId.OVERSEER, UnitTypeId.PYLONOVERCHARGED: UnitTypeId.PYLON, UnitTypeId.QUEENBURROWED: UnitTypeId.QUEEN, UnitTypeId.RAVAGERBURROWED: UnitTypeId.RAVAGER, UnitTypeId.ROACHBURROWED: UnitTypeId.ROACH, UnitTypeId.SIEGETANKSIEGED: UnitTypeId.SIEGETANK, UnitTypeId.SPINECRAWLERUPROOTED: UnitTypeId.SPINECRAWLER, UnitTypeId.SPORECRAWLERUPROOTED: UnitTypeId.SPORECRAWLER, UnitTypeId.STARPORTFLYING: UnitTypeId.STARPORT, UnitTypeId.SUPPLYDEPOTLOWERED: UnitTypeId.SUPPLYDEPOT, UnitTypeId.SWARMHOSTBURROWEDMP: UnitTypeId.SWARMHOSTMP, UnitTypeId.THORAP: UnitTypeId.THOR, UnitTypeId.ULTRALISKBURROWED: UnitTypeId.ULTRALISK, UnitTypeId.VIKINGASSAULT: UnitTypeId.VIKINGFIGHTER, UnitTypeId.WARPPRISMPHASING: UnitTypeId.WARPPRISM, UnitTypeId.WIDOWMINEBURROWED: UnitTypeId.WIDOWMINE, UnitTypeId.ZERGLINGBURROWED: UnitTypeId.ZERGLING, } ================================================ FILE: sc2/dicts/upgrade_researched_from.py ================================================ # THIS FILE WAS AUTOMATICALLY GENERATED BY "generate_dicts_from_data_json.py" DO NOT CHANGE MANUALLY! # ANY CHANGE WILL BE OVERWRITTEN from sc2.ids.unit_typeid import UnitTypeId from sc2.ids.upgrade_id import UpgradeId # from sc2.ids.buff_id import BuffId # from sc2.ids.effect_id import EffectId UPGRADE_RESEARCHED_FROM: dict[UpgradeId, UnitTypeId] = { UpgradeId.ADEPTPIERCINGATTACK: UnitTypeId.TWILIGHTCOUNCIL, UpgradeId.ANABOLICSYNTHESIS: UnitTypeId.ULTRALISKCAVERN, UpgradeId.BANSHEECLOAK: UnitTypeId.STARPORTTECHLAB, UpgradeId.BANSHEESPEED: UnitTypeId.STARPORTTECHLAB, UpgradeId.BATTLECRUISERENABLESPECIALIZATIONS: UnitTypeId.FUSIONCORE, UpgradeId.BLINKTECH: UnitTypeId.TWILIGHTCOUNCIL, UpgradeId.BURROW: UnitTypeId.HATCHERY, UpgradeId.CENTRIFICALHOOKS: UnitTypeId.BANELINGNEST, UpgradeId.CHARGE: UnitTypeId.TWILIGHTCOUNCIL, UpgradeId.CHITINOUSPLATING: UnitTypeId.ULTRALISKCAVERN, UpgradeId.CYCLONELOCKONDAMAGEUPGRADE: UnitTypeId.FACTORYTECHLAB, UpgradeId.DARKTEMPLARBLINKUPGRADE: UnitTypeId.DARKSHRINE, UpgradeId.DIGGINGCLAWS: UnitTypeId.LURKERDENMP, UpgradeId.DRILLCLAWS: UnitTypeId.FACTORYTECHLAB, UpgradeId.EVOLVEGROOVEDSPINES: UnitTypeId.HYDRALISKDEN, UpgradeId.EVOLVEMUSCULARAUGMENTS: UnitTypeId.HYDRALISKDEN, UpgradeId.EXTENDEDTHERMALLANCE: UnitTypeId.ROBOTICSBAY, UpgradeId.FRENZY: UnitTypeId.HYDRALISKDEN, UpgradeId.GLIALRECONSTITUTION: UnitTypeId.ROACHWARREN, UpgradeId.GRAVITICDRIVE: UnitTypeId.ROBOTICSBAY, UpgradeId.HIGHCAPACITYBARRELS: UnitTypeId.FACTORYTECHLAB, UpgradeId.HISECAUTOTRACKING: UnitTypeId.ENGINEERINGBAY, UpgradeId.INTERFERENCEMATRIX: UnitTypeId.STARPORTTECHLAB, UpgradeId.LIBERATORAGRANGEUPGRADE: UnitTypeId.FUSIONCORE, UpgradeId.LURKERRANGE: UnitTypeId.LURKERDENMP, UpgradeId.MEDIVACCADUCEUSREACTOR: UnitTypeId.FUSIONCORE, UpgradeId.NEURALPARASITE: UnitTypeId.INFESTATIONPIT, UpgradeId.OBSERVERGRAVITICBOOSTER: UnitTypeId.ROBOTICSBAY, UpgradeId.OVERLORDSPEED: UnitTypeId.HATCHERY, UpgradeId.PERSONALCLOAKING: UnitTypeId.GHOSTACADEMY, UpgradeId.PHOENIXRANGEUPGRADE: UnitTypeId.FLEETBEACON, UpgradeId.PROTOSSAIRARMORSLEVEL1: UnitTypeId.CYBERNETICSCORE, UpgradeId.PROTOSSAIRARMORSLEVEL2: UnitTypeId.CYBERNETICSCORE, UpgradeId.PROTOSSAIRARMORSLEVEL3: UnitTypeId.CYBERNETICSCORE, UpgradeId.PROTOSSAIRWEAPONSLEVEL1: UnitTypeId.CYBERNETICSCORE, UpgradeId.PROTOSSAIRWEAPONSLEVEL2: UnitTypeId.CYBERNETICSCORE, UpgradeId.PROTOSSAIRWEAPONSLEVEL3: UnitTypeId.CYBERNETICSCORE, UpgradeId.PROTOSSGROUNDARMORSLEVEL1: UnitTypeId.FORGE, UpgradeId.PROTOSSGROUNDARMORSLEVEL2: UnitTypeId.FORGE, UpgradeId.PROTOSSGROUNDARMORSLEVEL3: UnitTypeId.FORGE, UpgradeId.PROTOSSGROUNDWEAPONSLEVEL1: UnitTypeId.FORGE, UpgradeId.PROTOSSGROUNDWEAPONSLEVEL2: UnitTypeId.FORGE, UpgradeId.PROTOSSGROUNDWEAPONSLEVEL3: UnitTypeId.FORGE, UpgradeId.PROTOSSSHIELDSLEVEL1: UnitTypeId.FORGE, UpgradeId.PROTOSSSHIELDSLEVEL2: UnitTypeId.FORGE, UpgradeId.PROTOSSSHIELDSLEVEL3: UnitTypeId.FORGE, UpgradeId.PSISTORMTECH: UnitTypeId.TEMPLARARCHIVE, UpgradeId.PUNISHERGRENADES: UnitTypeId.BARRACKSTECHLAB, UpgradeId.SHIELDWALL: UnitTypeId.BARRACKSTECHLAB, UpgradeId.SMARTSERVOS: UnitTypeId.FACTORYTECHLAB, UpgradeId.STIMPACK: UnitTypeId.BARRACKSTECHLAB, UpgradeId.TEMPESTGROUNDATTACKUPGRADE: UnitTypeId.FLEETBEACON, UpgradeId.TERRANBUILDINGARMOR: UnitTypeId.ENGINEERINGBAY, UpgradeId.TERRANINFANTRYARMORSLEVEL1: UnitTypeId.ENGINEERINGBAY, UpgradeId.TERRANINFANTRYARMORSLEVEL2: UnitTypeId.ENGINEERINGBAY, UpgradeId.TERRANINFANTRYARMORSLEVEL3: UnitTypeId.ENGINEERINGBAY, UpgradeId.TERRANINFANTRYWEAPONSLEVEL1: UnitTypeId.ENGINEERINGBAY, UpgradeId.TERRANINFANTRYWEAPONSLEVEL2: UnitTypeId.ENGINEERINGBAY, UpgradeId.TERRANINFANTRYWEAPONSLEVEL3: UnitTypeId.ENGINEERINGBAY, UpgradeId.TERRANSHIPWEAPONSLEVEL1: UnitTypeId.ARMORY, UpgradeId.TERRANSHIPWEAPONSLEVEL2: UnitTypeId.ARMORY, UpgradeId.TERRANSHIPWEAPONSLEVEL3: UnitTypeId.ARMORY, UpgradeId.TERRANVEHICLEANDSHIPARMORSLEVEL1: UnitTypeId.ARMORY, UpgradeId.TERRANVEHICLEANDSHIPARMORSLEVEL2: UnitTypeId.ARMORY, UpgradeId.TERRANVEHICLEANDSHIPARMORSLEVEL3: UnitTypeId.ARMORY, UpgradeId.TERRANVEHICLEWEAPONSLEVEL1: UnitTypeId.ARMORY, UpgradeId.TERRANVEHICLEWEAPONSLEVEL2: UnitTypeId.ARMORY, UpgradeId.TERRANVEHICLEWEAPONSLEVEL3: UnitTypeId.ARMORY, UpgradeId.TUNNELINGCLAWS: UnitTypeId.ROACHWARREN, UpgradeId.VOIDRAYSPEEDUPGRADE: UnitTypeId.FLEETBEACON, UpgradeId.WARPGATERESEARCH: UnitTypeId.CYBERNETICSCORE, UpgradeId.ZERGFLYERARMORSLEVEL1: UnitTypeId.SPIRE, UpgradeId.ZERGFLYERARMORSLEVEL2: UnitTypeId.SPIRE, UpgradeId.ZERGFLYERARMORSLEVEL3: UnitTypeId.SPIRE, UpgradeId.ZERGFLYERWEAPONSLEVEL1: UnitTypeId.SPIRE, UpgradeId.ZERGFLYERWEAPONSLEVEL2: UnitTypeId.SPIRE, UpgradeId.ZERGFLYERWEAPONSLEVEL3: UnitTypeId.SPIRE, UpgradeId.ZERGGROUNDARMORSLEVEL1: UnitTypeId.EVOLUTIONCHAMBER, UpgradeId.ZERGGROUNDARMORSLEVEL2: UnitTypeId.EVOLUTIONCHAMBER, UpgradeId.ZERGGROUNDARMORSLEVEL3: UnitTypeId.EVOLUTIONCHAMBER, UpgradeId.ZERGLINGATTACKSPEED: UnitTypeId.SPAWNINGPOOL, UpgradeId.ZERGLINGMOVEMENTSPEED: UnitTypeId.SPAWNINGPOOL, UpgradeId.ZERGMELEEWEAPONSLEVEL1: UnitTypeId.EVOLUTIONCHAMBER, UpgradeId.ZERGMELEEWEAPONSLEVEL2: UnitTypeId.EVOLUTIONCHAMBER, UpgradeId.ZERGMELEEWEAPONSLEVEL3: UnitTypeId.EVOLUTIONCHAMBER, UpgradeId.ZERGMISSILEWEAPONSLEVEL1: UnitTypeId.EVOLUTIONCHAMBER, UpgradeId.ZERGMISSILEWEAPONSLEVEL2: UnitTypeId.EVOLUTIONCHAMBER, UpgradeId.ZERGMISSILEWEAPONSLEVEL3: UnitTypeId.EVOLUTIONCHAMBER, } ================================================ FILE: sc2/expiring_dict.py ================================================ from __future__ import annotations from collections import OrderedDict from collections.abc import Hashable, Iterable from threading import RLock from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from sc2.bot_ai import BotAI class ExpiringDict(OrderedDict[Hashable, Any]): """ An expiring dict that uses the bot.state.game_loop to only return items that are valid for a specific amount of time. Example usages:: async def on_step(iteration: int): # This dict will hold up to 10 items and only return values that have been added up to 20 frames ago my_dict = ExpiringDict(self, max_age_frames=20) if iteration == 0: # Add item my_dict["test"] = "something" if iteration == 2: # On default, one iteration is called every 8 frames if "test" in my_dict: print("test is in dict") if iteration == 20: if "test" not in my_dict: print("test is not anymore in dict") """ def __init__(self, bot: BotAI, max_age_frames: int = 1) -> None: assert max_age_frames >= -1 assert bot OrderedDict.__init__(self) self.bot: BotAI = bot self.max_age: int | float = max_age_frames self.lock: RLock = RLock() @property def frame(self) -> int: return self.bot.state.game_loop def __contains__(self, key: Hashable) -> bool: """Return True if dict has key, else False, e.g. 'key in dict'""" with self.lock: if OrderedDict.__contains__(self, key): # Each item is a list of [value, frame time] item = OrderedDict.__getitem__(self, key) if self.frame - item[1] < self.max_age: return True del self[key] return False def __getitem__(self, key: Hashable, with_age: bool = False) -> Any: """Return the item of the dict using d[key]""" with self.lock: # Each item is a list of [value, frame time] item = OrderedDict.__getitem__(self, key) if self.frame - item[1] < self.max_age: if with_age: return item[0], item[1] return item[0] OrderedDict.__delitem__(self, key) raise KeyError(key) def __setitem__(self, key: Hashable, value: Any) -> None: """Set d[key] = value""" with self.lock: OrderedDict.__setitem__(self, key, (value, self.frame)) def __repr__(self) -> str: """Printable version of the dict instead of getting memory adress""" print_list: list[str] = [] with self.lock: for key, value in OrderedDict.items(self): if self.frame - value[1] < self.max_age: print_list.append(f"{repr(key)}: {repr(value)}") print_str = ", ".join(print_list) return f"ExpiringDict({print_str})" def __str__(self) -> str: return self.__repr__() def __iter__(self) -> Iterable[Hashable]: """Override 'for key in dict:'""" with self.lock: return self.keys() # TODO find a way to improve len def __len__(self) -> int: """Override len method as key value pairs aren't instantly being deleted, but only on __get__(item). This function is slow because it has to check if each element is not expired yet.""" with self.lock: count = 0 for _ in self.values(): count += 1 return count def pop(self, key: Hashable, default: Any = None, with_age: bool = False): """Return the item and remove it""" with self.lock: if OrderedDict.__contains__(self, key): item = OrderedDict.__getitem__(self, key) if self.frame - item[1] < self.max_age: del self[key] if with_age: return item[0], item[1] return item[0] del self[key] if default is None: raise KeyError(key) if with_age: return default, self.frame return default def get(self, key: Hashable, default: Any = None, with_age: bool = False): """Return the value for key if key is in dict, else default""" with self.lock: if OrderedDict.__contains__(self, key): item = OrderedDict.__getitem__(self, key) if self.frame - item[1] < self.max_age: if with_age: return item[0], item[1] return item[0] if default is None: raise KeyError(key) if with_age: return default, self.frame return None return None def update(self, other_dict: dict[Hashable, Any]) -> None: with self.lock: for key, value in other_dict.items(): self[key] = value def items(self) -> Iterable[tuple[Hashable, Any]]: """Return iterator of zipped list [keys, values]""" with self.lock: for key, value in OrderedDict.items(self): if self.frame - value[1] < self.max_age: yield key, value[0] def keys(self) -> Iterable[Hashable]: """Return iterator of keys""" with self.lock: for key, value in OrderedDict.items(self): if self.frame - value[1] < self.max_age: yield key def values(self) -> Iterable[Any]: """Return iterator of values""" with self.lock: for value in OrderedDict.values(self): if self.frame - value[1] < self.max_age: yield value[0] ================================================ FILE: sc2/game_data.py ================================================ from __future__ import annotations from bisect import bisect_left from contextlib import suppress from dataclasses import dataclass from functools import lru_cache from s2clientprotocol import data_pb2, sc2api_pb2 from sc2.data import Attribute, Race from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId from sc2.unit_command import UnitCommand with suppress(ImportError): from sc2.dicts.unit_trained_from import UNIT_TRAINED_FROM # Set of parts of names of abilities that have no cost # E.g every ability that has 'Hold' in its name is free FREE_ABILITIES = {"Lower", "Raise", "Land", "Lift", "Hold", "Harvest"} class GameData: def __init__(self, data: sc2api_pb2.ResponseData) -> None: """ :param data: """ ids = {a.value for a in AbilityId if a.value != 0} self.abilities: dict[int, AbilityData] = { a.ability_id: AbilityData(self, a) for a in data.abilities if a.ability_id in ids } self.units: dict[int, UnitTypeData] = {u.unit_id: UnitTypeData(self, u) for u in data.units if u.available} self.upgrades: dict[int, UpgradeData] = {u.upgrade_id: UpgradeData(self, u) for u in data.upgrades} # Cached UnitTypeIds so that conversion does not take long. This needs to be moved elsewhere if a new GameData object is created multiple times per game @lru_cache(maxsize=256) def calculate_ability_cost(self, ability: AbilityData | AbilityId | UnitCommand) -> Cost: if isinstance(ability, AbilityId): ability = self.abilities[ability.value] elif isinstance(ability, UnitCommand): ability = self.abilities[ability.ability.value] assert isinstance(ability, AbilityData), f"Ability is not of type 'AbilityData', but was {type(ability)}" for unit in self.units.values(): if unit.creation_ability is None: continue if not AbilityData.id_exists(unit.creation_ability.id.value): continue if unit.creation_ability.is_free_morph: continue if unit.creation_ability == ability: if unit.id == UnitTypeId.ZERGLING: # HARD CODED: zerglings are generated in pairs return Cost(unit.cost.minerals * 2, unit.cost.vespene * 2, unit.cost.time) if unit.id == UnitTypeId.BANELING: # HARD CODED: banelings don't cost 50/25 as described in the API, but 25/25 return Cost(25, 25, unit.cost.time) # Correction for morphing units, e.g. orbital would return 550/0 instead of actual 150/0 morph_cost = unit.morph_cost if morph_cost: # can be None return morph_cost # Correction for zerg structures without morph: Extractor would return 75 instead of actual 25 return unit.cost_zerg_corrected for upgrade in self.upgrades.values(): if upgrade.research_ability == ability: return upgrade.cost return Cost(0, 0) class AbilityData: ability_ids: list[int] = [ability_id.value for ability_id in AbilityId][1:] # sorted list @classmethod def id_exists(cls, ability_id: int) -> bool: assert isinstance(ability_id, int), f"Wrong type: {ability_id} is not int" if ability_id == 0: return False i = bisect_left(cls.ability_ids, ability_id) # quick binary search return i != len(cls.ability_ids) and cls.ability_ids[i] == ability_id def __init__(self, game_data: GameData, proto: data_pb2.AbilityData) -> None: self._game_data = game_data self._proto = proto # What happens if we comment this out? Should this not be commented out? What is its purpose? assert self.id != 0 def __repr__(self) -> str: return f"AbilityData(name={self._proto.button_name})" @property def id(self) -> AbilityId: """Returns the generic remap ID. See sc2/dicts/generic_redirect_abilities.py""" if self._proto.remaps_to_ability_id: return AbilityId(self._proto.remaps_to_ability_id) return AbilityId(self._proto.ability_id) @property def exact_id(self) -> AbilityId: """Returns the exact ID of the ability""" return AbilityId(self._proto.ability_id) @property def link_name(self) -> str: """For Stimpack this returns 'BarracksTechLabResearch'""" return self._proto.link_name @property def button_name(self) -> str: """For Stimpack this returns 'Stimpack'""" return self._proto.button_name @property def friendly_name(self) -> str: """For Stimpack this returns 'Research Stimpack'""" return self._proto.friendly_name @property def is_free_morph(self) -> bool: return any(free in self._proto.link_name for free in FREE_ABILITIES) @property def cost(self) -> Cost: return self._game_data.calculate_ability_cost(self.id) class UnitTypeData: def __init__(self, game_data: GameData, proto: data_pb2.UnitTypeData) -> None: """ :param game_data: :param proto: """ # The ability_id for lurkers is # LURKERASPECTMPFROMHYDRALISKBURROWED_LURKERMPFROMHYDRALISKBURROWED # instead of the correct MORPH_LURKER. if proto.unit_id == UnitTypeId.LURKERMP.value: proto.ability_id = AbilityId.MORPH_LURKER.value self._game_data = game_data self._proto = proto def __repr__(self) -> str: return f"UnitTypeData(name={self.name})" @property def id(self) -> UnitTypeId: return UnitTypeId(self._proto.unit_id) @property def name(self) -> str: return self._proto.name @property def creation_ability(self) -> AbilityData | None: if self._proto.ability_id == 0: return None if self._proto.ability_id not in self._game_data.abilities: return None return self._game_data.abilities[self._proto.ability_id] @property def footprint_radius(self) -> float | None: """See unit.py footprint_radius""" if self.creation_ability is None: return None return self.creation_ability._proto.footprint_radius @property def attributes(self) -> list[Attribute]: return [Attribute(i) for i in self._proto.attributes] def has_attribute(self, attr: Attribute) -> bool: assert isinstance(attr, Attribute) return attr in self.attributes @property def has_minerals(self) -> bool: return self._proto.has_minerals @property def has_vespene(self) -> bool: return self._proto.has_vespene @property def cargo_size(self) -> int: """How much cargo this unit uses up in cargo_space""" return self._proto.cargo_size @property def tech_requirement(self) -> UnitTypeId | None: """Tech-building requirement of buildings - may work for units but unreliably""" if self._proto.tech_requirement == 0: return None if self._proto.tech_requirement not in self._game_data.units: return None return UnitTypeId(self._proto.tech_requirement) @property def tech_alias(self) -> list[UnitTypeId] | None: """Building tech equality, e.g. OrbitalCommand is the same as CommandCenter Building tech equality, e.g. Hive is the same as Lair and Hatchery For Hive, this returns [UnitTypeId.Hatchery, UnitTypeId.Lair] For SCV, this returns None""" return_list = [ UnitTypeId(tech_alias) for tech_alias in self._proto.tech_alias if tech_alias in self._game_data.units ] return return_list if return_list else None @property def unit_alias(self) -> UnitTypeId | None: """Building type equality, e.g. FlyingOrbitalCommand is the same as OrbitalCommand""" if self._proto.unit_alias == 0: return None if self._proto.unit_alias not in self._game_data.units: return None """ For flying OrbitalCommand, this returns UnitTypeId.OrbitalCommand """ return UnitTypeId(self._proto.unit_alias) @property def race(self) -> Race: return Race(self._proto.race) @property def cost(self) -> Cost: return Cost(self._proto.mineral_cost, self._proto.vespene_cost, self._proto.build_time) @property def cost_zerg_corrected(self) -> Cost: """This returns 25 for extractor and 200 for spawning pool instead of 75 and 250 respectively""" if self.race == Race.Zerg and Attribute.Structure.value in self._proto.attributes: return Cost(self._proto.mineral_cost - 50, self._proto.vespene_cost, self._proto.build_time) return self.cost @property def morph_cost(self) -> Cost | None: """This returns 150 minerals for OrbitalCommand instead of 550""" # Morphing units supply_cost = self._proto.food_required if supply_cost > 0 and self.id in UNIT_TRAINED_FROM and len(UNIT_TRAINED_FROM[self.id]) == 1: producer: UnitTypeId for producer in UNIT_TRAINED_FROM[self.id]: producer_unit_data = self._game_data.units[producer.value] if 0 < producer_unit_data._proto.food_required <= supply_cost: if producer == UnitTypeId.ZERGLING: producer_cost = Cost(25, 0) else: producer_cost = self._game_data.calculate_ability_cost(producer_unit_data.creation_ability) return Cost( self._proto.mineral_cost - producer_cost.minerals, self._proto.vespene_cost - producer_cost.vespene, self._proto.build_time, ) # Fix for BARRACKSREACTOR which has tech alias [REACTOR] which has (0, 0) cost if self.tech_alias is None or self.tech_alias[0] in {UnitTypeId.TECHLAB, UnitTypeId.REACTOR}: return None # Morphing a HIVE would have HATCHERY and LAIR in the tech alias - now subtract HIVE cost from LAIR cost instead of from HATCHERY cost tech_alias_cost_minerals = max( self._game_data.units[tech_alias.value].cost.minerals for tech_alias in self.tech_alias ) tech_alias_cost_vespene = max( self._game_data.units[tech_alias.value].cost.vespene for tech_alias in self.tech_alias ) return Cost( self._proto.mineral_cost - tech_alias_cost_minerals, self._proto.vespene_cost - tech_alias_cost_vespene, self._proto.build_time, ) class UpgradeData: def __init__(self, game_data: GameData, proto: data_pb2.UpgradeData) -> None: """ :param game_data: :param proto: """ self._game_data = game_data self._proto = proto def __repr__(self) -> str: return f"UpgradeData({self.name} - research ability: {self.research_ability}, {self.cost})" @property def name(self) -> str: return self._proto.name @property def research_ability(self) -> AbilityData | None: if self._proto.ability_id == 0: return None if self._proto.ability_id not in self._game_data.abilities: return None return self._game_data.abilities[self._proto.ability_id] @property def cost(self) -> Cost: return Cost(self._proto.mineral_cost, self._proto.vespene_cost, self._proto.research_time) @dataclass class Cost: """ The cost of an action, a structure, a unit or a research upgrade. The time is given in frames (22.4 frames per game second). """ minerals: int vespene: int time: float | None = None def __repr__(self) -> str: return f"Cost({self.minerals}, {self.vespene})" def __eq__(self, other: Cost) -> bool: return self.minerals == other.minerals and self.vespene == other.vespene def __ne__(self, other: Cost) -> bool: return self.minerals != other.minerals or self.vespene != other.vespene def __bool__(self) -> bool: return self.minerals != 0 or self.vespene != 0 def __add__(self, other: Cost) -> Cost: if not other: return self if not self: return other time = (self.time or 0) + (other.time or 0) return Cost(self.minerals + other.minerals, self.vespene + other.vespene, time=time) def __sub__(self, other: Cost) -> Cost: time = (self.time or 0) + (other.time or 0) return Cost(self.minerals - other.minerals, self.vespene - other.vespene, time=time) def __mul__(self, other: int) -> Cost: return Cost(self.minerals * other, self.vespene * other, time=self.time) def __rmul__(self, other: int) -> Cost: return Cost(self.minerals * other, self.vespene * other, time=self.time) ================================================ FILE: sc2/game_info.py ================================================ from __future__ import annotations import heapq from collections import deque from collections.abc import Iterable from dataclasses import dataclass from functools import cached_property import numpy as np from s2clientprotocol import sc2api_pb2 from sc2.pixel_map import PixelMap from sc2.player import Player from sc2.position import Point2, Rect, Size @dataclass class Ramp: points: frozenset[Point2] game_info: GameInfo @property def x_offset(self) -> float: # Tested by printing actual building locations vs calculated depot positions return 0.5 @property def y_offset(self) -> float: # Tested by printing actual building locations vs calculated depot positions return 0.5 @cached_property def _height_map(self) -> PixelMap: return self.game_info.terrain_height @cached_property def size(self) -> int: return len(self.points) def height_at(self, p: Point2) -> int: return self._height_map[p] @cached_property def upper(self) -> frozenset[Point2]: """Returns the upper points of a ramp.""" current_max = -10000 result = set() for p in self.points: height = self.height_at(p) if height > current_max: current_max = height result = {p} elif height == current_max: result.add(p) return frozenset(result) @cached_property def upper2_for_ramp_wall(self) -> frozenset[Point2]: """Returns the 2 upper ramp points of the main base ramp required for the supply depot and barracks placement properties used in this file.""" # From bottom center, find 2 points that are furthest away (within the same ramp) return frozenset(heapq.nlargest(2, self.upper, key=lambda x: x.distance_to_point2(self.bottom_center))) @cached_property def top_center(self) -> Point2: length = len(self.upper) pos = Point2((sum(p.x for p in self.upper) / length, sum(p.y for p in self.upper) / length)) return pos @cached_property def lower(self) -> frozenset[Point2]: current_min = 10000 result = set() for p in self.points: height = self.height_at(p) if height < current_min: current_min = height result = {p} elif height == current_min: result.add(p) return frozenset(result) @cached_property def bottom_center(self) -> Point2: length = len(self.lower) pos = Point2((sum(p.x for p in self.lower) / length, sum(p.y for p in self.lower) / length)) return pos @cached_property def barracks_in_middle(self) -> Point2 | None: """Barracks position in the middle of the 2 depots""" if len(self.upper) not in {2, 5}: return None if len(self.upper2_for_ramp_wall) == 2: points = set(self.upper2_for_ramp_wall) p1 = points.pop().offset((self.x_offset, self.y_offset)) p2 = points.pop().offset((self.x_offset, self.y_offset)) # Offset from top point to barracks center is (2, 1) intersects = p1.circle_intersection(p2, 5**0.5) any_lower_point = next(iter(self.lower)) return max(intersects, key=lambda p: p.distance_to_point2(any_lower_point)) raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.") @cached_property def depot_in_middle(self) -> Point2 | None: """Depot in the middle of the 3 depots""" if len(self.upper) not in {2, 5}: return None if len(self.upper2_for_ramp_wall) == 2: points = set(self.upper2_for_ramp_wall) p1 = points.pop().offset((self.x_offset, self.y_offset)) p2 = points.pop().offset((self.x_offset, self.y_offset)) # Offset from top point to depot center is (1.5, 0.5) try: intersects = p1.circle_intersection(p2, 2.5**0.5) except AssertionError: # Returns None when no placement was found, this is the case on the map Honorgrounds LE with an exceptionally large main base ramp return None any_lower_point = next(iter(self.lower)) return max(intersects, key=lambda p: p.distance_to_point2(any_lower_point)) raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.") @cached_property def corner_depots(self) -> set[Point2]: """Finds the 2 depot positions on the outside""" if not self.upper2_for_ramp_wall: return set() if len(self.upper2_for_ramp_wall) == 2: points = set(self.upper2_for_ramp_wall) p1 = points.pop().offset((self.x_offset, self.y_offset)) p2 = points.pop().offset((self.x_offset, self.y_offset)) center = p1.towards(p2, p1.distance_to_point2(p2) / 2) depot_position = self.depot_in_middle if depot_position is None: return set() # Offset from middle depot to corner depots is (2, 1) intersects = center.circle_intersection(depot_position, 5**0.5) return intersects raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.") @cached_property def barracks_can_fit_addon(self) -> bool: """Test if a barracks can fit an addon at natural ramp""" # https://i.imgur.com/4b2cXHZ.png if len(self.upper2_for_ramp_wall) == 2: # pyrefly: ignore return self.barracks_in_middle.x + 1 > max(self.corner_depots, key=lambda depot: depot.x).x raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.") @cached_property def barracks_correct_placement(self) -> Point2 | None: """Corrected placement so that an addon can fit""" if self.barracks_in_middle is None: return None if len(self.upper2_for_ramp_wall) == 2: if self.barracks_can_fit_addon: return self.barracks_in_middle return self.barracks_in_middle.offset((-2, 0)) raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.") @cached_property def protoss_wall_pylon(self) -> Point2 | None: """ Pylon position that powers the two wall buildings and the warpin position. """ if len(self.upper) not in {2, 5}: return None if len(self.upper2_for_ramp_wall) != 2: raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.") middle = self.depot_in_middle # direction up the ramp # pyrefly: ignore direction = self.barracks_in_middle.negative_offset(middle) return middle + 6 * direction @cached_property def protoss_wall_buildings(self) -> frozenset[Point2]: """ List of two positions for 3x3 buildings that form a wall with a spot for a one unit block. These buildings can be powered by a pylon on the protoss_wall_pylon position. """ if len(self.upper) not in {2, 5}: return frozenset() if len(self.upper2_for_ramp_wall) == 2: middle = self.depot_in_middle # direction up the ramp # pyrefly: ignore direction = self.barracks_in_middle.negative_offset(middle) # sort depots based on distance to start to get wallin orientation sorted_depots = sorted( self.corner_depots, key=lambda depot: depot.distance_to(self.game_info.player_start_location) ) wall1: Point2 = sorted_depots[1].offset(direction) # pyrefly: ignore wall2 = middle + direction + (middle - wall1) / 1.5 return frozenset([wall1, wall2]) raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.") @cached_property def protoss_wall_warpin(self) -> Point2 | None: """ Position for a unit to block the wall created by protoss_wall_buildings. Powered by protoss_wall_pylon. """ if len(self.upper) not in {2, 5}: return None if len(self.upper2_for_ramp_wall) != 2: raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.") middle = self.depot_in_middle # direction up the ramp # pyrefly: ignore direction = self.barracks_in_middle.negative_offset(middle) # sort depots based on distance to start to get wallin orientation sorted_depots = sorted(self.corner_depots, key=lambda x: x.distance_to(self.game_info.player_start_location)) return sorted_depots[0].negative_offset(direction) class GameInfo: def __init__(self, proto: sc2api_pb2.ResponseGameInfo) -> None: self._proto = proto self.players: list[Player] = [Player.from_proto(p) for p in self._proto.player_info] self.map_name: str = self._proto.map_name self.local_map_path: str = self._proto.local_map_path self.map_size: Size = Size.from_proto(self._proto.start_raw.map_size) # self.pathing_grid[point]: if 0, point is not pathable, if 1, point is pathable self.pathing_grid: PixelMap = PixelMap(self._proto.start_raw.pathing_grid, in_bits=True) # self.terrain_height[point]: returns the height in range of 0 to 255 at that point self.terrain_height: PixelMap = PixelMap(self._proto.start_raw.terrain_height) # self.placement_grid[point]: if 0, point is not placeable, if 1, point is pathable self.placement_grid: PixelMap = PixelMap(self._proto.start_raw.placement_grid, in_bits=True) self.playable_area = Rect.from_proto(self._proto.start_raw.playable_area) self.map_center = self.playable_area.center # pyrefly: ignore self.map_ramps: list[Ramp] = None # Filled later by BotAI._prepare_first_step # pyrefly: ignore self.vision_blockers: frozenset[Point2] = None # Filled later by BotAI._prepare_first_step self.player_races: dict[int, int] = { p.player_id: p.race_actual or p.race_requested for p in self._proto.player_info } self.start_locations: list[Point2] = [ Point2.from_proto(sl).round(decimals=1) for sl in self._proto.start_raw.start_locations ] # pyrefly: ignore self.player_start_location: Point2 = None # Filled later by BotAI._prepare_first_step def _find_ramps_and_vision_blockers(self) -> tuple[list[Ramp], frozenset[Point2]]: """Calculate points that are pathable but not placeable. Then divide them into ramp points if not all points around the points are equal height and into vision blockers if they are.""" def equal_height_around(tile): # mask to slice array 1 around tile sliced = self.terrain_height.data_numpy[tile[1] - 1 : tile[1] + 2, tile[0] - 1 : tile[0] + 2] return len(np.unique(sliced)) == 1 map_area = self.playable_area # all points in the playable area that are pathable but not placable points = [ Point2((a, b)) for (b, a), value in np.ndenumerate(self.pathing_grid.data_numpy) if value == 1 and map_area.x <= a < map_area.x + map_area.width and map_area.y <= b < map_area.y + map_area.height and self.placement_grid[(a, b)] == 0 ] # divide points into ramp points and vision blockers ramp_points = [point for point in points if not equal_height_around(point)] vision_blockers = frozenset(point for point in points if equal_height_around(point)) ramps = [Ramp(frozenset(group), self) for group in self._find_groups(ramp_points)] return ramps, vision_blockers def _find_groups(self, points: Iterable[Point2], minimum_points_per_group: int = 8) -> Iterable[frozenset[Point2]]: """ From a set of points, this function will try to group points together by painting clusters of points in a rectangular map using flood fill algorithm. Returns groups of points as list, like [{p1, p2, p3}, {p4, p5, p6, p7, p8}] """ # TODO do we actually need colors here? the ramps will never touch anyways. NOT_COLORED_YET = -1 map_width = self.pathing_grid.width map_height = self.pathing_grid.height current_color: int = NOT_COLORED_YET picture: list[list[int]] = [[-2 for _ in range(map_width)] for _ in range(map_height)] def paint(pt: Point2) -> None: # pyrefly: ignore picture[pt.y][pt.x] = current_color nearby: list[tuple[int, int]] = [(a, b) for a in [-1, 0, 1] for b in [-1, 0, 1] if a != 0 or b != 0] remaining: set[Point2] = set(points) for point in remaining: paint(point) current_color = 1 queue: deque[Point2] = deque() while remaining: current_group: set[Point2] = set() if not queue: start = remaining.pop() paint(start) queue.append(start) current_group.add(start) while queue: base: Point2 = queue.popleft() for offset in nearby: px, py = base.x + offset[0], base.y + offset[1] # Do we ever reach out of map bounds? if not (0 <= px < map_width and 0 <= py < map_height): continue # pyrefly: ignore if picture[py][px] != NOT_COLORED_YET: continue point: Point2 = Point2((px, py)) remaining.discard(point) paint(point) queue.append(point) current_group.add(point) if len(current_group) >= minimum_points_per_group: yield frozenset(current_group) ================================================ FILE: sc2/game_state.py ================================================ from __future__ import annotations from dataclasses import dataclass from functools import cached_property from itertools import chain from s2clientprotocol.raw_pb2 import Effect, Unit from loguru import logger from s2clientprotocol import raw_pb2, sc2api_pb2 from sc2.constants import IS_ENEMY, IS_MINE, FakeEffectID, FakeEffectRadii from sc2.data import Alliance, DisplayType from sc2.ids.ability_id import AbilityId from sc2.ids.effect_id import EffectId from sc2.ids.upgrade_id import UpgradeId from sc2.pixel_map import PixelMap from sc2.position import Point2, Point3 from sc2.power_source import PsionicMatrix from sc2.score import ScoreDetails try: from sc2.dicts.generic_redirect_abilities import GENERIC_REDIRECT_ABILITIES except ImportError: logger.info('Unable to import "GENERIC_REDIRECT_ABILITIES"') GENERIC_REDIRECT_ABILITIES = {} class Blip: def __init__(self, proto: raw_pb2.Unit) -> None: """ :param proto: """ self._proto = proto @property def is_blip(self) -> bool: """Detected by sensor tower.""" return self._proto.is_blip @property def is_snapshot(self) -> bool: return self._proto.display_type == DisplayType.Snapshot.value @property def is_visible(self) -> bool: return self._proto.display_type == DisplayType.Visible.value @property def alliance(self) -> int: return self._proto.alliance @property def is_mine(self) -> bool: return self._proto.alliance == Alliance.Self.value @property def is_enemy(self) -> bool: return self._proto.alliance == Alliance.Enemy.value @property def position(self) -> Point2: """2d position of the blip.""" return Point2.from_proto(self._proto.pos) @property def position3d(self) -> Point3: """3d position of the blip.""" return Point3.from_proto(self._proto.pos) class Common: ATTRIBUTES = [ "player_id", "minerals", "vespene", "food_cap", "food_used", "food_army", "food_workers", "idle_worker_count", "army_count", "warp_gate_count", "larva_count", ] def __init__(self, proto: sc2api_pb2.PlayerCommon) -> None: self._proto = proto def __getattr__(self, attr) -> int: assert attr in self.ATTRIBUTES, f"'{attr}' is not a valid attribute" return int(getattr(self._proto, attr)) class EffectData: def __init__(self, proto: raw_pb2.Effect | raw_pb2.Unit, fake: bool = False) -> None: """ :param proto: :param fake: """ self._proto: Effect | Unit = proto self.fake = fake @property def id(self) -> EffectId | str: if isinstance(self._proto, raw_pb2.Unit): # Returns the string from constants.py, e.g. "KD8CHARGE" return FakeEffectID[self._proto.unit_type] return EffectId(self._proto.effect_id) @property def positions(self) -> set[Point2]: if isinstance(self._proto, raw_pb2.Unit): return {Point2.from_proto(self._proto.pos)} return {Point2.from_proto(p) for p in self._proto.pos} @property def alliance(self) -> Alliance: return Alliance(self._proto.alliance) @property def is_mine(self) -> bool: """Checks if the effect is caused by me.""" return self._proto.alliance == IS_MINE @property def is_enemy(self) -> bool: """Checks if the effect is hostile.""" return self._proto.alliance == IS_ENEMY @property def owner(self) -> int: return self._proto.owner @property def radius(self) -> float: if isinstance(self._proto, Unit): return FakeEffectRadii[self._proto.unit_type] return self._proto.radius def __repr__(self) -> str: return f"{self.id} with radius {self.radius} at {self.positions}" @dataclass class ChatMessage: player_id: int message: str @dataclass class AbilityLookupTemplateClass: ability_id: int @property def exact_id(self) -> AbilityId: return AbilityId(self.ability_id) @property def generic_id(self) -> AbilityId: """ See https://github.com/BurnySc2/python-sc2/blob/511c34f6b7ae51bd11e06ba91b6a9624dc04a0c0/sc2/dicts/generic_redirect_abilities.py#L13 """ return GENERIC_REDIRECT_ABILITIES.get(self.exact_id, self.exact_id) @dataclass class ActionRawUnitCommand(AbilityLookupTemplateClass): game_loop: int unit_tags: list[int] queue_command: bool target_world_space_pos: Point2 | None target_unit_tag: int | None = None @dataclass class ActionRawToggleAutocast(AbilityLookupTemplateClass): game_loop: int unit_tags: list[int] @dataclass class ActionRawCameraMove: center_world_space: Point2 @dataclass class ActionError(AbilityLookupTemplateClass): unit_tag: int # See here for the codes of 'result': https://github.com/Blizzard/s2client-proto/blob/01ab351e21c786648e4c6693d4aad023a176d45c/s2clientprotocol/error.proto#L6 result: int class GameState: def __init__( self, response_observation: sc2api_pb2.ResponseObservation, previous_observation: sc2api_pb2.ResponseObservation | None = None, ) -> None: """ :param response_observation: :param previous_observation: """ # Only filled in realtime=True in case the bot skips frames self.previous_observation = previous_observation self.response_observation = response_observation # https://github.com/Blizzard/s2client-proto/blob/51662231c0965eba47d5183ed0a6336d5ae6b640/s2clientprotocol/sc2api.proto#L575 self.observation = response_observation.observation self.observation_raw = self.observation.raw_data self.player_result = response_observation.player_result self.common: Common = Common(self.observation.player_common) # Area covered by Pylons and Warpprisms self.psionic_matrix: PsionicMatrix = PsionicMatrix.from_proto(list(self.observation_raw.player.power_sources)) # 22.4 per second on faster game speed self.game_loop: int = self.observation.game_loop # https://github.com/Blizzard/s2client-proto/blob/33f0ecf615aa06ca845ffe4739ef3133f37265a9/s2clientprotocol/score.proto#L31 self.score: ScoreDetails = ScoreDetails(self.observation.score) self.abilities = self.observation.abilities # abilities of selected units self.upgrades: set[UpgradeId] = {UpgradeId(upgrade) for upgrade in self.observation_raw.player.upgrade_ids} # self.visibility[point]: 0=Hidden, 1=Fogged, 2=Visible self.visibility: PixelMap = PixelMap(self.observation_raw.map_state.visibility) # self.creep[point]: 0=No creep, 1=creep self.creep: PixelMap = PixelMap(self.observation_raw.map_state.creep, in_bits=True) # Effects like ravager bile shot, lurker attack, everything in effect_id.py self.effects: set[EffectData] = {EffectData(effect) for effect in self.observation_raw.effects} """ Usage: for effect in self.state.effects: if effect.id == EffectId.RAVAGERCORROSIVEBILECP: positions = effect.positions # dodge the ravager biles """ @cached_property def dead_units(self) -> set[int]: """A set of unit tags that died this frame""" _dead_units = set(self.observation_raw.event.dead_units) if self.previous_observation: return _dead_units | set(self.previous_observation.observation.raw_data.event.dead_units) return _dead_units @cached_property def chat(self) -> list[ChatMessage]: """List of chat messages sent this frame (by either player).""" previous_frame_chat = self.previous_observation.chat if self.previous_observation else [] return [ ChatMessage(message.player_id, message.message) for message in chain(previous_frame_chat, self.response_observation.chat) ] @cached_property def alerts(self) -> list[int]: """ Game alerts, see https://github.com/Blizzard/s2client-proto/blob/01ab351e21c786648e4c6693d4aad023a176d45c/s2clientprotocol/sc2api.proto#L683-L706 """ if self.previous_observation is not None: return list(chain(self.previous_observation.observation.alerts, self.observation.alerts)) return list(self.observation.alerts) @cached_property def actions(self) -> list[ActionRawUnitCommand | ActionRawToggleAutocast | ActionRawCameraMove]: """ List of successful actions since last frame. See https://github.com/Blizzard/s2client-proto/blob/01ab351e21c786648e4c6693d4aad023a176d45c/s2clientprotocol/sc2api.proto#L630-L637 Each action is converted into Python dataclasses: ActionRawUnitCommand, ActionRawToggleAutocast, ActionRawCameraMove """ previous_frame_actions = self.previous_observation.actions if self.previous_observation else [] actions: list[ActionRawUnitCommand | ActionRawToggleAutocast | ActionRawCameraMove] = [] for action in chain(previous_frame_actions, self.response_observation.actions): action_raw = action.action_raw game_loop = action.game_loop if action_raw.HasField("unit_command"): # Unit commands raw_unit_command = action_raw.unit_command if raw_unit_command.HasField("target_world_space_pos"): # Actions that have a point as target actions.append( ActionRawUnitCommand( game_loop, raw_unit_command.ability_id, list(raw_unit_command.unit_tags), raw_unit_command.queue_command, Point2.from_proto(raw_unit_command.target_world_space_pos), ) ) else: # Actions that have a unit as target actions.append( ActionRawUnitCommand( game_loop, raw_unit_command.ability_id, list(raw_unit_command.unit_tags), raw_unit_command.queue_command, None, raw_unit_command.target_unit_tag, ) ) elif action_raw.HasField("toggle_autocast"): # Toggle autocast actions raw_toggle_autocast_action = action_raw.toggle_autocast actions.append( ActionRawToggleAutocast( game_loop, raw_toggle_autocast_action.ability_id, list(raw_toggle_autocast_action.unit_tags), ) ) else: # Camera move actions actions.append(ActionRawCameraMove(Point2.from_proto(action.action_raw.camera_move.center_world_space))) return actions @cached_property def actions_unit_commands(self) -> list[ActionRawUnitCommand]: """ List of successful unit actions since last frame. See https://github.com/Blizzard/s2client-proto/blob/01ab351e21c786648e4c6693d4aad023a176d45c/s2clientprotocol/raw.proto#L185-L193 """ return list(filter(lambda action: isinstance(action, ActionRawUnitCommand), self.actions)) @cached_property def actions_toggle_autocast(self) -> list[ActionRawToggleAutocast]: """ List of successful autocast toggle actions since last frame. See https://github.com/Blizzard/s2client-proto/blob/01ab351e21c786648e4c6693d4aad023a176d45c/s2clientprotocol/raw.proto#L199-L202 """ return list(filter(lambda action: isinstance(action, ActionRawToggleAutocast), self.actions)) @cached_property def action_errors(self) -> list[ActionError]: """ List of erroneous actions since last frame. See https://github.com/Blizzard/s2client-proto/blob/01ab351e21c786648e4c6693d4aad023a176d45c/s2clientprotocol/sc2api.proto#L648-L652 """ previous_frame_errors = self.previous_observation.action_errors if self.previous_observation else [] return [ ActionError(error.ability_id, error.unit_tag, error.result) for error in chain(self.response_observation.action_errors, previous_frame_errors) ] ================================================ FILE: sc2/generate_ids.py ================================================ from __future__ import annotations import importlib import json import platform import sys from pathlib import Path from typing import Any from loguru import logger from sc2.game_data import GameData try: from sc2.ids.id_version import ID_VERSION_STRING except ImportError: ID_VERSION_STRING = "4.11.4.78285" class IdGenerator: def __init__( self, game_data: GameData | None = None, game_version: str | None = None, verbose: bool = False ) -> None: self.game_data = game_data self.game_version = game_version self.verbose = verbose self.HEADER = f""" from __future__ import annotations # DO NOT EDIT! # This file was automatically generated by "{Path(__file__).name}" """ self.PF = platform.system() self.HOME_DIR = str(Path.home()) self.DATA_JSON = { "Darwin": self.HOME_DIR + "/Library/Application Support/Blizzard/StarCraft II/stableid.json", "Windows": self.HOME_DIR + "/Documents/StarCraft II/stableid.json", "Linux": self.HOME_DIR + "/Documents/StarCraft II/stableid.json", } self.ENUM_TRANSLATE = { "Units": "UnitTypeId", "Abilities": "AbilityId", "Upgrades": "UpgradeId", "Buffs": "BuffId", "Effects": "EffectId", } self.FILE_TRANSLATE = { "Units": "unit_typeid", "Abilities": "ability_id", "Upgrades": "upgrade_id", "Buffs": "buff_id", "Effects": "effect_id", } @staticmethod def make_key(key: str) -> str: if key[0].isdigit(): key = "_" + key # In patch 5.0, the key has "@" character in it which is not possible with python enums return key.upper().replace(" ", "_").replace("@", "") def parse_data(self, data) -> dict[str, Any]: # for d in data: # Units, Abilities, Upgrades, Buffs, Effects units = self.parse_simple("Units", data) upgrades = self.parse_simple("Upgrades", data) effects = self.parse_simple("Effects", data) buffs = self.parse_simple("Buffs", data) abilities = {} for v in data["Abilities"]: key = v["buttonname"] remapid = v.get("remapid") if key == "" and v["index"] == 0: key = v["name"] if (not key) and (remapid is None): assert v["buttonname"] == "" continue if not key: if v["friendlyname"] != "": key = v["friendlyname"] else: sys.exit(f"Not mapped: {v!r}") key = key.upper().replace(" ", "_").replace("@", "") if "name" in v: key = f"{v['name'].upper().replace(' ', '_')}_{key}" if "friendlyname" in v: key = v["friendlyname"].upper().replace(" ", "_") if key[0].isdigit(): key = "_" + key if key in abilities and v["index"] == 0: logger.info(f"{key} has value 0 and id {v['id']}, overwriting {key}: {abilities[key]}") # Commented out to try to fix: 3670 is not a valid AbilityId abilities[key] = v["id"] elif key in abilities: logger.info(f"{key} has appeared a second time with id={v['id']}") else: abilities[key] = v["id"] abilities["SMART"] = 1 enums = {} enums["Units"] = units enums["Abilities"] = abilities enums["Upgrades"] = upgrades enums["Buffs"] = buffs enums["Effects"] = effects return enums def parse_simple(self, d, data): units = {} for v in data[d]: key = v["name"] if not key: continue key_to_insert = self.make_key(key) if key_to_insert in units: index = 2 tmp = f"{key_to_insert}_{index}" while tmp in units: index += 1 tmp = f"{key_to_insert}_{index}" key_to_insert = tmp units[key_to_insert] = v["id"] return units def generate_python_code(self, enums) -> None: assert {"Units", "Abilities", "Upgrades", "Buffs", "Effects"} <= enums.keys() sc2dir = Path(__file__).parent idsdir = sc2dir / "ids" idsdir.mkdir(exist_ok=True) with (idsdir / "__init__.py").open("w") as f: initstring = f"__all__ = {[n.lower() for n in self.FILE_TRANSLATE.values()]!r}\n".replace("'", '"') f.write("\n".join([self.HEADER, initstring])) for name, body in enums.items(): class_name = self.ENUM_TRANSLATE[name] code = [self.HEADER, "import enum", "\n", f"class {class_name}(enum.Enum):"] for key, value in sorted(body.items(), key=lambda p: p[1]): code.append(f" {key} = {value}") # Add repr function to more easily dump enums to dict code += f""" def __repr__(self) -> str: return f"{class_name}.{{self.name}}" """.split("\n") # Add missing ids function to not make the game crash when unknown BuffId was detected if class_name == "BuffId": code += f""" @classmethod def _missing_(cls, value: int) -> {class_name}: return cls.NULL """.split("\n") if class_name == "AbilityId": code += f""" @classmethod def _missing_(cls, value: int) -> {class_name}: return cls.NULL_NULL """.split("\n") code += f""" for item in {class_name}: globals()[item.name] = item """.split("\n") ids_file_path = (idsdir / self.FILE_TRANSLATE[name]).with_suffix(".py") with ids_file_path.open("w") as f: f.write("\n".join(code)) if self.game_version is not None: version_path = Path(__file__).parent / "ids" / "id_version.py" with Path(version_path).open("w") as f: f.write(f'ID_VERSION_STRING = "{self.game_version}"\n') def update_ids_from_stableid_json(self) -> None: if self.game_version is None or self.game_version != ID_VERSION_STRING: if self.verbose and self.game_version is not None: logger.info( f"Game version is different (Old: {self.game_version}, new: {ID_VERSION_STRING}. Updating ids to match game version" ) stable_id_path = Path(self.DATA_JSON[self.PF]) assert stable_id_path.is_file(), f'stable_id.json was not found at path "{stable_id_path}"' with stable_id_path.open(encoding="utf-8") as data_file: data = json.loads(data_file.read()) self.generate_python_code(self.parse_data(data)) @staticmethod def reimport_ids() -> None: # Reload the newly written "id" files # TODO This only re-imports modules, but if they haven't been imported, it will yield an error importlib.reload(sys.modules["sc2.ids.ability_id"]) importlib.reload(sys.modules["sc2.ids.unit_typeid"]) importlib.reload(sys.modules["sc2.ids.upgrade_id"]) importlib.reload(sys.modules["sc2.ids.effect_id"]) importlib.reload(sys.modules["sc2.ids.buff_id"]) # importlib.reload(sys.modules["sc2.ids.id_version"]) importlib.reload(sys.modules["sc2.constants"]) if __name__ == "__main__": updater = IdGenerator() updater.update_ids_from_stableid_json() ================================================ FILE: sc2/ids/__init__.py ================================================ from __future__ import annotations # DO NOT EDIT! # This file was automatically generated by "generate_ids.py" __all__ = ["unit_typeid", "ability_id", "upgrade_id", "buff_id", "effect_id"] ================================================ FILE: sc2/ids/ability_id.py ================================================ from __future__ import annotations # DO NOT EDIT! # This file was automatically generated by "generate_ids.py" import enum class AbilityId(enum.Enum): NULL_NULL = 0 SMART = 1 TAUNT_TAUNT = 2 STOP_STOP = 4 STOP_HOLDFIRESPECIAL = 5 STOP_CHEER = 6 STOP_DANCE = 7 HOLDFIRE_STOPSPECIAL = 10 HOLDFIRE_HOLDFIRE = 11 MOVE_MOVE = 16 PATROL_PATROL = 17 HOLDPOSITION_HOLD = 18 SCAN_MOVE = 19 MOVE_TURN = 20 BEACON_CANCEL = 21 BEACON_BEACONMOVE = 22 ATTACK_ATTACK = 23 ATTACK_ATTACKTOWARDS = 24 ATTACK_ATTACKBARRAGE = 25 EFFECT_SPRAY_TERRAN = 26 EFFECT_SPRAY_ZERG = 28 EFFECT_SPRAY_PROTOSS = 30 EFFECT_SALVAGE = 32 CORRUPTION_CORRUPTIONABILITY = 34 CORRUPTION_CANCEL = 35 BEHAVIOR_HOLDFIREON_GHOST = 36 BEHAVIOR_HOLDFIREOFF_GHOST = 38 MORPHTOINFESTEDTERRAN_INFESTEDTERRANS = 40 EXPLODE_EXPLODE = 42 RESEARCH_INTERCEPTORGRAVITONCATAPULT = 44 FLEETBEACONRESEARCH_RESEARCHINTERCEPTORLAUNCHSPEEDUPGRADE = 45 RESEARCH_PHOENIXANIONPULSECRYSTALS = 46 FLEETBEACONRESEARCH_TEMPESTRANGEUPGRADE = 47 FLEETBEACONRESEARCH_RESEARCHVOIDRAYSPEEDUPGRADE = 48 FLEETBEACONRESEARCH_TEMPESTRESEARCHGROUNDATTACKUPGRADE = 49 FUNGALGROWTH_FUNGALGROWTH = 74 GUARDIANSHIELD_GUARDIANSHIELD = 76 EFFECT_REPAIR_MULE = 78 MORPHZERGLINGTOBANELING_BANELING = 80 NEXUSTRAINMOTHERSHIP_MOTHERSHIP = 110 FEEDBACK_FEEDBACK = 140 EFFECT_MASSRECALL_STRATEGICRECALL = 142 PLACEPOINTDEFENSEDRONE_POINTDEFENSEDRONE = 144 HALLUCINATION_ARCHON = 146 HALLUCINATION_COLOSSUS = 148 HALLUCINATION_HIGHTEMPLAR = 150 HALLUCINATION_IMMORTAL = 152 HALLUCINATION_PHOENIX = 154 HALLUCINATION_PROBE = 156 HALLUCINATION_STALKER = 158 HALLUCINATION_VOIDRAY = 160 HALLUCINATION_WARPPRISM = 162 HALLUCINATION_ZEALOT = 164 HARVEST_GATHER_MULE = 166 HARVEST_RETURN_MULE = 167 SEEKERMISSILE_HUNTERSEEKERMISSILE = 169 CALLDOWNMULE_CALLDOWNMULE = 171 GRAVITONBEAM_GRAVITONBEAM = 173 CANCEL_GRAVITONBEAM = 174 BUILDINPROGRESSNYDUSCANAL_CANCEL = 175 SIPHON_SIPHON = 177 SIPHON_CANCEL = 178 LEECH_LEECH = 179 SPAWNCHANGELING_SPAWNCHANGELING = 181 DISGUISEASZEALOT_ZEALOT = 183 DISGUISEASMARINEWITHSHIELD_MARINE = 185 DISGUISEASMARINEWITHOUTSHIELD_MARINE = 187 DISGUISEASZERGLINGWITHWINGS_ZERGLING = 189 DISGUISEASZERGLINGWITHOUTWINGS_ZERGLING = 191 PHASESHIFT_PHASESHIFT = 193 RALLY_BUILDING = 195 RALLY_MORPHING_UNIT = 199 RALLY_COMMANDCENTER = 203 RALLY_NEXUS = 207 RALLY_HATCHERY_UNITS = 211 RALLY_HATCHERY_WORKERS = 212 ROACHWARRENRESEARCH_ROACHWARRENRESEARCH = 215 RESEARCH_GLIALREGENERATION = 216 RESEARCH_TUNNELINGCLAWS = 217 ROACHWARRENRESEARCH_ROACHSUPPLY = 218 SAPSTRUCTURE_SAPSTRUCTURE = 245 INFESTEDTERRANS_INFESTEDTERRANS = 247 NEURALPARASITE_NEURALPARASITE = 249 CANCEL_NEURALPARASITE = 250 EFFECT_INJECTLARVA = 251 EFFECT_STIM_MARAUDER = 253 SUPPLYDROP_SUPPLYDROP = 255 _250MMSTRIKECANNONS_250MMSTRIKECANNONS = 257 _250MMSTRIKECANNONS_CANCEL = 258 TEMPORALRIFT_TEMPORALRIFT = 259 EFFECT_CHRONOBOOST = 261 RESEARCH_ANABOLICSYNTHESIS = 263 RESEARCH_CHITINOUSPLATING = 265 WORMHOLETRANSIT_WORMHOLETRANSIT = 293 HARVEST_GATHER_SCV = 295 HARVEST_RETURN_SCV = 296 HARVEST_GATHER_PROBE = 298 HARVEST_RETURN_PROBE = 299 ATTACKWARPPRISM_ATTACKWARPPRISM = 301 ATTACKWARPPRISM_ATTACKTOWARDS = 302 ATTACKWARPPRISM_ATTACKBARRAGE = 303 CANCEL_QUEUE1 = 304 CANCELSLOT_QUEUE1 = 305 CANCEL_QUEUE5 = 306 CANCELSLOT_QUEUE5 = 307 CANCEL_QUEUECANCELTOSELECTION = 308 CANCELSLOT_QUEUECANCELTOSELECTION = 309 QUE5LONGBLEND_CANCEL = 310 QUE5LONGBLEND_CANCELSLOT = 311 CANCEL_QUEUEADDON = 312 CANCELSLOT_ADDON = 313 CANCEL_BUILDINPROGRESS = 314 HALT_BUILDING = 315 EFFECT_REPAIR_SCV = 316 TERRANBUILD_COMMANDCENTER = 318 TERRANBUILD_SUPPLYDEPOT = 319 TERRANBUILD_REFINERY = 320 TERRANBUILD_BARRACKS = 321 TERRANBUILD_ENGINEERINGBAY = 322 TERRANBUILD_MISSILETURRET = 323 TERRANBUILD_BUNKER = 324 TERRANBUILD_SENSORTOWER = 326 TERRANBUILD_GHOSTACADEMY = 327 TERRANBUILD_FACTORY = 328 TERRANBUILD_STARPORT = 329 TERRANBUILD_ARMORY = 331 TERRANBUILD_FUSIONCORE = 333 HALT_TERRANBUILD = 348 RAVENBUILD_AUTOTURRET = 349 RAVENBUILD_CANCEL = 379 EFFECT_STIM_MARINE = 380 BEHAVIOR_CLOAKON_GHOST = 382 BEHAVIOR_CLOAKOFF_GHOST = 383 SNIPE_SNIPE = 384 MEDIVACHEAL_HEAL = 386 SIEGEMODE_SIEGEMODE = 388 UNSIEGE_UNSIEGE = 390 BEHAVIOR_CLOAKON_BANSHEE = 392 BEHAVIOR_CLOAKOFF_BANSHEE = 393 LOAD_MEDIVAC = 394 UNLOADALLAT_MEDIVAC = 396 UNLOADUNIT_MEDIVAC = 397 SCANNERSWEEP_SCAN = 399 YAMATO_YAMATOGUN = 401 MORPH_VIKINGASSAULTMODE = 403 MORPH_VIKINGFIGHTERMODE = 405 LOAD_BUNKER = 407 UNLOADALL_BUNKER = 408 UNLOADUNIT_BUNKER = 410 COMMANDCENTERTRANSPORT_COMMANDCENTERTRANSPORT = 412 UNLOADALL_COMMANDCENTER = 413 UNLOADUNIT_COMMANDCENTER = 415 LOADALL_COMMANDCENTER = 416 LIFT_COMMANDCENTER = 417 LAND_COMMANDCENTER = 419 BUILD_TECHLAB_BARRACKS = 421 BUILD_REACTOR_BARRACKS = 422 CANCEL_BARRACKSADDON = 451 LIFT_BARRACKS = 452 BUILD_TECHLAB_FACTORY = 454 BUILD_REACTOR_FACTORY = 455 CANCEL_FACTORYADDON = 484 LIFT_FACTORY = 485 BUILD_TECHLAB_STARPORT = 487 BUILD_REACTOR_STARPORT = 488 CANCEL_STARPORTADDON = 517 LIFT_STARPORT = 518 LAND_FACTORY = 520 LAND_STARPORT = 522 COMMANDCENTERTRAIN_SCV = 524 LAND_BARRACKS = 554 MORPH_SUPPLYDEPOT_LOWER = 556 MORPH_SUPPLYDEPOT_RAISE = 558 BARRACKSTRAIN_MARINE = 560 BARRACKSTRAIN_REAPER = 561 BARRACKSTRAIN_GHOST = 562 BARRACKSTRAIN_MARAUDER = 563 FACTORYTRAIN_FACTORYTRAIN = 590 FACTORYTRAIN_SIEGETANK = 591 FACTORYTRAIN_THOR = 594 FACTORYTRAIN_HELLION = 595 TRAIN_HELLBAT = 596 TRAIN_CYCLONE = 597 FACTORYTRAIN_WIDOWMINE = 614 STARPORTTRAIN_MEDIVAC = 620 STARPORTTRAIN_BANSHEE = 621 STARPORTTRAIN_RAVEN = 622 STARPORTTRAIN_BATTLECRUISER = 623 STARPORTTRAIN_VIKINGFIGHTER = 624 STARPORTTRAIN_LIBERATOR = 626 RESEARCH_HISECAUTOTRACKING = 650 RESEARCH_TERRANSTRUCTUREARMORUPGRADE = 651 ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL1 = 652 ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL2 = 653 ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL3 = 654 RESEARCH_NEOSTEELFRAME = 655 ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL1 = 656 ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL2 = 657 ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL3 = 658 MERCCOMPOUNDRESEARCH_MERCCOMPOUNDRESEARCH = 680 MERCCOMPOUNDRESEARCH_REAPERSPEED = 683 BUILD_NUKE = 710 BARRACKSTECHLABRESEARCH_STIMPACK = 730 RESEARCH_COMBATSHIELD = 731 RESEARCH_CONCUSSIVESHELLS = 732 FACTORYTECHLABRESEARCH_FACTORYTECHLABRESEARCH = 760 RESEARCH_INFERNALPREIGNITER = 761 FACTORYTECHLABRESEARCH_RESEARCHTRANSFORMATIONSERVOS = 763 RESEARCH_DRILLINGCLAWS = 764 FACTORYTECHLABRESEARCH_RESEARCHLOCKONRANGEUPGRADE = 765 RESEARCH_SMARTSERVOS = 766 FACTORYTECHLABRESEARCH_RESEARCHARMORPIERCINGROCKETS = 767 RESEARCH_CYCLONERAPIDFIRELAUNCHERS = 768 RESEARCH_CYCLONELOCKONDAMAGE = 769 FACTORYTECHLABRESEARCH_CYCLONERESEARCHHURRICANETHRUSTERS = 770 RESEARCH_BANSHEECLOAKINGFIELD = 790 STARPORTTECHLABRESEARCH_RESEARCHMEDIVACENERGYUPGRADE = 792 RESEARCH_RAVENCORVIDREACTOR = 793 STARPORTTECHLABRESEARCH_RESEARCHSEEKERMISSILE = 796 STARPORTTECHLABRESEARCH_RESEARCHDURABLEMATERIALS = 797 RESEARCH_BANSHEEHYPERFLIGHTROTORS = 799 STARPORTTECHLABRESEARCH_RESEARCHLIBERATORAGMODE = 800 STARPORTTECHLABRESEARCH_RESEARCHRAPIDDEPLOYMENT = 802 RESEARCH_RAVENRECALIBRATEDEXPLOSIVES = 803 RESEARCH_HIGHCAPACITYFUELTANKS = 804 RESEARCH_ADVANCEDBALLISTICS = 805 STARPORTTECHLABRESEARCH_RAVENRESEARCHENHANCEDMUNITIONS = 806 STARPORTTECHLABRESEARCH_RESEARCHRAVENINTERFERENCEMATRIX = 807 RESEARCH_PERSONALCLOAKING = 820 ARMORYRESEARCH_ARMORYRESEARCH = 850 ARMORYRESEARCH_TERRANVEHICLEPLATINGLEVEL1 = 852 ARMORYRESEARCH_TERRANVEHICLEPLATINGLEVEL2 = 853 ARMORYRESEARCH_TERRANVEHICLEPLATINGLEVEL3 = 854 ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL1 = 855 ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL2 = 856 ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL3 = 857 ARMORYRESEARCH_TERRANSHIPPLATINGLEVEL1 = 858 ARMORYRESEARCH_TERRANSHIPPLATINGLEVEL2 = 859 ARMORYRESEARCH_TERRANSHIPPLATINGLEVEL3 = 860 ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL1 = 861 ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL2 = 862 ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL3 = 863 ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL1 = 864 ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL2 = 865 ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL3 = 866 PROTOSSBUILD_NEXUS = 880 PROTOSSBUILD_PYLON = 881 PROTOSSBUILD_ASSIMILATOR = 882 PROTOSSBUILD_GATEWAY = 883 PROTOSSBUILD_FORGE = 884 PROTOSSBUILD_FLEETBEACON = 885 PROTOSSBUILD_TWILIGHTCOUNCIL = 886 PROTOSSBUILD_PHOTONCANNON = 887 PROTOSSBUILD_STARGATE = 889 PROTOSSBUILD_TEMPLARARCHIVE = 890 PROTOSSBUILD_DARKSHRINE = 891 PROTOSSBUILD_ROBOTICSBAY = 892 PROTOSSBUILD_ROBOTICSFACILITY = 893 PROTOSSBUILD_CYBERNETICSCORE = 894 BUILD_SHIELDBATTERY = 895 PROTOSSBUILD_CANCEL = 910 LOAD_WARPPRISM = 911 UNLOADALL_WARPPRISM = 912 UNLOADALLAT_WARPPRISM = 913 UNLOADUNIT_WARPPRISM = 914 GATEWAYTRAIN_ZEALOT = 916 GATEWAYTRAIN_STALKER = 917 GATEWAYTRAIN_HIGHTEMPLAR = 919 GATEWAYTRAIN_DARKTEMPLAR = 920 GATEWAYTRAIN_SENTRY = 921 TRAIN_ADEPT = 922 STARGATETRAIN_PHOENIX = 946 STARGATETRAIN_CARRIER = 948 STARGATETRAIN_VOIDRAY = 950 STARGATETRAIN_ORACLE = 954 STARGATETRAIN_TEMPEST = 955 ROBOTICSFACILITYTRAIN_WARPPRISM = 976 ROBOTICSFACILITYTRAIN_OBSERVER = 977 ROBOTICSFACILITYTRAIN_COLOSSUS = 978 ROBOTICSFACILITYTRAIN_IMMORTAL = 979 TRAIN_DISRUPTOR = 994 NEXUSTRAIN_PROBE = 1006 PSISTORM_PSISTORM = 1036 CANCEL_HANGARQUEUE5 = 1038 CANCELSLOT_HANGARQUEUE5 = 1039 BROODLORDQUEUE2_CANCEL = 1040 BROODLORDQUEUE2_CANCELSLOT = 1041 BUILD_INTERCEPTORS = 1042 FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL1 = 1062 FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL2 = 1063 FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL3 = 1064 FORGERESEARCH_PROTOSSGROUNDARMORLEVEL1 = 1065 FORGERESEARCH_PROTOSSGROUNDARMORLEVEL2 = 1066 FORGERESEARCH_PROTOSSGROUNDARMORLEVEL3 = 1067 FORGERESEARCH_PROTOSSSHIELDSLEVEL1 = 1068 FORGERESEARCH_PROTOSSSHIELDSLEVEL2 = 1069 FORGERESEARCH_PROTOSSSHIELDSLEVEL3 = 1070 ROBOTICSBAYRESEARCH_ROBOTICSBAYRESEARCH = 1092 RESEARCH_GRAVITICBOOSTER = 1093 RESEARCH_GRAVITICDRIVE = 1094 RESEARCH_EXTENDEDTHERMALLANCE = 1097 ROBOTICSBAYRESEARCH_RESEARCHIMMORTALREVIVE = 1099 TEMPLARARCHIVESRESEARCH_TEMPLARARCHIVESRESEARCH = 1122 RESEARCH_PSISTORM = 1126 ZERGBUILD_HATCHERY = 1152 ZERGBUILD_CREEPTUMOR = 1153 ZERGBUILD_EXTRACTOR = 1154 ZERGBUILD_SPAWNINGPOOL = 1155 ZERGBUILD_EVOLUTIONCHAMBER = 1156 ZERGBUILD_HYDRALISKDEN = 1157 ZERGBUILD_SPIRE = 1158 ZERGBUILD_ULTRALISKCAVERN = 1159 ZERGBUILD_INFESTATIONPIT = 1160 ZERGBUILD_NYDUSNETWORK = 1161 ZERGBUILD_BANELINGNEST = 1162 BUILD_LURKERDEN = 1163 ZERGBUILD_ROACHWARREN = 1165 ZERGBUILD_SPINECRAWLER = 1166 ZERGBUILD_SPORECRAWLER = 1167 ZERGBUILD_CANCEL = 1182 HARVEST_GATHER_DRONE = 1183 HARVEST_RETURN_DRONE = 1184 RESEARCH_ZERGMELEEWEAPONSLEVEL1 = 1186 RESEARCH_ZERGMELEEWEAPONSLEVEL2 = 1187 RESEARCH_ZERGMELEEWEAPONSLEVEL3 = 1188 RESEARCH_ZERGGROUNDARMORLEVEL1 = 1189 RESEARCH_ZERGGROUNDARMORLEVEL2 = 1190 RESEARCH_ZERGGROUNDARMORLEVEL3 = 1191 RESEARCH_ZERGMISSILEWEAPONSLEVEL1 = 1192 RESEARCH_ZERGMISSILEWEAPONSLEVEL2 = 1193 RESEARCH_ZERGMISSILEWEAPONSLEVEL3 = 1194 EVOLUTIONCHAMBERRESEARCH_EVOLVEPROPULSIVEPERISTALSIS = 1195 UPGRADETOLAIR_LAIR = 1216 CANCEL_MORPHLAIR = 1217 UPGRADETOHIVE_HIVE = 1218 CANCEL_MORPHHIVE = 1219 UPGRADETOGREATERSPIRE_GREATERSPIRE = 1220 CANCEL_MORPHGREATERSPIRE = 1221 LAIRRESEARCH_LAIRRESEARCH = 1222 RESEARCH_PNEUMATIZEDCARAPACE = 1223 LAIRRESEARCH_EVOLVEVENTRALSACKS = 1224 RESEARCH_BURROW = 1225 RESEARCH_ZERGLINGADRENALGLANDS = 1252 RESEARCH_ZERGLINGMETABOLICBOOST = 1253 RESEARCH_GROOVEDSPINES = 1282 RESEARCH_MUSCULARAUGMENTS = 1283 HYDRALISKDENRESEARCH_RESEARCHFRENZY = 1284 HYDRALISKDENRESEARCH_RESEARCHLURKERRANGE = 1286 RESEARCH_ZERGFLYERATTACKLEVEL1 = 1312 RESEARCH_ZERGFLYERATTACKLEVEL2 = 1313 RESEARCH_ZERGFLYERATTACKLEVEL3 = 1314 RESEARCH_ZERGFLYERARMORLEVEL1 = 1315 RESEARCH_ZERGFLYERARMORLEVEL2 = 1316 RESEARCH_ZERGFLYERARMORLEVEL3 = 1317 LARVATRAIN_DRONE = 1342 LARVATRAIN_ZERGLING = 1343 LARVATRAIN_OVERLORD = 1344 LARVATRAIN_HYDRALISK = 1345 LARVATRAIN_MUTALISK = 1346 LARVATRAIN_ULTRALISK = 1348 LARVATRAIN_ROACH = 1351 LARVATRAIN_INFESTOR = 1352 LARVATRAIN_CORRUPTOR = 1353 LARVATRAIN_VIPER = 1354 TRAIN_SWARMHOST = 1356 MORPHTOBROODLORD_BROODLORD = 1372 CANCEL_MORPHBROODLORD = 1373 BURROWDOWN_BANELING = 1374 BURROWBANELINGDOWN_CANCEL = 1375 BURROWUP_BANELING = 1376 BURROWDOWN_DRONE = 1378 BURROWDRONEDOWN_CANCEL = 1379 BURROWUP_DRONE = 1380 BURROWDOWN_HYDRALISK = 1382 BURROWHYDRALISKDOWN_CANCEL = 1383 BURROWUP_HYDRALISK = 1384 BURROWDOWN_ROACH = 1386 BURROWROACHDOWN_CANCEL = 1387 BURROWUP_ROACH = 1388 BURROWDOWN_ZERGLING = 1390 BURROWZERGLINGDOWN_CANCEL = 1391 BURROWUP_ZERGLING = 1392 BURROWDOWN_INFESTORTERRAN = 1394 BURROWUP_INFESTORTERRAN = 1396 REDSTONELAVACRITTERBURROW_BURROWDOWN = 1398 REDSTONELAVACRITTERINJUREDBURROW_BURROWDOWN = 1400 REDSTONELAVACRITTERUNBURROW_BURROWUP = 1402 REDSTONELAVACRITTERINJUREDUNBURROW_BURROWUP = 1404 LOAD_OVERLORD = 1406 UNLOADALLAT_OVERLORD = 1408 UNLOADUNIT_OVERLORD = 1409 MERGEABLE_CANCEL = 1411 WARPABLE_CANCEL = 1412 WARPGATETRAIN_ZEALOT = 1413 WARPGATETRAIN_STALKER = 1414 WARPGATETRAIN_HIGHTEMPLAR = 1416 WARPGATETRAIN_DARKTEMPLAR = 1417 WARPGATETRAIN_SENTRY = 1418 TRAINWARP_ADEPT = 1419 BURROWDOWN_QUEEN = 1433 BURROWQUEENDOWN_CANCEL = 1434 BURROWUP_QUEEN = 1435 LOAD_NYDUSNETWORK = 1437 UNLOADALL_NYDASNETWORK = 1438 UNLOADUNIT_NYDASNETWORK = 1440 EFFECT_BLINK_STALKER = 1442 BURROWDOWN_INFESTOR = 1444 BURROWINFESTORDOWN_CANCEL = 1445 BURROWUP_INFESTOR = 1446 MORPH_OVERSEER = 1448 CANCEL_MORPHOVERSEER = 1449 UPGRADETOPLANETARYFORTRESS_PLANETARYFORTRESS = 1450 CANCEL_MORPHPLANETARYFORTRESS = 1451 INFESTATIONPITRESEARCH_INFESTATIONPITRESEARCH = 1452 RESEARCH_NEURALPARASITE = 1455 INFESTATIONPITRESEARCH_RESEARCHLOCUSTLIFETIMEINCREASE = 1456 INFESTATIONPITRESEARCH_EVOLVEAMORPHOUSARMORCLOUD = 1457 RESEARCH_CENTRIFUGALHOOKS = 1482 BURROWDOWN_ULTRALISK = 1512 BURROWUP_ULTRALISK = 1514 UPGRADETOORBITAL_ORBITALCOMMAND = 1516 CANCEL_MORPHORBITAL = 1517 MORPH_WARPGATE = 1518 UPGRADETOWARPGATE_CANCEL = 1519 MORPH_GATEWAY = 1520 MORPHBACKTOGATEWAY_CANCEL = 1521 LIFT_ORBITALCOMMAND = 1522 LAND_ORBITALCOMMAND = 1524 FORCEFIELD_FORCEFIELD = 1526 FORCEFIELD_CANCEL = 1527 MORPH_WARPPRISMPHASINGMODE = 1528 PHASINGMODE_CANCEL = 1529 MORPH_WARPPRISMTRANSPORTMODE = 1530 TRANSPORTMODE_CANCEL = 1531 RESEARCH_BATTLECRUISERWEAPONREFIT = 1532 FUSIONCORERESEARCH_RESEARCHBALLISTICRANGE = 1533 FUSIONCORERESEARCH_RESEARCHRAPIDREIGNITIONSYSTEM = 1534 FUSIONCORERESEARCH_RESEARCHMEDIVACENERGYUPGRADE = 1535 CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL1 = 1562 CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL2 = 1563 CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL3 = 1564 CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL1 = 1565 CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL2 = 1566 CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL3 = 1567 RESEARCH_WARPGATE = 1568 CYBERNETICSCORERESEARCH_RESEARCHHALLUCINATION = 1571 RESEARCH_CHARGE = 1592 RESEARCH_BLINK = 1593 RESEARCH_ADEPTRESONATINGGLAIVES = 1594 TWILIGHTCOUNCILRESEARCH_RESEARCHPSIONICSURGE = 1595 TWILIGHTCOUNCILRESEARCH_RESEARCHAMPLIFIEDSHIELDING = 1596 TWILIGHTCOUNCILRESEARCH_RESEARCHPSIONICAMPLIFIERS = 1597 TACNUKESTRIKE_NUKECALLDOWN = 1622 CANCEL_NUKE = 1623 SALVAGEBUNKERREFUND_SALVAGE = 1624 SALVAGEBUNKER_SALVAGE = 1626 EMP_EMP = 1628 VORTEX_VORTEX = 1630 TRAINQUEEN_QUEEN = 1632 BURROWCREEPTUMORDOWN_BURROWDOWN = 1662 TRANSFUSION_TRANSFUSION = 1664 TECHLABMORPH_TECHLABMORPH = 1666 BARRACKSTECHLABMORPH_TECHLABBARRACKS = 1668 FACTORYTECHLABMORPH_TECHLABFACTORY = 1670 STARPORTTECHLABMORPH_TECHLABSTARPORT = 1672 REACTORMORPH_REACTORMORPH = 1674 BARRACKSREACTORMORPH_REACTOR = 1676 FACTORYREACTORMORPH_REACTOR = 1678 STARPORTREACTORMORPH_REACTOR = 1680 ATTACK_REDIRECT = 1682 EFFECT_STIM_MARINE_REDIRECT = 1683 EFFECT_STIM_MARAUDER_REDIRECT = 1684 BURROWEDSTOP_STOPROACHBURROWED = 1685 BURROWEDSTOP_HOLDFIRESPECIAL = 1686 STOP_REDIRECT = 1691 BEHAVIOR_GENERATECREEPON = 1692 BEHAVIOR_GENERATECREEPOFF = 1693 BUILD_CREEPTUMOR_QUEEN = 1694 QUEENBUILD_CANCEL = 1724 SPINECRAWLERUPROOT_SPINECRAWLERUPROOT = 1725 SPINECRAWLERUPROOT_CANCEL = 1726 SPORECRAWLERUPROOT_SPORECRAWLERUPROOT = 1727 SPORECRAWLERUPROOT_CANCEL = 1728 SPINECRAWLERROOT_SPINECRAWLERROOT = 1729 CANCEL_SPINECRAWLERROOT = 1730 SPORECRAWLERROOT_SPORECRAWLERROOT = 1731 CANCEL_SPORECRAWLERROOT = 1732 BUILD_CREEPTUMOR_TUMOR = 1733 CANCEL_CREEPTUMOR = 1763 BUILDAUTOTURRET_AUTOTURRET = 1764 MORPH_ARCHON = 1766 ARCHON_WARP_TARGET = 1767 BUILD_NYDUSWORM = 1768 BUILDNYDUSCANAL_SUMMONNYDUSCANALATTACKER = 1769 BUILDNYDUSCANAL_CANCEL = 1798 BROODLORDHANGAR_BROODLORDHANGAR = 1799 EFFECT_CHARGE = 1819 TOWERCAPTURE_TOWERCAPTURE = 1820 HERDINTERACT_HERD = 1821 FRENZY_FRENZY = 1823 CONTAMINATE_CONTAMINATE = 1825 SHATTER_SHATTER = 1827 INFESTEDTERRANSLAYEGG_INFESTEDTERRANS = 1829 CANCEL_QUEUEPASIVE = 1831 CANCELSLOT_QUEUEPASSIVE = 1832 CANCEL_QUEUEPASSIVECANCELTOSELECTION = 1833 CANCELSLOT_QUEUEPASSIVECANCELTOSELECTION = 1834 MORPHTOGHOSTALTERNATE_MOVE = 1835 MORPHTOGHOSTNOVA_MOVE = 1837 DIGESTERCREEPSPRAY_DIGESTERCREEPSPRAY = 1839 MORPHTOCOLLAPSIBLETERRANTOWERDEBRIS_MORPHTOCOLLAPSIBLETERRANTOWERDEBRIS = 1841 MORPHTOCOLLAPSIBLETERRANTOWERDEBRIS_CANCEL = 1842 MORPHTOCOLLAPSIBLETERRANTOWERDEBRISRAMPLEFT_MORPHTOCOLLAPSIBLETERRANTOWERDEBRISRAMPLEFT = 1843 MORPHTOCOLLAPSIBLETERRANTOWERDEBRISRAMPLEFT_CANCEL = 1844 MORPHTOCOLLAPSIBLETERRANTOWERDEBRISRAMPRIGHT_MORPHTOCOLLAPSIBLETERRANTOWERDEBRISRAMPRIGHT = 1845 MORPHTOCOLLAPSIBLETERRANTOWERDEBRISRAMPRIGHT_CANCEL = 1846 MORPH_MOTHERSHIP = 1847 CANCEL_MORPHMOTHERSHIP = 1848 MOTHERSHIPSTASIS_MOTHERSHIPSTASIS = 1849 CANCEL_MOTHERSHIPSTASIS = 1850 MOTHERSHIPCOREWEAPON_MOTHERSHIPSTASIS = 1851 NEXUSTRAINMOTHERSHIPCORE_MOTHERSHIPCORE = 1853 MOTHERSHIPCORETELEPORT_MOTHERSHIPCORETELEPORT = 1883 SALVAGEDRONEREFUND_SALVAGE = 1885 SALVAGEDRONE_SALVAGE = 1887 SALVAGEZERGLINGREFUND_SALVAGE = 1889 SALVAGEZERGLING_SALVAGE = 1891 SALVAGEQUEENREFUND_SALVAGE = 1893 SALVAGEQUEEN_SALVAGE = 1895 SALVAGEROACHREFUND_SALVAGE = 1897 SALVAGEROACH_SALVAGE = 1899 SALVAGEBANELINGREFUND_SALVAGE = 1901 SALVAGEBANELING_SALVAGE = 1903 SALVAGEHYDRALISKREFUND_SALVAGE = 1905 SALVAGEHYDRALISK_SALVAGE = 1907 SALVAGEINFESTORREFUND_SALVAGE = 1909 SALVAGEINFESTOR_SALVAGE = 1911 SALVAGESWARMHOSTREFUND_SALVAGE = 1913 SALVAGESWARMHOST_SALVAGE = 1915 SALVAGEULTRALISKREFUND_SALVAGE = 1917 SALVAGEULTRALISK_SALVAGE = 1919 DIGESTERTRANSPORT_LOADDIGESTER = 1921 SPECTRESHIELD_SPECTRESHIELD = 1926 XELNAGAHEALINGSHRINE_XELNAGAHEALINGSHRINE = 1928 NEXUSINVULNERABILITY_NEXUSINVULNERABILITY = 1930 NEXUSPHASESHIFT_NEXUSPHASESHIFT = 1932 SPAWNCHANGELINGTARGET_SPAWNCHANGELING = 1934 QUEENLAND_QUEENLAND = 1936 QUEENFLY_QUEENFLY = 1938 ORACLECLOAKFIELD_ORACLECLOAKFIELD = 1940 FLYERSHIELD_FLYERSHIELD = 1942 LOCUSTTRAIN_SWARMHOST = 1944 EFFECT_MASSRECALL_MOTHERSHIPCORE = 1974 SINGLERECALL_SINGLERECALL = 1976 MORPH_HELLION = 1978 RESTORESHIELDS_RESTORESHIELDS = 1980 SCRYER_SCRYER = 1982 BURROWCHARGETRIAL_BURROWCHARGETRIAL = 1984 LEECHRESOURCES_LEECHRESOURCES = 1986 LEECHRESOURCES_CANCEL = 1987 SNIPEDOT_SNIPEDOT = 1988 SWARMHOSTSPAWNLOCUSTS_LOCUSTMP = 1990 CLONE_CLONE = 1992 BUILDINGSHIELD_BUILDINGSHIELD = 1994 MORPHTOCOLLAPSIBLEROCKTOWERDEBRIS_MORPHTOCOLLAPSIBLEROCKTOWERDEBRIS = 1996 MORPHTOCOLLAPSIBLEROCKTOWERDEBRIS_CANCEL = 1997 MORPH_HELLBAT = 1998 BUILDINGSTASIS_BUILDINGSTASIS = 2000 RESOURCEBLOCKER_RESOURCEBLOCKER = 2002 RESOURCESTUN_RESOURCESTUN = 2004 MAXIUMTHRUST_MAXIMUMTHRUST = 2006 SACRIFICE_SACRIFICE = 2008 BURROWCHARGEMP_BURROWCHARGEMP = 2010 BURROWCHARGEREVD_BURROWCHARGEREVD = 2012 BURROWDOWN_SWARMHOST = 2014 MORPHTOSWARMHOSTBURROWEDMP_CANCEL = 2015 BURROWUP_SWARMHOST = 2016 SPAWNINFESTEDTERRAN_LOCUSTMP = 2018 ATTACKPROTOSSBUILDING_ATTACKBUILDING = 2048 ATTACKPROTOSSBUILDING_ATTACKTOWARDS = 2049 ATTACKPROTOSSBUILDING_ATTACKBARRAGE = 2050 BURROWEDBANELINGSTOP_STOPROACHBURROWED = 2051 BURROWEDBANELINGSTOP_HOLDFIRESPECIAL = 2052 STOP_BUILDING = 2057 STOPPROTOSSBUILDING_HOLDFIRE = 2058 STOPPROTOSSBUILDING_CHEER = 2059 STOPPROTOSSBUILDING_DANCE = 2060 BLINDINGCLOUD_BLINDINGCLOUD = 2063 EYESTALK_EYESTALK = 2065 EYESTALK_CANCEL = 2066 EFFECT_ABDUCT = 2067 VIPERCONSUME_VIPERCONSUME = 2069 VIPERCONSUMEMINERALS_VIPERCONSUME = 2071 VIPERCONSUMESTRUCTURE_VIPERCONSUME = 2073 CANCEL_PROTOSSBUILDINGQUEUE = 2075 PROTOSSBUILDINGQUEUE_CANCELSLOT = 2076 QUE8_CANCEL = 2077 QUE8_CANCELSLOT = 2078 TESTZERG_TESTZERG = 2079 TESTZERG_CANCEL = 2080 BEHAVIOR_BUILDINGATTACKON = 2081 BEHAVIOR_BUILDINGATTACKOFF = 2082 PICKUPSCRAPSMALL_PICKUPSCRAPSMALL = 2083 PICKUPSCRAPMEDIUM_PICKUPSCRAPMEDIUM = 2085 PICKUPSCRAPLARGE_PICKUPSCRAPLARGE = 2087 PICKUPPALLETGAS_PICKUPPALLETGAS = 2089 PICKUPPALLETMINERALS_PICKUPPALLETMINERALS = 2091 MASSIVEKNOCKOVER_MASSIVEKNOCKOVER = 2093 BURROWDOWN_WIDOWMINE = 2095 WIDOWMINEBURROW_CANCEL = 2096 BURROWUP_WIDOWMINE = 2097 WIDOWMINEATTACK_WIDOWMINEATTACK = 2099 TORNADOMISSILE_TORNADOMISSILE = 2101 MOTHERSHIPCOREENERGIZE_MOTHERSHIPCOREENERGIZE = 2102 MOTHERSHIPCOREENERGIZE_CANCEL = 2103 LURKERASPECTMPFROMHYDRALISKBURROWED_LURKERMPFROMHYDRALISKBURROWED = 2104 LURKERASPECTMPFROMHYDRALISKBURROWED_CANCEL = 2105 LURKERASPECTMP_LURKERMP = 2106 LURKERASPECTMP_CANCEL = 2107 BURROWDOWN_LURKER = 2108 BURROWLURKERMPDOWN_CANCEL = 2109 BURROWUP_LURKER = 2110 MORPH_LURKERDEN = 2112 CANCEL_MORPHLURKERDEN = 2113 HALLUCINATION_ORACLE = 2114 EFFECT_MEDIVACIGNITEAFTERBURNERS = 2116 EXTENDINGBRIDGENEWIDE8OUT_BRIDGEEXTEND = 2118 EXTENDINGBRIDGENEWIDE8_BRIDGERETRACT = 2120 EXTENDINGBRIDGENWWIDE8OUT_BRIDGEEXTEND = 2122 EXTENDINGBRIDGENWWIDE8_BRIDGERETRACT = 2124 EXTENDINGBRIDGENEWIDE10OUT_BRIDGEEXTEND = 2126 EXTENDINGBRIDGENEWIDE10_BRIDGERETRACT = 2128 EXTENDINGBRIDGENWWIDE10OUT_BRIDGEEXTEND = 2130 EXTENDINGBRIDGENWWIDE10_BRIDGERETRACT = 2132 EXTENDINGBRIDGENEWIDE12OUT_BRIDGEEXTEND = 2134 EXTENDINGBRIDGENEWIDE12_BRIDGERETRACT = 2136 EXTENDINGBRIDGENWWIDE12OUT_BRIDGEEXTEND = 2138 EXTENDINGBRIDGENWWIDE12_BRIDGERETRACT = 2140 INVULNERABILITYSHIELD_INVULNERABILITYSHIELD = 2142 CRITTERFLEE_CRITTERFLEE = 2144 ORACLEREVELATION_ORACLEREVELATION = 2146 ORACLEREVELATIONMODE_ORACLEREVELATIONMODE = 2148 ORACLEREVELATIONMODE_CANCEL = 2149 ORACLENORMALMODE_ORACLENORMALMODE = 2150 ORACLENORMALMODE_CANCEL = 2151 MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPRIGHT_MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPRIGHT = 2152 MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPRIGHT_CANCEL = 2153 MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPLEFT_MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPLEFT = 2154 MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPLEFT_CANCEL = 2155 VOIDSIPHON_VOIDSIPHON = 2156 ULTRALISKWEAPONCOOLDOWN_ULTRALISKWEAPONCOOLDOWN = 2158 MOTHERSHIPCOREPURIFYNEXUSCANCEL_CANCEL = 2160 EFFECT_PHOTONOVERCHARGE = 2162 XELNAGA_CAVERNS_DOORE_XELNAGA_CAVERNS_DOORDEFAULTCLOSE = 2164 XELNAGA_CAVERNS_DOOREOPENED_XELNAGA_CAVERNS_DOORDEFAULTCLOSE = 2166 XELNAGA_CAVERNS_DOORN_XELNAGA_CAVERNS_DOORDEFAULTCLOSE = 2168 XELNAGA_CAVERNS_DOORNE_XELNAGA_CAVERNS_DOORDEFAULTCLOSE = 2170 XELNAGA_CAVERNS_DOORNEOPENED_XELNAGA_CAVERNS_DOORDEFAULTOPEN = 2172 XELNAGA_CAVERNS_DOORNOPENED_XELNAGA_CAVERNS_DOORDEFAULTOPEN = 2174 XELNAGA_CAVERNS_DOORNW_XELNAGA_CAVERNS_DOORDEFAULTCLOSE = 2176 XELNAGA_CAVERNS_DOORNWOPENED_XELNAGA_CAVERNS_DOORDEFAULTOPEN = 2178 XELNAGA_CAVERNS_DOORS_XELNAGA_CAVERNS_DOORDEFAULTCLOSE = 2180 XELNAGA_CAVERNS_DOORSE_XELNAGA_CAVERNS_DOORDEFAULTCLOSE = 2182 XELNAGA_CAVERNS_DOORSEOPENED_XELNAGA_CAVERNS_DOORDEFAULTOPEN = 2184 XELNAGA_CAVERNS_DOORSOPENED_XELNAGA_CAVERNS_DOORDEFAULTOPEN = 2186 XELNAGA_CAVERNS_DOORSW_XELNAGA_CAVERNS_DOORDEFAULTCLOSE = 2188 XELNAGA_CAVERNS_DOORSWOPENED_XELNAGA_CAVERNS_DOORDEFAULTOPEN = 2190 XELNAGA_CAVERNS_DOORW_XELNAGA_CAVERNS_DOORDEFAULTCLOSE = 2192 XELNAGA_CAVERNS_DOORWOPENED_XELNAGA_CAVERNS_DOORDEFAULTOPEN = 2194 XELNAGA_CAVERNS_FLOATING_BRIDGENE8OUT_BRIDGEEXTEND = 2196 XELNAGA_CAVERNS_FLOATING_BRIDGENE8_BRIDGERETRACT = 2198 XELNAGA_CAVERNS_FLOATING_BRIDGENW8OUT_BRIDGEEXTEND = 2200 XELNAGA_CAVERNS_FLOATING_BRIDGENW8_BRIDGERETRACT = 2202 XELNAGA_CAVERNS_FLOATING_BRIDGENE10OUT_BRIDGEEXTEND = 2204 XELNAGA_CAVERNS_FLOATING_BRIDGENE10_BRIDGERETRACT = 2206 XELNAGA_CAVERNS_FLOATING_BRIDGENW10OUT_BRIDGEEXTEND = 2208 XELNAGA_CAVERNS_FLOATING_BRIDGENW10_BRIDGERETRACT = 2210 XELNAGA_CAVERNS_FLOATING_BRIDGENE12OUT_BRIDGEEXTEND = 2212 XELNAGA_CAVERNS_FLOATING_BRIDGENE12_BRIDGERETRACT = 2214 XELNAGA_CAVERNS_FLOATING_BRIDGENW12OUT_BRIDGEEXTEND = 2216 XELNAGA_CAVERNS_FLOATING_BRIDGENW12_BRIDGERETRACT = 2218 XELNAGA_CAVERNS_FLOATING_BRIDGEH8OUT_BRIDGEEXTEND = 2220 XELNAGA_CAVERNS_FLOATING_BRIDGEH8_BRIDGERETRACT = 2222 XELNAGA_CAVERNS_FLOATING_BRIDGEV8OUT_BRIDGEEXTEND = 2224 XELNAGA_CAVERNS_FLOATING_BRIDGEV8_BRIDGERETRACT = 2226 XELNAGA_CAVERNS_FLOATING_BRIDGEH10OUT_BRIDGEEXTEND = 2228 XELNAGA_CAVERNS_FLOATING_BRIDGEH10_BRIDGERETRACT = 2230 XELNAGA_CAVERNS_FLOATING_BRIDGEV10OUT_BRIDGEEXTEND = 2232 XELNAGA_CAVERNS_FLOATING_BRIDGEV10_BRIDGERETRACT = 2234 XELNAGA_CAVERNS_FLOATING_BRIDGEH12OUT_BRIDGEEXTEND = 2236 XELNAGA_CAVERNS_FLOATING_BRIDGEH12_BRIDGERETRACT = 2238 XELNAGA_CAVERNS_FLOATING_BRIDGEV12OUT_BRIDGEEXTEND = 2240 XELNAGA_CAVERNS_FLOATING_BRIDGEV12_BRIDGERETRACT = 2242 EFFECT_TIMEWARP = 2244 SNOWREFINERY_TERRAN_EXTENDINGBRIDGENESHORT8OUT_BRIDGEEXTEND = 2246 SNOWREFINERY_TERRAN_EXTENDINGBRIDGENESHORT8_BRIDGERETRACT = 2248 SNOWREFINERY_TERRAN_EXTENDINGBRIDGENWSHORT8OUT_BRIDGEEXTEND = 2250 SNOWREFINERY_TERRAN_EXTENDINGBRIDGENWSHORT8_BRIDGERETRACT = 2252 SNOWREFINERY_TERRAN_EXTENDINGBRIDGENESHORT10OUT_BRIDGEEXTEND = 2254 SNOWREFINERY_TERRAN_EXTENDINGBRIDGENESHORT10_BRIDGERETRACT = 2256 SNOWREFINERY_TERRAN_EXTENDINGBRIDGENWSHORT10OUT_BRIDGEEXTEND = 2258 SNOWREFINERY_TERRAN_EXTENDINGBRIDGENWSHORT10_BRIDGERETRACT = 2260 TARSONIS_DOORN_TARSONIS_DOORN = 2262 TARSONIS_DOORNLOWERED_TARSONIS_DOORNLOWERED = 2264 TARSONIS_DOORNE_TARSONIS_DOORNE = 2266 TARSONIS_DOORNELOWERED_TARSONIS_DOORNELOWERED = 2268 TARSONIS_DOORE_TARSONIS_DOORE = 2270 TARSONIS_DOORELOWERED_TARSONIS_DOORELOWERED = 2272 TARSONIS_DOORNW_TARSONIS_DOORNW = 2274 TARSONIS_DOORNWLOWERED_TARSONIS_DOORNWLOWERED = 2276 COMPOUNDMANSION_DOORN_COMPOUNDMANSION_DOORN = 2278 COMPOUNDMANSION_DOORNLOWERED_COMPOUNDMANSION_DOORNLOWERED = 2280 COMPOUNDMANSION_DOORNE_COMPOUNDMANSION_DOORNE = 2282 COMPOUNDMANSION_DOORNELOWERED_COMPOUNDMANSION_DOORNELOWERED = 2284 COMPOUNDMANSION_DOORE_COMPOUNDMANSION_DOORE = 2286 COMPOUNDMANSION_DOORELOWERED_COMPOUNDMANSION_DOORELOWERED = 2288 COMPOUNDMANSION_DOORNW_COMPOUNDMANSION_DOORNW = 2290 COMPOUNDMANSION_DOORNWLOWERED_COMPOUNDMANSION_DOORNWLOWERED = 2292 ARMORYRESEARCHSWARM_TERRANVEHICLEANDSHIPWEAPONSLEVEL1 = 2294 ARMORYRESEARCHSWARM_TERRANVEHICLEANDSHIPWEAPONSLEVEL2 = 2295 ARMORYRESEARCHSWARM_TERRANVEHICLEANDSHIPWEAPONSLEVEL3 = 2296 ARMORYRESEARCHSWARM_TERRANVEHICLEANDSHIPPLATINGLEVEL1 = 2297 ARMORYRESEARCHSWARM_TERRANVEHICLEANDSHIPPLATINGLEVEL2 = 2298 ARMORYRESEARCHSWARM_TERRANVEHICLEANDSHIPPLATINGLEVEL3 = 2299 CAUSTICSPRAY_CAUSTICSPRAY = 2324 ORACLECLOAKINGFIELDTARGETED_ORACLECLOAKINGFIELDTARGETED = 2326 EFFECT_IMMORTALBARRIER = 2328 MORPHTORAVAGER_RAVAGER = 2330 CANCEL_MORPHRAVAGER = 2331 MORPH_LURKER = 2332 CANCEL_MORPHLURKER = 2333 ORACLEPHASESHIFT_ORACLEPHASESHIFT = 2334 RELEASEINTERCEPTORS_RELEASEINTERCEPTORS = 2336 EFFECT_CORROSIVEBILE = 2338 BURROWDOWN_RAVAGER = 2340 BURROWRAVAGERDOWN_CANCEL = 2341 BURROWUP_RAVAGER = 2342 PURIFICATIONNOVA_PURIFICATIONNOVA = 2344 EFFECT_PURIFICATIONNOVA = 2346 IMPALE_IMPALE = 2348 LOCKON_LOCKON = 2350 LOCKONAIR_LOCKONAIR = 2352 CANCEL_LOCKON = 2354 CORRUPTIONBOMB_CORRUPTIONBOMB = 2356 CORRUPTIONBOMB_CANCEL = 2357 EFFECT_TACTICALJUMP = 2358 OVERCHARGE_OVERCHARGE = 2360 MORPH_THORHIGHIMPACTMODE = 2362 THORAPMODE_CANCEL = 2363 MORPH_THOREXPLOSIVEMODE = 2364 CANCEL_MORPHTHOREXPLOSIVEMODE = 2365 LIGHTOFAIUR_LIGHTOFAIUR = 2366 EFFECT_MASSRECALL_MOTHERSHIP = 2368 LOAD_NYDUSWORM = 2370 UNLOADALL_NYDUSWORM = 2371 BEHAVIOR_PULSARBEAMON = 2375 BEHAVIOR_PULSARBEAMOFF = 2376 PULSARBEAM_RIPFIELD = 2377 PULSARCANNON_PULSARCANNON = 2379 VOIDSWARMHOSTSPAWNLOCUST_VOIDSWARMHOSTSPAWNLOCUST = 2381 LOCUSTMPFLYINGMORPHTOGROUND_LOCUSTMPFLYINGSWOOP = 2383 LOCUSTMPMORPHTOAIR_LOCUSTMPFLYINGSWOOP = 2385 EFFECT_LOCUSTSWOOP = 2387 HALLUCINATION_DISRUPTOR = 2389 HALLUCINATION_ADEPT = 2391 EFFECT_VOIDRAYPRISMATICALIGNMENT = 2393 SEEKERDUMMYCHANNEL_SEEKERDUMMYCHANNEL = 2395 AIURLIGHTBRIDGENE8OUT_BRIDGEEXTEND = 2397 AIURLIGHTBRIDGENE8_BRIDGERETRACT = 2399 AIURLIGHTBRIDGENE10OUT_BRIDGEEXTEND = 2401 AIURLIGHTBRIDGENE10_BRIDGERETRACT = 2403 AIURLIGHTBRIDGENE12OUT_BRIDGEEXTEND = 2405 AIURLIGHTBRIDGENE12_BRIDGERETRACT = 2407 AIURLIGHTBRIDGENW8OUT_BRIDGEEXTEND = 2409 AIURLIGHTBRIDGENW8_BRIDGERETRACT = 2411 AIURLIGHTBRIDGENW10OUT_BRIDGEEXTEND = 2413 AIURLIGHTBRIDGENW10_BRIDGERETRACT = 2415 AIURLIGHTBRIDGENW12OUT_BRIDGEEXTEND = 2417 AIURLIGHTBRIDGENW12_BRIDGERETRACT = 2419 AIURTEMPLEBRIDGENE8OUT_BRIDGEEXTEND = 2421 AIURTEMPLEBRIDGENE8_BRIDGERETRACT = 2423 AIURTEMPLEBRIDGENE10OUT_BRIDGEEXTEND = 2425 AIURTEMPLEBRIDGENE10_BRIDGERETRACT = 2427 AIURTEMPLEBRIDGENE12OUT_BRIDGEEXTEND = 2429 AIURTEMPLEBRIDGENE12_BRIDGERETRACT = 2431 AIURTEMPLEBRIDGENW8OUT_BRIDGEEXTEND = 2433 AIURTEMPLEBRIDGENW8_BRIDGERETRACT = 2435 AIURTEMPLEBRIDGENW10OUT_BRIDGEEXTEND = 2437 AIURTEMPLEBRIDGENW10_BRIDGERETRACT = 2439 AIURTEMPLEBRIDGENW12OUT_BRIDGEEXTEND = 2441 AIURTEMPLEBRIDGENW12_BRIDGERETRACT = 2443 SHAKURASLIGHTBRIDGENE8OUT_BRIDGEEXTEND = 2445 SHAKURASLIGHTBRIDGENE8_BRIDGERETRACT = 2447 SHAKURASLIGHTBRIDGENE10OUT_BRIDGEEXTEND = 2449 SHAKURASLIGHTBRIDGENE10_BRIDGERETRACT = 2451 SHAKURASLIGHTBRIDGENE12OUT_BRIDGEEXTEND = 2453 SHAKURASLIGHTBRIDGENE12_BRIDGERETRACT = 2455 SHAKURASLIGHTBRIDGENW8OUT_BRIDGEEXTEND = 2457 SHAKURASLIGHTBRIDGENW8_BRIDGERETRACT = 2459 SHAKURASLIGHTBRIDGENW10OUT_BRIDGEEXTEND = 2461 SHAKURASLIGHTBRIDGENW10_BRIDGERETRACT = 2463 SHAKURASLIGHTBRIDGENW12OUT_BRIDGEEXTEND = 2465 SHAKURASLIGHTBRIDGENW12_BRIDGERETRACT = 2467 VOIDMPIMMORTALREVIVEREBUILD_IMMORTAL = 2469 VOIDMPIMMORTALREVIVEDEATH_IMMORTAL = 2471 ARBITERMPSTASISFIELD_ARBITERMPSTASISFIELD = 2473 ARBITERMPRECALL_ARBITERMPRECALL = 2475 CORSAIRMPDISRUPTIONWEB_CORSAIRMPDISRUPTIONWEB = 2477 MORPHTOGUARDIANMP_MORPHTOGUARDIANMP = 2479 MORPHTOGUARDIANMP_CANCEL = 2480 MORPHTODEVOURERMP_MORPHTODEVOURERMP = 2481 MORPHTODEVOURERMP_CANCEL = 2482 DEFILERMPCONSUME_DEFILERMPCONSUME = 2483 DEFILERMPDARKSWARM_DEFILERMPDARKSWARM = 2485 DEFILERMPPLAGUE_DEFILERMPPLAGUE = 2487 DEFILERMPBURROW_BURROWDOWN = 2489 DEFILERMPBURROW_CANCEL = 2490 DEFILERMPUNBURROW_BURROWUP = 2491 QUEENMPENSNARE_QUEENMPENSNARE = 2493 QUEENMPSPAWNBROODLINGS_QUEENMPSPAWNBROODLINGS = 2495 QUEENMPINFESTCOMMANDCENTER_QUEENMPINFESTCOMMANDCENTER = 2497 LIGHTNINGBOMB_LIGHTNINGBOMB = 2499 GRAPPLE_GRAPPLE = 2501 ORACLESTASISTRAP_ORACLEBUILDSTASISTRAP = 2503 BUILD_STASISTRAP = 2505 CANCEL_STASISTRAP = 2535 ORACLESTASISTRAPACTIVATE_ACTIVATESTASISWARD = 2536 SELFREPAIR_SELFREPAIR = 2538 SELFREPAIR_CANCEL = 2539 AGGRESSIVEMUTATION_AGGRESSIVEMUTATION = 2540 PARASITICBOMB_PARASITICBOMB = 2542 ADEPTPHASESHIFT_ADEPTPHASESHIFT = 2544 PURIFICATIONNOVAMORPH_PURIFICATIONNOVA = 2546 PURIFICATIONNOVAMORPHBACK_PURIFICATIONNOVA = 2548 BEHAVIOR_HOLDFIREON_LURKER = 2550 BEHAVIOR_HOLDFIREOFF_LURKER = 2552 LIBERATORMORPHTOAG_LIBERATORAGMODE = 2554 LIBERATORMORPHTOAA_LIBERATORAAMODE = 2556 MORPH_LIBERATORAGMODE = 2558 MORPH_LIBERATORAAMODE = 2560 TIMESTOP_TIMESTOP = 2562 TIMESTOP_CANCEL = 2563 AIURLIGHTBRIDGEABANDONEDNE8OUT_BRIDGEEXTEND = 2564 AIURLIGHTBRIDGEABANDONEDNE8_BRIDGERETRACT = 2566 AIURLIGHTBRIDGEABANDONEDNE10OUT_BRIDGEEXTEND = 2568 AIURLIGHTBRIDGEABANDONEDNE10_BRIDGERETRACT = 2570 AIURLIGHTBRIDGEABANDONEDNE12OUT_BRIDGEEXTEND = 2572 AIURLIGHTBRIDGEABANDONEDNE12_BRIDGERETRACT = 2574 AIURLIGHTBRIDGEABANDONEDNW8OUT_BRIDGEEXTEND = 2576 AIURLIGHTBRIDGEABANDONEDNW8_BRIDGERETRACT = 2578 AIURLIGHTBRIDGEABANDONEDNW10OUT_BRIDGEEXTEND = 2580 AIURLIGHTBRIDGEABANDONEDNW10_BRIDGERETRACT = 2582 AIURLIGHTBRIDGEABANDONEDNW12OUT_BRIDGEEXTEND = 2584 AIURLIGHTBRIDGEABANDONEDNW12_BRIDGERETRACT = 2586 KD8CHARGE_KD8CHARGE = 2588 PENETRATINGSHOT_PENETRATINGSHOT = 2590 CLOAKINGDRONE_CLOAKINGDRONE = 2592 CANCEL_ADEPTPHASESHIFT = 2594 CANCEL_ADEPTSHADEPHASESHIFT = 2596 SLAYNELEMENTALGRAB_SLAYNELEMENTALGRAB = 2598 MORPHTOCOLLAPSIBLEPURIFIERTOWERDEBRIS_MORPHTOCOLLAPSIBLEPURIFIERTOWERDEBRIS = 2600 MORPHTOCOLLAPSIBLEPURIFIERTOWERDEBRIS_CANCEL = 2601 PORTCITY_BRIDGE_UNITNE8OUT_BRIDGEEXTEND = 2602 PORTCITY_BRIDGE_UNITNE8_BRIDGERETRACT = 2604 PORTCITY_BRIDGE_UNITSE8OUT_BRIDGEEXTEND = 2606 PORTCITY_BRIDGE_UNITSE8_BRIDGERETRACT = 2608 PORTCITY_BRIDGE_UNITNW8OUT_BRIDGEEXTEND = 2610 PORTCITY_BRIDGE_UNITNW8_BRIDGERETRACT = 2612 PORTCITY_BRIDGE_UNITSW8OUT_BRIDGEEXTEND = 2614 PORTCITY_BRIDGE_UNITSW8_BRIDGERETRACT = 2616 PORTCITY_BRIDGE_UNITNE10OUT_BRIDGEEXTEND = 2618 PORTCITY_BRIDGE_UNITNE10_BRIDGERETRACT = 2620 PORTCITY_BRIDGE_UNITSE10OUT_BRIDGEEXTEND = 2622 PORTCITY_BRIDGE_UNITSE10_BRIDGERETRACT = 2624 PORTCITY_BRIDGE_UNITNW10OUT_BRIDGEEXTEND = 2626 PORTCITY_BRIDGE_UNITNW10_BRIDGERETRACT = 2628 PORTCITY_BRIDGE_UNITSW10OUT_BRIDGEEXTEND = 2630 PORTCITY_BRIDGE_UNITSW10_BRIDGERETRACT = 2632 PORTCITY_BRIDGE_UNITNE12OUT_BRIDGEEXTEND = 2634 PORTCITY_BRIDGE_UNITNE12_BRIDGERETRACT = 2636 PORTCITY_BRIDGE_UNITSE12OUT_BRIDGEEXTEND = 2638 PORTCITY_BRIDGE_UNITSE12_BRIDGERETRACT = 2640 PORTCITY_BRIDGE_UNITNW12OUT_BRIDGEEXTEND = 2642 PORTCITY_BRIDGE_UNITNW12_BRIDGERETRACT = 2644 PORTCITY_BRIDGE_UNITSW12OUT_BRIDGEEXTEND = 2646 PORTCITY_BRIDGE_UNITSW12_BRIDGERETRACT = 2648 PORTCITY_BRIDGE_UNITN8OUT_BRIDGEEXTEND = 2650 PORTCITY_BRIDGE_UNITN8_BRIDGERETRACT = 2652 PORTCITY_BRIDGE_UNITS8OUT_BRIDGEEXTEND = 2654 PORTCITY_BRIDGE_UNITS8_BRIDGERETRACT = 2656 PORTCITY_BRIDGE_UNITE8OUT_BRIDGEEXTEND = 2658 PORTCITY_BRIDGE_UNITE8_BRIDGERETRACT = 2660 PORTCITY_BRIDGE_UNITW8OUT_BRIDGEEXTEND = 2662 PORTCITY_BRIDGE_UNITW8_BRIDGERETRACT = 2664 PORTCITY_BRIDGE_UNITN10OUT_BRIDGEEXTEND = 2666 PORTCITY_BRIDGE_UNITN10_BRIDGERETRACT = 2668 PORTCITY_BRIDGE_UNITS10OUT_BRIDGEEXTEND = 2670 PORTCITY_BRIDGE_UNITS10_BRIDGERETRACT = 2672 PORTCITY_BRIDGE_UNITE10OUT_BRIDGEEXTEND = 2674 PORTCITY_BRIDGE_UNITE10_BRIDGERETRACT = 2676 PORTCITY_BRIDGE_UNITW10OUT_BRIDGEEXTEND = 2678 PORTCITY_BRIDGE_UNITW10_BRIDGERETRACT = 2680 PORTCITY_BRIDGE_UNITN12OUT_BRIDGEEXTEND = 2682 PORTCITY_BRIDGE_UNITN12_BRIDGERETRACT = 2684 PORTCITY_BRIDGE_UNITS12OUT_BRIDGEEXTEND = 2686 PORTCITY_BRIDGE_UNITS12_BRIDGERETRACT = 2688 PORTCITY_BRIDGE_UNITE12OUT_BRIDGEEXTEND = 2690 PORTCITY_BRIDGE_UNITE12_BRIDGERETRACT = 2692 PORTCITY_BRIDGE_UNITW12OUT_BRIDGEEXTEND = 2694 PORTCITY_BRIDGE_UNITW12_BRIDGERETRACT = 2696 TEMPESTDISRUPTIONBLAST_TEMPESTDISRUPTIONBLAST = 2698 CANCEL_TEMPESTDISRUPTIONBLAST = 2699 EFFECT_SHADOWSTRIDE = 2700 LAUNCHINTERCEPTORS_LAUNCHINTERCEPTORS = 2702 EFFECT_SPAWNLOCUSTS = 2704 LOCUSTMPFLYINGSWOOPATTACK_LOCUSTMPFLYINGSWOOP = 2706 MORPH_OVERLORDTRANSPORT = 2708 CANCEL_MORPHOVERLORDTRANSPORT = 2709 BYPASSARMOR_BYPASSARMOR = 2710 BYPASSARMORDRONECU_BYPASSARMORDRONECU = 2712 EFFECT_GHOSTSNIPE = 2714 CHANNELSNIPE_CANCEL = 2715 PURIFYMORPHPYLON_MOTHERSHIPCOREWEAPON = 2716 PURIFYMORPHPYLONBACK_MOTHERSHIPCOREWEAPON = 2718 RESEARCH_SHADOWSTRIKE = 2720 HEAL_MEDICHEAL = 2750 LURKERASPECT_LURKER = 2752 LURKERASPECT_CANCEL = 2753 BURROWLURKERDOWN_BURROWDOWN = 2754 BURROWLURKERDOWN_CANCEL = 2755 BURROWLURKERUP_BURROWUP = 2756 D8CHARGE_D8CHARGE = 2758 DEFENSIVEMATRIX_DEFENSIVEMATRIX = 2760 MISSILEPODS_MISSILEPODS = 2762 LOKIMISSILEPODS_MISSILEPODS = 2764 HUTTRANSPORT_HUTLOAD = 2766 HUTTRANSPORT_HUTUNLOADALL = 2767 MORPHTOTECHREACTOR_MORPHTOTECHREACTOR = 2771 LEVIATHANSPAWNBROODLORD_SPAWNBROODLORD = 2773 SS_CARRIERBOSSATTACKLAUNCH_SS_SHOOTING = 2775 SS_CARRIERSPAWNINTERCEPTOR_SS_CARRIERSPAWNINTERCEPTOR = 2777 SS_CARRIERBOSSATTACKTARGET_SS_SHOOTING = 2779 SS_FIGHTERBOMB_SS_FIGHTERBOMB = 2781 SS_LIGHTNINGPROJECTORTOGGLE_SS_LIGHTNINGPROJECTORTOGGLE = 2783 SS_PHOENIXSHOOTING_SS_SHOOTING = 2785 SS_POWERUPMORPHTOBOMB_SS_POWERUPMORPHTOBOMB = 2787 SS_BATTLECRUISERMISSILEATTACK_SS_SHOOTING = 2789 SS_LEVIATHANSPAWNBOMBS_SS_LEVIATHANSPAWNBOMBS = 2791 SS_BATTLECRUISERHUNTERSEEKERATTACK_SS_SHOOTING = 2793 SS_POWERUPMORPHTOHEALTH_SS_POWERUPMORPHTOHEALTH = 2795 SS_LEVIATHANTENTACLEATTACKL1NODELAY_SS_LEVIATHANTENTACLEATTACKL1NODELAY = 2797 SS_LEVIATHANTENTACLEATTACKL2NODELAY_SS_LEVIATHANTENTACLEATTACKL2NODELAY = 2799 SS_LEVIATHANTENTACLEATTACKR1NODELAY_SS_LEVIATHANTENTACLEATTACKR1NODELAY = 2801 SS_LEVIATHANTENTACLEATTACKR2NODELAY_SS_LEVIATHANTENTACLEATTACKR2NODELAY = 2803 SS_SCIENCEVESSELTELEPORT_ZERATULBLINK = 2805 SS_TERRATRONBEAMATTACK_SS_TERRATRONBEAMATTACK = 2807 SS_TERRATRONSAWATTACK_SS_TERRATRONSAWATTACK = 2809 SS_WRAITHATTACK_SS_SHOOTING = 2811 SS_SWARMGUARDIANATTACK_SS_SHOOTING = 2813 SS_POWERUPMORPHTOSIDEMISSILES_SS_POWERUPMORPHTOSIDEMISSILES = 2815 SS_POWERUPMORPHTOSTRONGERMISSILES_SS_POWERUPMORPHTOSTRONGERMISSILES = 2817 SS_SCOUTATTACK_SS_SHOOTING = 2819 SS_INTERCEPTORATTACK_SS_SHOOTING = 2821 SS_CORRUPTORATTACK_SS_SHOOTING = 2823 SS_LEVIATHANTENTACLEATTACKL2_SS_LEVIATHANTENTACLEATTACKL2 = 2825 SS_LEVIATHANTENTACLEATTACKR1_SS_LEVIATHANTENTACLEATTACKR1 = 2827 SS_LEVIATHANTENTACLEATTACKL1_SS_LEVIATHANTENTACLEATTACKL1 = 2829 SS_LEVIATHANTENTACLEATTACKR2_SS_LEVIATHANTENTACLEATTACKR2 = 2831 SS_SCIENCEVESSELATTACK_SS_SHOOTING = 2833 HEALREDIRECT_HEALREDIRECT = 2835 LURKERASPECTFROMHYDRALISKBURROWED_LURKERFROMHYDRALISKBURROWED = 2836 LURKERASPECTFROMHYDRALISKBURROWED_CANCEL = 2837 UPGRADETOLURKERDEN_LURKERDEN = 2838 UPGRADETOLURKERDEN_CANCEL = 2839 ADVANCEDCONSTRUCTION_CANCEL = 2840 BUILDINPROGRESSNONCANCELLABLE_CANCEL = 2842 INFESTEDVENTSPAWNCORRUPTOR_SPAWNCORRUPTOR = 2844 INFESTEDVENTSPAWNBROODLORD_SPAWNBROODLORD = 2846 IRRADIATE_IRRADIATE = 2848 IRRADIATE_CANCEL = 2849 INFESTEDVENTSPAWNMUTALISK_LEVIATHANSPAWNMUTALISK = 2850 MAKEVULTURESPIDERMINES_SPIDERMINEREPLENISH = 2852 MEDIVACDOUBLEBEAMHEAL_HEAL = 2872 MINDCONTROL_MINDCONTROL = 2874 OBLITERATE_OBLITERATE = 2876 VOODOOSHIELD_VOODOOSHIELD = 2878 RELEASEMINION_RELEASEMINION = 2880 ULTRASONICPULSE_ULTRASONICPULSE = 2882 ARCHIVESEAL_ARCHIVESEAL = 2884 ARTANISVORTEX_VORTEX = 2886 ARTANISWORMHOLETRANSIT_WORMHOLETRANSIT = 2888 BUNKERATTACK_BUNKERATTACK = 2890 BUNKERATTACK_ATTACKTOWARDS = 2891 BUNKERATTACK_ATTACKBARRAGE = 2892 BUNKERSTOP_STOPBUNKER = 2893 BUNKERSTOP_HOLDFIRESPECIAL = 2894 CANCELTERRAZINEHARVEST_CANCEL = 2899 LEVIATHANSPAWNMUTALISK_LEVIATHANSPAWNMUTALISK = 2901 PARKCOLONISTVEHICLE_PARKCOLONISTVEHICLE = 2903 STARTCOLONISTVEHICLE_STARTCOLONISTVEHICLE = 2905 CONSUMPTION_CONSUMPTION = 2907 CONSUMEDNA_CONSUMEDNA = 2909 EGGPOP_EGGPOP = 2911 EXPERIMENTALPLASMAGUN_EXPERIMENTALPLASMAGUN = 2913 GATHERSPECIALOBJECT_GATHERSPECIALOBJECT = 2915 KERRIGANSEARCH_KERRIGANSEARCH = 2917 LOKIUNDOCK_LIFT = 2919 MINDBLAST_MINDBLAST = 2921 MORPHTOINFESTEDCIVILIAN_MORPHTOINFESTEDCIVILIAN = 2923 QUEENSHOCKWAVE_QUEENSHOCKWAVE = 2925 TAURENOUTHOUSELIFTOFF_TAURENOUTHOUSEFLY = 2927 TAURENOUTHOUSETRANSPORT_LOADTAURENOUTHOUSE = 2929 TAURENOUTHOUSETRANSPORT_UNLOADTAURENOUTHOUSE = 2930 TYCHUS03OMEGASTORM_OMEGASTORM = 2934 RAYNORSNIPE_RAYNORSNIPE = 2936 BONESHEAL_BONESHEAL = 2938 BONESTOSSGRENADE_TOSSGRENADETYCHUS = 2940 HERCULESTRANSPORT_MEDIVACLOAD = 2942 HERCULESTRANSPORT_MEDIVACUNLOADALL = 2944 SPECOPSDROPSHIPTRANSPORT_MEDIVACLOAD = 2947 SPECOPSDROPSHIPTRANSPORT_MEDIVACUNLOADALL = 2949 DUSKWINGBANSHEECLOAKINGFIELD_CLOAKONBANSHEE = 2952 DUSKWINGBANSHEECLOAKINGFIELD_CLOAKOFF = 2953 HYPERIONYAMATOSPECIAL_HYPERIONYAMATOGUN = 2954 INFESTABLEHUTTRANSPORT_HUTLOAD = 2956 INFESTABLEHUTTRANSPORT_HUTUNLOADALL = 2957 DUTCHPLACETURRET_DUTCHPLACETURRET = 2961 BURROWINFESTEDCIVILIANDOWN_BURROWDOWN = 2963 BURROWINFESTEDCIVILIANUP_BURROWUP = 2965 SELENDISHANGAR_INTERCEPTOR = 2967 FORCEFIELDBEAM_FORCEFIELDBEAM = 2987 SIEGEBREAKERSIEGE_SIEGEMODE = 2989 SIEGEBREAKERUNSIEGE_UNSIEGE = 2991 SOULCHANNEL_SOULCHANNEL = 2993 SOULCHANNEL_CANCEL = 2994 PERDITIONTURRETBURROW_PERDITIONTURRETBURROW = 2995 PERDITIONTURRETUNBURROW_PERDITIONTURRETUNBURROW = 2997 SENTRYGUNBURROW_BURROWTURRET = 2999 SENTRYGUNUNBURROW_UNBURROWTURRET = 3001 SPIDERMINEUNBURROWRANGEDUMMY_SPIDERMINEUNBURROWRANGEDUMMY = 3003 GRAVITONPRISON_GRAVITONPRISON = 3005 IMPLOSION_IMPLOSION = 3007 OMEGASTORM_OMEGASTORM = 3009 PSIONICSHOCKWAVE_PSIONICSHOCKWAVE = 3011 HYBRIDFAOESTUN_HYBRIDFAOESTUN = 3013 SUMMONMERCENARIES_HIREKELMORIANMINERS = 3015 SUMMONMERCENARIES_HIREDEVILDOGS = 3016 SUMMONMERCENARIES_HIRESPARTANCOMPANY = 3017 SUMMONMERCENARIES_HIREHAMMERSECURITIES = 3018 SUMMONMERCENARIES_HIRESIEGEBREAKERS = 3019 SUMMONMERCENARIES_HIREHELSANGELS = 3020 SUMMONMERCENARIES_HIREDUSKWING = 3021 SUMMONMERCENARIES_HIREDUKESREVENGE = 3022 SUMMONMERCENARIESPH_HIREKELMORIANMINERSPH = 3045 ENERGYNOVA_ENERGYNOVA = 3075 THEMOROSDEVICE_THEMOROSDEVICE = 3077 TOSSGRENADE_TOSSGRENADE = 3079 VOIDSEEKERTRANSPORT_MEDIVACLOAD = 3081 VOIDSEEKERTRANSPORT_MEDIVACUNLOADALL = 3083 TERRANBUILDDROP_SUPPLYDEPOTDROP = 3086 TERRANBUILDDROP_CANCEL = 3116 ODINNUCLEARSTRIKE_ODINNUKECALLDOWN = 3117 ODINNUCLEARSTRIKE_CANCEL = 3118 ODINWRECKAGE_ODIN = 3119 RESEARCHLABTRANSPORT_HUTLOAD = 3121 RESEARCHLABTRANSPORT_HUTUNLOADALL = 3122 COLONYSHIPTRANSPORT_MEDIVACLOAD = 3126 COLONYSHIPTRANSPORT_MEDIVACUNLOADALL = 3128 COLONYINFESTATION_COLONYINFESTATION = 3131 DOMINATION_DOMINATION = 3133 DOMINATION_CANCEL = 3134 KARASSPLASMASURGE_KARASSPLASMASURGE = 3135 KARASSPSISTORM_PSISTORM = 3137 HYBRIDBLINK_ZERATULBLINK = 3139 HYBRIDCPLASMABLAST_HYBRIDCPLASMABLAST = 3141 HEROARMNUKE_NUKEARM = 3143 HERONUCLEARSTRIKE_NUKECALLDOWN = 3163 HERONUCLEARSTRIKE_CANCEL = 3164 ODINBARRAGE_ODINBARRAGE = 3165 ODINBARRAGE_CANCEL = 3166 PURIFIERTOGGLEPOWER_PURIFIERPOWERDOWN = 3167 PURIFIERTOGGLEPOWER_PURIFIERPOWERUP = 3168 PHASEMINEBLAST_PHASEMINEBLAST = 3169 VOIDSEEKERPHASEMINEBLAST_PHASEMINEBLAST = 3171 TRANSPORTTRUCKTRANSPORT_TRANSPORTTRUCKLOAD = 3173 TRANSPORTTRUCKTRANSPORT_TRANSPORTTRUCKUNLOADALL = 3174 VAL03QUEENOFBLADESBURROW_BURROWDOWN = 3178 VAL03QUEENOFBLADESDEEPTUNNEL_DEEPTUNNEL = 3180 VAL03QUEENOFBLADESUNBURROW_BURROWUP = 3182 VULTURESPIDERMINEBURROW_VULTURESPIDERMINEBURROW = 3184 VULTURESPIDERMINEUNBURROW_VULTURESPIDERMINEUNBURROW = 3186 LOKIYAMATO_LOKIYAMATOGUN = 3188 DUKESREVENGEYAMATO_YAMATOGUN = 3190 ZERATULBLINK_ZERATULBLINK = 3192 ROGUEGHOSTCLOAK_CLOAKONSPECTRE = 3194 ROGUEGHOSTCLOAK_CLOAKOFF = 3195 VULTURESPIDERMINES_SPIDERMINE = 3196 VULTUREQUEUE3_CANCEL = 3198 VULTUREQUEUE3_CANCELSLOT = 3199 SUPERWARPGATETRAIN_ZEALOT = 3200 SUPERWARPGATETRAIN_STALKER = 3201 SUPERWARPGATETRAIN_IMMORTAL = 3202 SUPERWARPGATETRAIN_HIGHTEMPLAR = 3203 SUPERWARPGATETRAIN_DARKTEMPLAR = 3204 SUPERWARPGATETRAIN_SENTRY = 3205 SUPERWARPGATETRAIN_CARRIER = 3206 SUPERWARPGATETRAIN_PHOENIX = 3207 SUPERWARPGATETRAIN_VOIDRAY = 3208 SUPERWARPGATETRAIN_ARCHON = 3209 SUPERWARPGATETRAIN_WARPINZERATUL = 3210 SUPERWARPGATETRAIN_WARPINURUN = 3211 SUPERWARPGATETRAIN_WARPINMOHANDAR = 3212 SUPERWARPGATETRAIN_WARPINSELENDIS = 3213 SUPERWARPGATETRAIN_WARPINSCOUT = 3214 SUPERWARPGATETRAIN_COLOSSUS = 3215 SUPERWARPGATETRAIN_WARPPRISM = 3216 BURROWOMEGALISKDOWN_BURROWDOWN = 3220 BURROWOMEGALISKUP_BURROWUP = 3222 BURROWINFESTEDABOMINATIONDOWN_BURROWDOWN = 3224 BURROWINFESTEDABOMINATIONUP_BURROWUP = 3226 BURROWHUNTERKILLERDOWN_BURROWDOWN = 3228 BURROWHUNTERKILLERDOWN_CANCEL = 3229 BURROWHUNTERKILLERUP_BURROWUP = 3230 NOVASNIPE_NOVASNIPE = 3232 VORTEXPURIFIER_VORTEX = 3234 TALDARIMVORTEX_VORTEX = 3236 PURIFIERPLANETCRACKER_PLANETCRACKER = 3238 BURROWINFESTEDTERRANCAMPAIGNDOWN_BURROWDOWN = 3240 BURROWINFESTEDTERRANCAMPAIGNUP_BURROWUP = 3242 INFESTEDMONSTERTRAIN_INFESTEDCIVILIAN = 3244 INFESTEDMONSTERTRAIN_INFESTEDTERRANCAMPAIGN = 3245 INFESTEDMONSTERTRAIN_INFESTEDABOMINATION = 3246 BIODOMETRANSPORT_BIODOMELOAD = 3274 BIODOMETRANSPORT_BIODOMEUNLOADALL = 3275 CHECKSTATION_CHECKSTATION = 3279 CHECKSTATIONDIAGONALBLUR_CHECKSTATIONDIAGONALBLUR = 3281 CHECKSTATIONDIAGONALULBR_CHECKSTATIONDIAGONALULBR = 3283 CHECKSTATIONVERTICAL_CHECKSTATIONVERTICAL = 3285 CHECKSTATIONOPENED_CHECKSTATIONOPENED = 3287 CHECKSTATIONDIAGONALBLUROPENED_CHECKSTATIONDIAGONALBLUROPENED = 3289 CHECKSTATIONDIAGONALULBROPENED_CHECKSTATIONDIAGONALULBROPENED = 3291 CHECKSTATIONVERTICALOPENED_CHECKSTATIONVERTICALOPENED = 3293 ATTACKALLOWSINVULNERABLE_ATTACKALLOWSINVULNERABLE = 3295 ATTACKALLOWSINVULNERABLE_ATTACKTOWARDS = 3296 ATTACKALLOWSINVULNERABLE_ATTACKBARRAGE = 3297 ZERATULSTUN_ZERATULSTUN = 3298 WRAITHCLOAK_WRAITHCLOAK = 3300 WRAITHCLOAK_CLOAKOFF = 3301 TECHREACTORMORPH_TECHREACTORMORPH = 3302 BARRACKSTECHREACTORMORPH_TECHLABBARRACKS = 3304 FACTORYTECHREACTORMORPH_TECHLABFACTORY = 3306 STARPORTTECHREACTORMORPH_TECHLABSTARPORT = 3308 SS_FIGHTERSHOOTING_SS_SHOOTING = 3310 RAYNORC4_PLANTC4CHARGE = 3312 DUKESREVENGEDEFENSIVEMATRIX_DEFENSIVEMATRIX = 3314 DUKESREVENGEMISSILEPODS_MISSILEPODS = 3316 THORWRECKAGE_THOR = 3318 _330MMBARRAGECANNONS_330MMBARRAGECANNONS = 3320 _330MMBARRAGECANNONS_CANCEL = 3321 THORREBORN_THOR = 3322 THORREBORN_CANCEL = 3323 SPECTRENUKE_SPECTRENUKECALLDOWN = 3324 SPECTRENUKE_CANCEL = 3325 SPECTRENUKESILOARMMAGAZINE_SPECTRENUKESILOARMMAGAZINE = 3326 SPECTRENUKESILOARMMAGAZINE_SPECTRENUKEARM = 3327 COLONISTSHIPLIFTOFF_LIFT = 3346 COLONISTSHIPLAND_LAND = 3348 BIODOMECOMMANDLIFTOFF_LIFT = 3350 BIODOMECOMMANDLAND_LAND = 3352 HERCULESLIFTOFF_LIFT = 3354 HERCULESLAND_HERCULESLAND = 3356 LIGHTBRIDGEOFF_LIGHTBRIDGEOFF = 3358 LIGHTBRIDGEON_LIGHTBRIDGEON = 3360 LIBRARYDOWN_LIBRARYDOWN = 3362 LIBRARYUP_LIBRARYUP = 3364 TEMPLEDOORDOWN_TEMPLEDOORDOWN = 3366 TEMPLEDOORUP_TEMPLEDOORUP = 3368 TEMPLEDOORDOWNURDL_TEMPLEDOORDOWNURDL = 3370 TEMPLEDOORUPURDL_TEMPLEDOORUPURDL = 3372 PSYTROUSOXIDE_PSYTROUSOXIDEON = 3374 PSYTROUSOXIDE_PSYTROUSOXIDEOFF = 3375 VOIDSEEKERDOCK_VOIDSEEKERDOCK = 3376 BIOPLASMIDDISCHARGE_BIOPLASMIDDISCHARGE = 3378 WRECKINGCREWASSAULTMODE_ASSAULTMODE = 3380 WRECKINGCREWFIGHTERMODE_FIGHTERMODE = 3382 BIOSTASIS_BIOSTASIS = 3384 COLONISTTRANSPORTTRANSPORT_COLONISTTRANSPORTLOAD = 3386 COLONISTTRANSPORTTRANSPORT_COLONISTTRANSPORTUNLOADALL = 3387 DROPTOSUPPLYDEPOT_RAISE = 3391 REFINERYTOAUTOMATEDREFINERY_RAISE = 3393 HELIOSCRASHMORPH_CRASHMORPH = 3395 NANOREPAIR_HEAL = 3397 PICKUP_PICKUP = 3399 PICKUPARCADE_PICKUP = 3401 PICKUPGAS100_PICKUPGAS100 = 3403 PICKUPMINERALS100_PICKUPMINERALS100 = 3405 PICKUPHEALTH25_PICKUPHEALTH25 = 3407 PICKUPHEALTH50_PICKUPHEALTH50 = 3409 PICKUPHEALTH100_PICKUPHEALTH100 = 3411 PICKUPHEALTHFULL_PICKUPHEALTHFULL = 3413 PICKUPENERGY25_PICKUPENERGY25 = 3415 PICKUPENERGY50_PICKUPENERGY50 = 3417 PICKUPENERGY100_PICKUPENERGY100 = 3419 PICKUPENERGYFULL_PICKUPENERGYFULL = 3421 TAURENSTIMPACK_STIM = 3423 TESTINVENTORY_TESTINVENTORY = 3425 TESTPAWN_TESTPAWN = 3434 TESTREVIVE_SCV = 3454 TESTSELL_TESTSELL = 3484 TESTINTERACT_DESIGNATE = 3514 CLIFFDOOROPEN0_SPACEPLATFORMDOOROPEN = 3515 CLIFFDOORCLOSE0_SPACEPLATFORMDOORCLOSE = 3517 CLIFFDOOROPEN1_SPACEPLATFORMDOOROPEN = 3519 CLIFFDOORCLOSE1_SPACEPLATFORMDOORCLOSE = 3521 DESTRUCTIBLEGATEDIAGONALBLURLOWERED_GATEOPEN = 3523 DESTRUCTIBLEGATEDIAGONALULBRLOWERED_GATEOPEN = 3525 DESTRUCTIBLEGATESTRAIGHTHORIZONTALBFLOWERED_GATEOPEN = 3527 DESTRUCTIBLEGATESTRAIGHTHORIZONTALLOWERED_GATEOPEN = 3529 DESTRUCTIBLEGATESTRAIGHTVERTICALLFLOWERED_GATEOPEN = 3531 DESTRUCTIBLEGATESTRAIGHTVERTICALLOWERED_GATEOPEN = 3533 DESTRUCTIBLEGATEDIAGONALBLUR_GATECLOSE = 3535 DESTRUCTIBLEGATEDIAGONALULBR_GATECLOSE = 3537 DESTRUCTIBLEGATESTRAIGHTHORIZONTALBF_GATECLOSE = 3539 DESTRUCTIBLEGATESTRAIGHTHORIZONTAL_GATECLOSE = 3541 DESTRUCTIBLEGATESTRAIGHTVERTICALLF_GATECLOSE = 3543 DESTRUCTIBLEGATESTRAIGHTVERTICAL_GATECLOSE = 3545 TESTLEARN_TESTLEARN = 3547 TESTLEVELEDSPELL_YAMATOGUN = 3567 METALGATEDIAGONALBLURLOWERED_GATEOPEN = 3569 METALGATEDIAGONALULBRLOWERED_GATEOPEN = 3571 METALGATESTRAIGHTHORIZONTALBFLOWERED_GATEOPEN = 3573 METALGATESTRAIGHTHORIZONTALLOWERED_GATEOPEN = 3575 METALGATESTRAIGHTVERTICALLFLOWERED_GATEOPEN = 3577 METALGATESTRAIGHTVERTICALLOWERED_GATEOPEN = 3579 METALGATEDIAGONALBLUR_GATECLOSE = 3581 METALGATEDIAGONALULBR_GATECLOSE = 3583 METALGATESTRAIGHTHORIZONTALBF_GATECLOSE = 3585 METALGATESTRAIGHTHORIZONTAL_GATECLOSE = 3587 METALGATESTRAIGHTVERTICALLF_GATECLOSE = 3589 METALGATESTRAIGHTVERTICAL_GATECLOSE = 3591 SECURITYGATEDIAGONALBLURLOWERED_GATEOPEN = 3593 SECURITYGATEDIAGONALULBRLOWERED_GATEOPEN = 3595 SECURITYGATESTRAIGHTHORIZONTALBFLOWERED_GATEOPEN = 3597 SECURITYGATESTRAIGHTHORIZONTALLOWERED_GATEOPEN = 3599 SECURITYGATESTRAIGHTVERTICALLFLOWERED_GATEOPEN = 3601 SECURITYGATESTRAIGHTVERTICALLOWERED_GATEOPEN = 3603 SECURITYGATEDIAGONALBLUR_GATECLOSE = 3605 SECURITYGATEDIAGONALULBR_GATECLOSE = 3607 SECURITYGATESTRAIGHTHORIZONTALBF_GATECLOSE = 3609 SECURITYGATESTRAIGHTHORIZONTAL_GATECLOSE = 3611 SECURITYGATESTRAIGHTVERTICALLF_GATECLOSE = 3613 SECURITYGATESTRAIGHTVERTICAL_GATECLOSE = 3615 CHANGESHRINETERRAN_CHANGESHRINETERRAN = 3617 CHANGESHRINEPROTOSS_CHANGESHRINEPROTOSS = 3619 SPECTREHOLDFIRE_SPECTREHOLDFIRE = 3621 SPECTREWEAPONSFREE_WEAPONSFREE = 3623 GWALEARN_TESTLEARN = 3625 REAPERPLACEMENTMORPH_REAPERPLACEMENTMORPH = 3645 LIGHTBRIDGEOFFTOPRIGHT_LIGHTBRIDGEOFF = 3647 LIGHTBRIDGEONTOPRIGHT_LIGHTBRIDGEON = 3649 TESTHEROGRAB_GRABZERGLING = 3651 TESTHEROTHROW_THROWZERGLING = 3653 TESTHERODEBUGMISSILEABILITY_TESTHERODEBUGMISSILEABILITY = 3655 TESTHERODEBUGTRACKINGABILITY_TESTHERODEBUGTRACKINGABILITY = 3657 TESTHERODEBUGTRACKINGABILITY_CANCEL = 3658 CANCEL = 3659 HALT = 3660 BURROWDOWN = 3661 BURROWUP = 3662 LOADALL = 3663 UNLOADALL = 3664 STOP = 3665 HARVEST_GATHER = 3666 HARVEST_RETURN = 3667 LOAD = 3668 UNLOADALLAT = 3669 CANCEL_LAST = 3671 CANCEL_SLOT = 3672 RALLY_UNITS = 3673 ATTACK = 3674 EFFECT_STIM = 3675 BEHAVIOR_CLOAKON = 3676 BEHAVIOR_CLOAKOFF = 3677 LAND = 3678 LIFT = 3679 MORPH_ROOT = 3680 MORPH_UPROOT = 3681 BUILD_TECHLAB = 3682 BUILD_REACTOR = 3683 EFFECT_SPRAY = 3684 EFFECT_REPAIR = 3685 EFFECT_MASSRECALL = 3686 EFFECT_BLINK = 3687 BEHAVIOR_HOLDFIREON = 3688 BEHAVIOR_HOLDFIREOFF = 3689 RALLY_WORKERS = 3690 BUILD_CREEPTUMOR = 3691 RESEARCH_PROTOSSAIRARMOR = 3692 RESEARCH_PROTOSSAIRWEAPONS = 3693 RESEARCH_PROTOSSGROUNDARMOR = 3694 RESEARCH_PROTOSSGROUNDWEAPONS = 3695 RESEARCH_PROTOSSSHIELDS = 3696 RESEARCH_TERRANINFANTRYARMOR = 3697 RESEARCH_TERRANINFANTRYWEAPONS = 3698 RESEARCH_TERRANSHIPWEAPONS = 3699 RESEARCH_TERRANVEHICLEANDSHIPPLATING = 3700 RESEARCH_TERRANVEHICLEWEAPONS = 3701 RESEARCH_ZERGFLYERARMOR = 3702 RESEARCH_ZERGFLYERATTACK = 3703 RESEARCH_ZERGGROUNDARMOR = 3704 RESEARCH_ZERGMELEEWEAPONS = 3705 RESEARCH_ZERGMISSILEWEAPONS = 3706 CANCEL_VOIDRAYPRISMATICALIGNMENT = 3707 RESEARCH_ADAPTIVETALONS = 3709 LURKERDENRESEARCH_RESEARCHLURKERRANGE = 3710 MORPH_OBSERVERMODE = 3739 MORPH_SURVEILLANCEMODE = 3741 MORPH_OVERSIGHTMODE = 3743 MORPH_OVERSEERMODE = 3745 EFFECT_INTERFERENCEMATRIX = 3747 EFFECT_REPAIRDRONE = 3749 EFFECT_REPAIR_REPAIRDRONE = 3751 EFFECT_ANTIARMORMISSILE = 3753 EFFECT_CHRONOBOOSTENERGYCOST = 3755 EFFECT_MASSRECALL_NEXUS = 3757 NEXUSSHIELDRECHARGE_NEXUSSHIELDRECHARGE = 3759 NEXUSSHIELDRECHARGEONPYLON_NEXUSSHIELDRECHARGEONPYLON = 3761 INFESTORENSNARE_INFESTORENSNARE = 3763 EFFECT_RESTORE = 3765 NEXUSSHIELDOVERCHARGE_NEXUSSHIELDOVERCHARGE = 3767 NEXUSSHIELDOVERCHARGEOFF_NEXUSSHIELDOVERCHARGEOFF = 3769 ATTACK_BATTLECRUISER = 3771 BATTLECRUISERATTACK_ATTACKTOWARDS = 3772 BATTLECRUISERATTACK_ATTACKBARRAGE = 3773 BATTLECRUISERATTACKEVALUATOR_MOTHERSHIPCOREATTACK = 3774 MOVE_BATTLECRUISER = 3776 PATROL_BATTLECRUISER = 3777 HOLDPOSITION_BATTLECRUISER = 3778 BATTLECRUISERMOVE_ACQUIREMOVE = 3779 BATTLECRUISERMOVE_TURN = 3780 BATTLECRUISERSTOPEVALUATOR_STOP = 3781 STOP_BATTLECRUISER = 3783 BATTLECRUISERSTOP_HOLDFIRE = 3784 BATTLECRUISERSTOP_CHEER = 3785 BATTLECRUISERSTOP_DANCE = 3786 VIPERPARASITICBOMBRELAY_PARASITICBOMB = 3789 PARASITICBOMBRELAYDODGE_PARASITICBOMB = 3791 HOLDPOSITION = 3793 MOVE = 3794 PATROL = 3795 UNLOADUNIT = 3796 LOADOUTSPRAY_LOADOUTSPRAY1 = 3797 LOADOUTSPRAY_LOADOUTSPRAY2 = 3798 LOADOUTSPRAY_LOADOUTSPRAY3 = 3799 LOADOUTSPRAY_LOADOUTSPRAY4 = 3800 LOADOUTSPRAY_LOADOUTSPRAY5 = 3801 LOADOUTSPRAY_LOADOUTSPRAY6 = 3802 LOADOUTSPRAY_LOADOUTSPRAY7 = 3803 LOADOUTSPRAY_LOADOUTSPRAY8 = 3804 LOADOUTSPRAY_LOADOUTSPRAY9 = 3805 LOADOUTSPRAY_LOADOUTSPRAY10 = 3806 LOADOUTSPRAY_LOADOUTSPRAY11 = 3807 LOADOUTSPRAY_LOADOUTSPRAY12 = 3808 LOADOUTSPRAY_LOADOUTSPRAY13 = 3809 LOADOUTSPRAY_LOADOUTSPRAY14 = 3810 MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPLEFTGREEN_MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPLEFTGREEN = 3966 MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPLEFTGREEN_CANCEL = 3967 MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPRIGHTGREEN_MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPRIGHTGREEN = 3969 MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPRIGHTGREEN_CANCEL = 3970 BATTERYOVERCHARGE_BATTERYOVERCHARGE = 4107 HYDRALISKFRENZY_HYDRALISKFRENZY = 4109 AMORPHOUSARMORCLOUD_AMORPHOUSARMORCLOUD = 4111 SHIELDBATTERYRECHARGEEX5_SHIELDBATTERYRECHARGE = 4113 SHIELDBATTERYRECHARGEEX5_STOP = 4114 MORPHTOBANELING_BANELING = 4121 MORPHTOBANELING_CANCEL = 4122 MOTHERSHIPCLOAK_ORACLECLOAKFIELD = 4124 ENERGYRECHARGE_ENERGYRECHARGE = 4126 SALVAGEEFFECT_SALVAGE = 4128 SALVAGESENSORTOWERREFUND_SALVAGE = 4130 WORKERSTOPIDLEABILITYVESPENE_GATHER = 4132 def __repr__(self) -> str: return f"AbilityId.{self.name}" @classmethod def _missing_(cls, value: int) -> AbilityId: return cls.NULL_NULL for item in AbilityId: globals()[item.name] = item ================================================ FILE: sc2/ids/buff_id.py ================================================ from __future__ import annotations # DO NOT EDIT! # This file was automatically generated by "generate_ids.py" import enum class BuffId(enum.Enum): NULL = 0 RADAR25 = 1 TAUNTB = 2 DISABLEABILS = 3 TRANSIENTMORPH = 4 GRAVITONBEAM = 5 GHOSTCLOAK = 6 BANSHEECLOAK = 7 POWERUSERWARPABLE = 8 VORTEXBEHAVIORENEMY = 9 CORRUPTION = 10 QUEENSPAWNLARVATIMER = 11 GHOSTHOLDFIRE = 12 GHOSTHOLDFIREB = 13 LEECH = 14 LEECHDISABLEABILITIES = 15 EMPDECLOAK = 16 FUNGALGROWTH = 17 GUARDIANSHIELD = 18 SEEKERMISSILETIMEOUT = 19 TIMEWARPPRODUCTION = 20 ETHEREAL = 21 NEURALPARASITE = 22 NEURALPARASITEWAIT = 23 STIMPACKMARAUDER = 24 SUPPLYDROP = 25 _250MMSTRIKECANNONS = 26 STIMPACK = 27 PSISTORM = 28 CLOAKFIELDEFFECT = 29 CHARGING = 30 AIDANGERBUFF = 31 VORTEXBEHAVIOR = 32 SLOW = 33 TEMPORALRIFTUNIT = 34 SHEEPBUSY = 35 CONTAMINATED = 36 TIMESCALECONVERSIONBEHAVIOR = 37 BLINDINGCLOUDSTRUCTURE = 38 COLLAPSIBLEROCKTOWERCONJOINEDSEARCH = 39 COLLAPSIBLEROCKTOWERRAMPDIAGONALCONJOINEDSEARCH = 40 COLLAPSIBLETERRANTOWERCONJOINEDSEARCH = 41 COLLAPSIBLETERRANTOWERRAMPDIAGONALCONJOINEDSEARCH = 42 DIGESTERCREEPSPRAYVISION = 43 INVULNERABILITYSHIELD = 44 MINEDRONECOUNTDOWN = 45 MOTHERSHIPSTASIS = 46 MOTHERSHIPSTASISCASTER = 47 MOTHERSHIPCOREENERGIZEVISUAL = 48 ORACLEREVELATION = 49 GHOSTSNIPEDOT = 50 NEXUSPHASESHIFT = 51 NEXUSINVULNERABILITY = 52 ROUGHTERRAINSEARCH = 53 ROUGHTERRAINSLOW = 54 ORACLECLOAKFIELD = 55 ORACLECLOAKFIELDEFFECT = 56 SCRYERFRIENDLY = 57 SPECTRESHIELD = 58 VIPERCONSUMESTRUCTURE = 59 RESTORESHIELDS = 60 MERCENARYCYCLONEMISSILES = 61 MERCENARYSENSORDISH = 62 MERCENARYSHIELD = 63 SCRYER = 64 STUNROUNDINITIALBEHAVIOR = 65 BUILDINGSHIELD = 66 LASERSIGHT = 67 PROTECTIVEBARRIER = 68 CORRUPTORGROUNDATTACKDEBUFF = 69 BATTLECRUISERANTIAIRDISABLE = 70 BUILDINGSTASIS = 71 STASIS = 72 RESOURCESTUN = 73 MAXIMUMTHRUST = 74 CHARGEUP = 75 CLOAKUNIT = 76 NULLFIELD = 77 RESCUE = 78 BENIGN = 79 LASERTARGETING = 80 ENGAGE = 81 CAPRESOURCE = 82 BLINDINGCLOUD = 83 DOOMDAMAGEDELAY = 84 EYESTALK = 85 BURROWCHARGE = 86 HIDDEN = 87 MINEDRONEDOT = 88 MEDIVACSPEEDBOOST = 89 EXTENDBRIDGEEXTENDINGBRIDGENEWIDE8OUT = 90 EXTENDBRIDGEEXTENDINGBRIDGENWWIDE8OUT = 91 EXTENDBRIDGEEXTENDINGBRIDGENEWIDE10OUT = 92 EXTENDBRIDGEEXTENDINGBRIDGENWWIDE10OUT = 93 EXTENDBRIDGEEXTENDINGBRIDGENEWIDE12OUT = 94 EXTENDBRIDGEEXTENDINGBRIDGENWWIDE12OUT = 95 PHASESHIELD = 96 PURIFY = 97 VOIDSIPHON = 98 ORACLEWEAPON = 99 ANTIAIRWEAPONSWITCHCOOLDOWN = 100 ARBITERMPSTASISFIELD = 101 IMMORTALOVERLOAD = 102 CLOAKINGFIELDTARGETED = 103 LIGHTNINGBOMB = 104 ORACLEPHASESHIFT = 105 RELEASEINTERCEPTORSCOOLDOWN = 106 RELEASEINTERCEPTORSTIMEDLIFEWARNING = 107 RELEASEINTERCEPTORSWANDERDELAY = 108 RELEASEINTERCEPTORSBEACON = 109 ARBITERMPCLOAKFIELDEFFECT = 110 PURIFICATIONNOVA = 111 CORRUPTIONBOMBDAMAGE = 112 CORSAIRMPDISRUPTIONWEB = 113 DISRUPTORPUSH = 114 LIGHTOFAIUR = 115 LOCKON = 116 OVERCHARGE = 117 OVERCHARGEDAMAGE = 118 OVERCHARGESPEEDBOOST = 119 SEEKERMISSILE = 120 TEMPORALFIELD = 121 VOIDRAYSWARMDAMAGEBOOST = 122 VOIDMPIMMORTALREVIVESUPRESSED = 123 DEVOURERMPACIDSPORES = 124 DEFILERMPCONSUME = 125 DEFILERMPDARKSWARM = 126 DEFILERMPPLAGUE = 127 QUEENMPENSNARE = 128 ORACLESTASISTRAPTARGET = 129 SELFREPAIR = 130 AGGRESSIVEMUTATION = 131 PARASITICBOMB = 132 PARASITICBOMBUNITKU = 133 PARASITICBOMBSECONDARYUNITSEARCH = 134 ADEPTDEATHCHECK = 135 LURKERHOLDFIRE = 136 LURKERHOLDFIREB = 137 TIMESTOPSTUN = 138 SLAYNELEMENTALGRABSTUN = 139 PURIFICATIONNOVAPOST = 140 DISABLEINTERCEPTORS = 141 BYPASSARMORDEBUFFONE = 142 BYPASSARMORDEBUFFTWO = 143 BYPASSARMORDEBUFFTHREE = 144 CHANNELSNIPECOMBAT = 145 TEMPESTDISRUPTIONBLASTSTUNBEHAVIOR = 146 GRAVITONPRISON = 147 INFESTORDISEASE = 148 SS_LIGHTNINGPROJECTOR = 149 PURIFIERPLANETCRACKERCHARGE = 150 SPECTRECLOAKING = 151 WRAITHCLOAK = 152 PSYTROUSOXIDE = 153 BANSHEECLOAKCROSSSPECTRUMDAMPENERS = 154 SS_BATTLECRUISERHUNTERSEEKERTIMEOUT = 155 SS_STRONGERENEMYBUFF = 156 SS_TERRATRONARMMISSILETARGETCHECK = 157 SS_MISSILETIMEOUT = 158 SS_LEVIATHANBOMBCOLLISIONCHECK = 159 SS_LEVIATHANBOMBEXPLODETIMER = 160 SS_LEVIATHANBOMBMISSILETARGETCHECK = 161 SS_TERRATRONCOLLISIONCHECK = 162 SS_CARRIERBOSSCOLLISIONCHECK = 163 SS_CORRUPTORMISSILETARGETCHECK = 164 SS_INVULNERABLE = 165 SS_LEVIATHANTENTACLEMISSILETARGETCHECK = 166 SS_LEVIATHANTENTACLEMISSILETARGETCHECKINVERTED = 167 SS_LEVIATHANTENTACLETARGETDEATHDELAY = 168 SS_LEVIATHANTENTACLEMISSILESCANSWAPDELAY = 169 SS_POWERUPDIAGONAL2 = 170 SS_BATTLECRUISERCOLLISIONCHECK = 171 SS_TERRATRONMISSILESPINNERMISSILELAUNCHER = 172 SS_TERRATRONMISSILESPINNERCOLLISIONCHECK = 173 SS_TERRATRONMISSILELAUNCHER = 174 SS_BATTLECRUISERMISSILELAUNCHER = 175 SS_TERRATRONSTUN = 176 SS_VIKINGRESPAWN = 177 SS_WRAITHCOLLISIONCHECK = 178 SS_SCOURGEMISSILETARGETCHECK = 179 SS_SCOURGEDEATH = 180 SS_SWARMGUARDIANCOLLISIONCHECK = 181 SS_FIGHTERBOMBMISSILEDEATH = 182 SS_FIGHTERDRONEDAMAGERESPONSE = 183 SS_INTERCEPTORCOLLISIONCHECK = 184 SS_CARRIERCOLLISIONCHECK = 185 SS_MISSILETARGETCHECKVIKINGDRONE = 186 SS_MISSILETARGETCHECKVIKINGSTRONG1 = 187 SS_MISSILETARGETCHECKVIKINGSTRONG2 = 188 SS_POWERUPHEALTH1 = 189 SS_POWERUPHEALTH2 = 190 SS_POWERUPSTRONG = 191 SS_POWERUPMORPHTOBOMB = 192 SS_POWERUPMORPHTOHEALTH = 193 SS_POWERUPMORPHTOSIDEMISSILES = 194 SS_POWERUPMORPHTOSTRONGERMISSILES = 195 SS_CORRUPTORCOLLISIONCHECK = 196 SS_SCOUTCOLLISIONCHECK = 197 SS_PHOENIXCOLLISIONCHECK = 198 SS_SCOURGECOLLISIONCHECK = 199 SS_LEVIATHANCOLLISIONCHECK = 200 SS_SCIENCEVESSELCOLLISIONCHECK = 201 SS_TERRATRONSAWCOLLISIONCHECK = 202 SS_LIGHTNINGPROJECTORCOLLISIONCHECK = 203 SHIFTDELAY = 204 BIOSTASIS = 205 PERSONALCLOAKINGFREE = 206 EMPDRAIN = 207 MINDBLASTSTUN = 208 _330MMBARRAGECANNONS = 209 VOODOOSHIELD = 210 SPECTRECLOAKINGFREE = 211 ULTRASONICPULSESTUN = 212 IRRADIATE = 213 NYDUSWORMLAVAINSTANTDEATH = 214 PREDATORCLOAKING = 215 PSIDISRUPTION = 216 MINDCONTROL = 217 QUEENKNOCKDOWN = 218 SCIENCEVESSELCLOAKFIELD = 219 SPORECANNONMISSILE = 220 ARTANISTEMPORALRIFTUNIT = 221 ARTANISCLOAKINGFIELDEFFECT = 222 ARTANISVORTEXBEHAVIOR = 223 INCAPACITATED = 224 KARASSPSISTORM = 225 DUTCHMARAUDERSLOW = 226 JUMPSTOMPSTUN = 227 JUMPSTOMPFSTUN = 228 RAYNORMISSILETIMEDLIFE = 229 PSIONICSHOCKWAVEHEIGHTANDSTUN = 230 SHADOWCLONE = 231 AUTOMATEDREPAIR = 232 SLIMED = 233 RAYNORTIMEBOMBMISSILE = 234 RAYNORTIMEBOMBUNIT = 235 TYCHUSCOMMANDOSTIMPACK = 236 VIRALPLASMA = 237 NAPALM = 238 BURSTCAPACITORSDAMAGEBUFF = 239 COLONYINFESTATION = 240 DOMINATION = 241 EMPBURST = 242 HYBRIDCZERGYROOTS = 243 HYBRIDFZERGYROOTS = 244 LOCKDOWNB = 245 SPECTRELOCKDOWNB = 246 VOODOOLOCKDOWN = 247 ZERATULSTUN = 248 BUILDINGSCARAB = 249 VORTEXBEHAVIORERADICATOR = 250 GHOSTBLAST = 251 HEROICBUFF03 = 252 CANNONRADAR = 253 SS_MISSILETARGETCHECKVIKING = 254 SS_MISSILETARGETCHECK = 255 SS_MAXSPEED = 256 SS_MAXACCELERATION = 257 SS_POWERUPDIAGONAL1 = 258 WATER = 259 DEFENSIVEMATRIX = 260 TESTATTRIBUTE = 261 TESTVETERANCY = 262 SHREDDERSWARMDAMAGEAPPLY = 263 CORRUPTORINFESTING = 264 MERCGROUNDDROPDELAY = 265 MERCGROUNDDROP = 266 MERCAIRDROPDELAY = 267 SPECTREHOLDFIRE = 268 SPECTREHOLDFIREB = 269 ITEMGRAVITYBOMBS = 270 CARRYMINERALFIELDMINERALS = 271 CARRYHIGHYIELDMINERALFIELDMINERALS = 272 CARRYHARVESTABLEVESPENEGEYSERGAS = 273 CARRYHARVESTABLEVESPENEGEYSERGASPROTOSS = 274 CARRYHARVESTABLEVESPENEGEYSERGASZERG = 275 PERMANENTLYCLOAKED = 276 RAVENSCRAMBLERMISSILE = 277 RAVENSHREDDERMISSILETIMEOUT = 278 RAVENSHREDDERMISSILETINT = 279 RAVENSHREDDERMISSILEARMORREDUCTION = 280 CHRONOBOOSTENERGYCOST = 281 NEXUSSHIELDRECHARGEONPYLONBEHAVIOR = 282 NEXUSSHIELDRECHARGEONPYLONBEHAVIORSECONDARYONTARGET = 283 INFESTORENSNARE = 284 INFESTORENSNAREMAKEPRECURSORREHEIGHTSOURCE = 285 NEXUSSHIELDOVERCHARGE = 286 PARASITICBOMBDELAYTIMEDLIFE = 287 TRANSFUSION = 288 ACCELERATIONZONETEMPORALFIELD = 289 ACCELERATIONZONEFLYINGTEMPORALFIELD = 290 INHIBITORZONEFLYINGTEMPORALFIELD = 291 LOADOUTSPRAYTRACKER = 292 INHIBITORZONETEMPORALFIELD = 293 CLOAKFIELD = 294 RESONATINGGLAIVESPHASESHIFT = 295 NEURALPARASITECHILDREN = 296 AMORPHOUSARMORCLOUD = 297 RAVENSHREDDERMISSILEARMORREDUCTIONUISUBTRUCT = 298 TAKENDAMAGE = 299 RAVENSCRAMBLERMISSILECARRIER = 300 BATTERYOVERCHARGE = 301 HYDRALISKFRENZY = 302 def __repr__(self) -> str: return f"BuffId.{self.name}" @classmethod def _missing_(cls, value: int) -> BuffId: return cls.NULL for item in BuffId: globals()[item.name] = item ================================================ FILE: sc2/ids/effect_id.py ================================================ from __future__ import annotations # DO NOT EDIT! # This file was automatically generated by "generate_ids.py" import enum class EffectId(enum.Enum): NULL = 0 PSISTORMPERSISTENT = 1 GUARDIANSHIELDPERSISTENT = 2 TEMPORALFIELDGROWINGBUBBLECREATEPERSISTENT = 3 TEMPORALFIELDAFTERBUBBLECREATEPERSISTENT = 4 THERMALLANCESFORWARD = 5 SCANNERSWEEP = 6 NUKEPERSISTENT = 7 LIBERATORTARGETMORPHDELAYPERSISTENT = 8 LIBERATORTARGETMORPHPERSISTENT = 9 BLINDINGCLOUDCP = 10 RAVAGERCORROSIVEBILECP = 11 LURKERMP = 12 def __repr__(self) -> str: return f"EffectId.{self.name}" for item in EffectId: globals()[item.name] = item ================================================ FILE: sc2/ids/id_version.py ================================================ ID_VERSION_STRING = "4.11.4.78285" ================================================ FILE: sc2/ids/unit_typeid.py ================================================ from __future__ import annotations # DO NOT EDIT! # This file was automatically generated by "generate_ids.py" import enum class UnitTypeId(enum.Enum): NOTAUNIT = 0 SYSTEM_SNAPSHOT_DUMMY = 1 BALL = 2 STEREOSCOPICOPTIONSUNIT = 3 COLOSSUS = 4 TECHLAB = 5 REACTOR = 6 INFESTORTERRAN = 7 BANELINGCOCOON = 8 BANELING = 9 MOTHERSHIP = 10 POINTDEFENSEDRONE = 11 CHANGELING = 12 CHANGELINGZEALOT = 13 CHANGELINGMARINESHIELD = 14 CHANGELINGMARINE = 15 CHANGELINGZERGLINGWINGS = 16 CHANGELINGZERGLING = 17 COMMANDCENTER = 18 SUPPLYDEPOT = 19 REFINERY = 20 BARRACKS = 21 ENGINEERINGBAY = 22 MISSILETURRET = 23 BUNKER = 24 SENSORTOWER = 25 GHOSTACADEMY = 26 FACTORY = 27 STARPORT = 28 ARMORY = 29 FUSIONCORE = 30 AUTOTURRET = 31 SIEGETANKSIEGED = 32 SIEGETANK = 33 VIKINGASSAULT = 34 VIKINGFIGHTER = 35 COMMANDCENTERFLYING = 36 BARRACKSTECHLAB = 37 BARRACKSREACTOR = 38 FACTORYTECHLAB = 39 FACTORYREACTOR = 40 STARPORTTECHLAB = 41 STARPORTREACTOR = 42 FACTORYFLYING = 43 STARPORTFLYING = 44 SCV = 45 BARRACKSFLYING = 46 SUPPLYDEPOTLOWERED = 47 MARINE = 48 REAPER = 49 GHOST = 50 MARAUDER = 51 THOR = 52 HELLION = 53 MEDIVAC = 54 BANSHEE = 55 RAVEN = 56 BATTLECRUISER = 57 NUKE = 58 NEXUS = 59 PYLON = 60 ASSIMILATOR = 61 GATEWAY = 62 FORGE = 63 FLEETBEACON = 64 TWILIGHTCOUNCIL = 65 PHOTONCANNON = 66 STARGATE = 67 TEMPLARARCHIVE = 68 DARKSHRINE = 69 ROBOTICSBAY = 70 ROBOTICSFACILITY = 71 CYBERNETICSCORE = 72 ZEALOT = 73 STALKER = 74 HIGHTEMPLAR = 75 DARKTEMPLAR = 76 SENTRY = 77 PHOENIX = 78 CARRIER = 79 VOIDRAY = 80 WARPPRISM = 81 OBSERVER = 82 IMMORTAL = 83 PROBE = 84 INTERCEPTOR = 85 HATCHERY = 86 CREEPTUMOR = 87 EXTRACTOR = 88 SPAWNINGPOOL = 89 EVOLUTIONCHAMBER = 90 HYDRALISKDEN = 91 SPIRE = 92 ULTRALISKCAVERN = 93 INFESTATIONPIT = 94 NYDUSNETWORK = 95 BANELINGNEST = 96 ROACHWARREN = 97 SPINECRAWLER = 98 SPORECRAWLER = 99 LAIR = 100 HIVE = 101 GREATERSPIRE = 102 EGG = 103 DRONE = 104 ZERGLING = 105 OVERLORD = 106 HYDRALISK = 107 MUTALISK = 108 ULTRALISK = 109 ROACH = 110 INFESTOR = 111 CORRUPTOR = 112 BROODLORDCOCOON = 113 BROODLORD = 114 BANELINGBURROWED = 115 DRONEBURROWED = 116 HYDRALISKBURROWED = 117 ROACHBURROWED = 118 ZERGLINGBURROWED = 119 INFESTORTERRANBURROWED = 120 REDSTONELAVACRITTERBURROWED = 121 REDSTONELAVACRITTERINJUREDBURROWED = 122 REDSTONELAVACRITTER = 123 REDSTONELAVACRITTERINJURED = 124 QUEENBURROWED = 125 QUEEN = 126 INFESTORBURROWED = 127 OVERLORDCOCOON = 128 OVERSEER = 129 PLANETARYFORTRESS = 130 ULTRALISKBURROWED = 131 ORBITALCOMMAND = 132 WARPGATE = 133 ORBITALCOMMANDFLYING = 134 FORCEFIELD = 135 WARPPRISMPHASING = 136 CREEPTUMORBURROWED = 137 CREEPTUMORQUEEN = 138 SPINECRAWLERUPROOTED = 139 SPORECRAWLERUPROOTED = 140 ARCHON = 141 NYDUSCANAL = 142 BROODLINGESCORT = 143 GHOSTALTERNATE = 144 GHOSTNOVA = 145 RICHMINERALFIELD = 146 RICHMINERALFIELD750 = 147 URSADON = 148 XELNAGATOWER = 149 INFESTEDTERRANSEGG = 150 LARVA = 151 REAPERPLACEHOLDER = 152 MARINEACGLUESCREENDUMMY = 153 FIREBATACGLUESCREENDUMMY = 154 MEDICACGLUESCREENDUMMY = 155 MARAUDERACGLUESCREENDUMMY = 156 VULTUREACGLUESCREENDUMMY = 157 SIEGETANKACGLUESCREENDUMMY = 158 VIKINGACGLUESCREENDUMMY = 159 BANSHEEACGLUESCREENDUMMY = 160 BATTLECRUISERACGLUESCREENDUMMY = 161 ORBITALCOMMANDACGLUESCREENDUMMY = 162 BUNKERACGLUESCREENDUMMY = 163 BUNKERUPGRADEDACGLUESCREENDUMMY = 164 MISSILETURRETACGLUESCREENDUMMY = 165 HELLBATACGLUESCREENDUMMY = 166 GOLIATHACGLUESCREENDUMMY = 167 CYCLONEACGLUESCREENDUMMY = 168 WRAITHACGLUESCREENDUMMY = 169 SCIENCEVESSELACGLUESCREENDUMMY = 170 HERCULESACGLUESCREENDUMMY = 171 THORACGLUESCREENDUMMY = 172 PERDITIONTURRETACGLUESCREENDUMMY = 173 FLAMINGBETTYACGLUESCREENDUMMY = 174 DEVASTATIONTURRETACGLUESCREENDUMMY = 175 BLASTERBILLYACGLUESCREENDUMMY = 176 SPINNINGDIZZYACGLUESCREENDUMMY = 177 ZERGLINGKERRIGANACGLUESCREENDUMMY = 178 RAPTORACGLUESCREENDUMMY = 179 QUEENCOOPACGLUESCREENDUMMY = 180 HYDRALISKACGLUESCREENDUMMY = 181 HYDRALISKLURKERACGLUESCREENDUMMY = 182 MUTALISKBROODLORDACGLUESCREENDUMMY = 183 BROODLORDACGLUESCREENDUMMY = 184 ULTRALISKACGLUESCREENDUMMY = 185 TORRASQUEACGLUESCREENDUMMY = 186 OVERSEERACGLUESCREENDUMMY = 187 LURKERACGLUESCREENDUMMY = 188 SPINECRAWLERACGLUESCREENDUMMY = 189 SPORECRAWLERACGLUESCREENDUMMY = 190 NYDUSNETWORKACGLUESCREENDUMMY = 191 OMEGANETWORKACGLUESCREENDUMMY = 192 ZERGLINGZAGARAACGLUESCREENDUMMY = 193 SWARMLINGACGLUESCREENDUMMY = 194 BANELINGACGLUESCREENDUMMY = 195 SPLITTERLINGACGLUESCREENDUMMY = 196 ABERRATIONACGLUESCREENDUMMY = 197 SCOURGEACGLUESCREENDUMMY = 198 CORRUPTORACGLUESCREENDUMMY = 199 BILELAUNCHERACGLUESCREENDUMMY = 200 SWARMQUEENACGLUESCREENDUMMY = 201 ROACHACGLUESCREENDUMMY = 202 ROACHVILEACGLUESCREENDUMMY = 203 RAVAGERACGLUESCREENDUMMY = 204 SWARMHOSTACGLUESCREENDUMMY = 205 MUTALISKACGLUESCREENDUMMY = 206 GUARDIANACGLUESCREENDUMMY = 207 DEVOURERACGLUESCREENDUMMY = 208 VIPERACGLUESCREENDUMMY = 209 BRUTALISKACGLUESCREENDUMMY = 210 LEVIATHANACGLUESCREENDUMMY = 211 ZEALOTACGLUESCREENDUMMY = 212 ZEALOTAIURACGLUESCREENDUMMY = 213 DRAGOONACGLUESCREENDUMMY = 214 HIGHTEMPLARACGLUESCREENDUMMY = 215 ARCHONACGLUESCREENDUMMY = 216 IMMORTALACGLUESCREENDUMMY = 217 OBSERVERACGLUESCREENDUMMY = 218 PHOENIXAIURACGLUESCREENDUMMY = 219 REAVERACGLUESCREENDUMMY = 220 TEMPESTACGLUESCREENDUMMY = 221 PHOTONCANNONACGLUESCREENDUMMY = 222 ZEALOTVORAZUNACGLUESCREENDUMMY = 223 ZEALOTSHAKURASACGLUESCREENDUMMY = 224 STALKERSHAKURASACGLUESCREENDUMMY = 225 DARKTEMPLARSHAKURASACGLUESCREENDUMMY = 226 CORSAIRACGLUESCREENDUMMY = 227 VOIDRAYACGLUESCREENDUMMY = 228 VOIDRAYSHAKURASACGLUESCREENDUMMY = 229 ORACLEACGLUESCREENDUMMY = 230 DARKARCHONACGLUESCREENDUMMY = 231 DARKPYLONACGLUESCREENDUMMY = 232 ZEALOTPURIFIERACGLUESCREENDUMMY = 233 SENTRYPURIFIERACGLUESCREENDUMMY = 234 IMMORTALKARAXACGLUESCREENDUMMY = 235 COLOSSUSACGLUESCREENDUMMY = 236 COLOSSUSPURIFIERACGLUESCREENDUMMY = 237 PHOENIXPURIFIERACGLUESCREENDUMMY = 238 CARRIERACGLUESCREENDUMMY = 239 CARRIERAIURACGLUESCREENDUMMY = 240 KHAYDARINMONOLITHACGLUESCREENDUMMY = 241 SHIELDBATTERYACGLUESCREENDUMMY = 242 ELITEMARINEACGLUESCREENDUMMY = 243 MARAUDERCOMMANDOACGLUESCREENDUMMY = 244 SPECOPSGHOSTACGLUESCREENDUMMY = 245 HELLBATRANGERACGLUESCREENDUMMY = 246 STRIKEGOLIATHACGLUESCREENDUMMY = 247 HEAVYSIEGETANKACGLUESCREENDUMMY = 248 RAIDLIBERATORACGLUESCREENDUMMY = 249 RAVENTYPEIIACGLUESCREENDUMMY = 250 COVERTBANSHEEACGLUESCREENDUMMY = 251 RAILGUNTURRETACGLUESCREENDUMMY = 252 BLACKOPSMISSILETURRETACGLUESCREENDUMMY = 253 SUPPLICANTACGLUESCREENDUMMY = 254 STALKERTALDARIMACGLUESCREENDUMMY = 255 SENTRYTALDARIMACGLUESCREENDUMMY = 256 HIGHTEMPLARTALDARIMACGLUESCREENDUMMY = 257 IMMORTALTALDARIMACGLUESCREENDUMMY = 258 COLOSSUSTALDARIMACGLUESCREENDUMMY = 259 WARPPRISMTALDARIMACGLUESCREENDUMMY = 260 PHOTONCANNONTALDARIMACGLUESCREENDUMMY = 261 NEEDLESPINESWEAPON = 262 CORRUPTIONWEAPON = 263 INFESTEDTERRANSWEAPON = 264 NEURALPARASITEWEAPON = 265 POINTDEFENSEDRONERELEASEWEAPON = 266 HUNTERSEEKERWEAPON = 267 MULE = 268 THORAAWEAPON = 269 PUNISHERGRENADESLMWEAPON = 270 VIKINGFIGHTERWEAPON = 271 ATALASERBATTERYLMWEAPON = 272 ATSLASERBATTERYLMWEAPON = 273 LONGBOLTMISSILEWEAPON = 274 D8CHARGEWEAPON = 275 YAMATOWEAPON = 276 IONCANNONSWEAPON = 277 ACIDSALIVAWEAPON = 278 SPINECRAWLERWEAPON = 279 SPORECRAWLERWEAPON = 280 GLAIVEWURMWEAPON = 281 GLAIVEWURMM2WEAPON = 282 GLAIVEWURMM3WEAPON = 283 STALKERWEAPON = 284 EMP2WEAPON = 285 BACKLASHROCKETSLMWEAPON = 286 PHOTONCANNONWEAPON = 287 PARASITESPOREWEAPON = 288 BROODLING = 289 BROODLORDBWEAPON = 290 AUTOTURRETRELEASEWEAPON = 291 LARVARELEASEMISSILE = 292 ACIDSPINESWEAPON = 293 FRENZYWEAPON = 294 CONTAMINATEWEAPON = 295 BEACONRALLY = 296 BEACONARMY = 297 BEACONATTACK = 298 BEACONDEFEND = 299 BEACONHARASS = 300 BEACONIDLE = 301 BEACONAUTO = 302 BEACONDETECT = 303 BEACONSCOUT = 304 BEACONCLAIM = 305 BEACONEXPAND = 306 BEACONCUSTOM1 = 307 BEACONCUSTOM2 = 308 BEACONCUSTOM3 = 309 BEACONCUSTOM4 = 310 ADEPT = 311 ROCKS2X2NONCONJOINED = 312 FUNGALGROWTHMISSILE = 313 NEURALPARASITETENTACLEMISSILE = 314 BEACON_PROTOSS = 315 BEACON_PROTOSSSMALL = 316 BEACON_TERRAN = 317 BEACON_TERRANSMALL = 318 BEACON_ZERG = 319 BEACON_ZERGSMALL = 320 LYOTE = 321 CARRIONBIRD = 322 KARAKMALE = 323 KARAKFEMALE = 324 URSADAKFEMALEEXOTIC = 325 URSADAKMALE = 326 URSADAKFEMALE = 327 URSADAKCALF = 328 URSADAKMALEEXOTIC = 329 UTILITYBOT = 330 COMMENTATORBOT1 = 331 COMMENTATORBOT2 = 332 COMMENTATORBOT3 = 333 COMMENTATORBOT4 = 334 SCANTIPEDE = 335 DOG = 336 SHEEP = 337 COW = 338 INFESTEDTERRANSEGGPLACEMENT = 339 INFESTORTERRANSWEAPON = 340 MINERALFIELD = 341 VESPENEGEYSER = 342 SPACEPLATFORMGEYSER = 343 RICHVESPENEGEYSER = 344 DESTRUCTIBLESEARCHLIGHT = 345 DESTRUCTIBLEBULLHORNLIGHTS = 346 DESTRUCTIBLESTREETLIGHT = 347 DESTRUCTIBLESPACEPLATFORMSIGN = 348 DESTRUCTIBLESTOREFRONTCITYPROPS = 349 DESTRUCTIBLEBILLBOARDTALL = 350 DESTRUCTIBLEBILLBOARDSCROLLINGTEXT = 351 DESTRUCTIBLESPACEPLATFORMBARRIER = 352 DESTRUCTIBLESIGNSDIRECTIONAL = 353 DESTRUCTIBLESIGNSCONSTRUCTION = 354 DESTRUCTIBLESIGNSFUNNY = 355 DESTRUCTIBLESIGNSICONS = 356 DESTRUCTIBLESIGNSWARNING = 357 DESTRUCTIBLEGARAGE = 358 DESTRUCTIBLEGARAGELARGE = 359 DESTRUCTIBLETRAFFICSIGNAL = 360 TRAFFICSIGNAL = 361 BRAXISALPHADESTRUCTIBLE1X1 = 362 BRAXISALPHADESTRUCTIBLE2X2 = 363 DESTRUCTIBLEDEBRIS4X4 = 364 DESTRUCTIBLEDEBRIS6X6 = 365 DESTRUCTIBLEROCK2X4VERTICAL = 366 DESTRUCTIBLEROCK2X4HORIZONTAL = 367 DESTRUCTIBLEROCK2X6VERTICAL = 368 DESTRUCTIBLEROCK2X6HORIZONTAL = 369 DESTRUCTIBLEROCK4X4 = 370 DESTRUCTIBLEROCK6X6 = 371 DESTRUCTIBLERAMPDIAGONALHUGEULBR = 372 DESTRUCTIBLERAMPDIAGONALHUGEBLUR = 373 DESTRUCTIBLERAMPVERTICALHUGE = 374 DESTRUCTIBLERAMPHORIZONTALHUGE = 375 DESTRUCTIBLEDEBRISRAMPDIAGONALHUGEULBR = 376 DESTRUCTIBLEDEBRISRAMPDIAGONALHUGEBLUR = 377 OVERLORDGENERATECREEPKEYBIND = 378 MENGSKSTATUEALONE = 379 MENGSKSTATUE = 380 WOLFSTATUE = 381 GLOBESTATUE = 382 WEAPON = 383 GLAIVEWURMBOUNCEWEAPON = 384 BROODLORDWEAPON = 385 BROODLORDAWEAPON = 386 CREEPBLOCKER1X1 = 387 PERMANENTCREEPBLOCKER1X1 = 388 PATHINGBLOCKER1X1 = 389 PATHINGBLOCKER2X2 = 390 AUTOTESTATTACKTARGETGROUND = 391 AUTOTESTATTACKTARGETAIR = 392 AUTOTESTATTACKER = 393 HELPEREMITTERSELECTIONARROW = 394 MULTIKILLOBJECT = 395 SHAPEGOLFBALL = 396 SHAPECONE = 397 SHAPECUBE = 398 SHAPECYLINDER = 399 SHAPEDODECAHEDRON = 400 SHAPEICOSAHEDRON = 401 SHAPEOCTAHEDRON = 402 SHAPEPYRAMID = 403 SHAPEROUNDEDCUBE = 404 SHAPESPHERE = 405 SHAPETETRAHEDRON = 406 SHAPETHICKTORUS = 407 SHAPETHINTORUS = 408 SHAPETORUS = 409 SHAPE4POINTSTAR = 410 SHAPE5POINTSTAR = 411 SHAPE6POINTSTAR = 412 SHAPE8POINTSTAR = 413 SHAPEARROWPOINTER = 414 SHAPEBOWL = 415 SHAPEBOX = 416 SHAPECAPSULE = 417 SHAPECRESCENTMOON = 418 SHAPEDECAHEDRON = 419 SHAPEDIAMOND = 420 SHAPEFOOTBALL = 421 SHAPEGEMSTONE = 422 SHAPEHEART = 423 SHAPEJACK = 424 SHAPEPLUSSIGN = 425 SHAPESHAMROCK = 426 SHAPESPADE = 427 SHAPETUBE = 428 SHAPEEGG = 429 SHAPEYENSIGN = 430 SHAPEX = 431 SHAPEWATERMELON = 432 SHAPEWONSIGN = 433 SHAPETENNISBALL = 434 SHAPESTRAWBERRY = 435 SHAPESMILEYFACE = 436 SHAPESOCCERBALL = 437 SHAPERAINBOW = 438 SHAPESADFACE = 439 SHAPEPOUNDSIGN = 440 SHAPEPEAR = 441 SHAPEPINEAPPLE = 442 SHAPEORANGE = 443 SHAPEPEANUT = 444 SHAPEO = 445 SHAPELEMON = 446 SHAPEMONEYBAG = 447 SHAPEHORSESHOE = 448 SHAPEHOCKEYSTICK = 449 SHAPEHOCKEYPUCK = 450 SHAPEHAND = 451 SHAPEGOLFCLUB = 452 SHAPEGRAPE = 453 SHAPEEUROSIGN = 454 SHAPEDOLLARSIGN = 455 SHAPEBASKETBALL = 456 SHAPECARROT = 457 SHAPECHERRY = 458 SHAPEBASEBALL = 459 SHAPEBASEBALLBAT = 460 SHAPEBANANA = 461 SHAPEAPPLE = 462 SHAPECASHLARGE = 463 SHAPECASHMEDIUM = 464 SHAPECASHSMALL = 465 SHAPEFOOTBALLCOLORED = 466 SHAPELEMONSMALL = 467 SHAPEORANGESMALL = 468 SHAPETREASURECHESTOPEN = 469 SHAPETREASURECHESTCLOSED = 470 SHAPEWATERMELONSMALL = 471 UNBUILDABLEROCKSDESTRUCTIBLE = 472 UNBUILDABLEBRICKSDESTRUCTIBLE = 473 UNBUILDABLEPLATESDESTRUCTIBLE = 474 DEBRIS2X2NONCONJOINED = 475 ENEMYPATHINGBLOCKER1X1 = 476 ENEMYPATHINGBLOCKER2X2 = 477 ENEMYPATHINGBLOCKER4X4 = 478 ENEMYPATHINGBLOCKER8X8 = 479 ENEMYPATHINGBLOCKER16X16 = 480 SCOPETEST = 481 SENTRYACGLUESCREENDUMMY = 482 MINERALFIELD750 = 483 HELLIONTANK = 484 COLLAPSIBLETERRANTOWERDEBRIS = 485 DEBRISRAMPLEFT = 486 DEBRISRAMPRIGHT = 487 MOTHERSHIPCORE = 488 LOCUSTMP = 489 COLLAPSIBLEROCKTOWERDEBRIS = 490 NYDUSCANALATTACKER = 491 NYDUSCANALCREEPER = 492 SWARMHOSTBURROWEDMP = 493 SWARMHOSTMP = 494 ORACLE = 495 TEMPEST = 496 WARHOUND = 497 WIDOWMINE = 498 VIPER = 499 WIDOWMINEBURROWED = 500 LURKERMPEGG = 501 LURKERMP = 502 LURKERMPBURROWED = 503 LURKERDENMP = 504 EXTENDINGBRIDGENEWIDE8OUT = 505 EXTENDINGBRIDGENEWIDE8 = 506 EXTENDINGBRIDGENWWIDE8OUT = 507 EXTENDINGBRIDGENWWIDE8 = 508 EXTENDINGBRIDGENEWIDE10OUT = 509 EXTENDINGBRIDGENEWIDE10 = 510 EXTENDINGBRIDGENWWIDE10OUT = 511 EXTENDINGBRIDGENWWIDE10 = 512 EXTENDINGBRIDGENEWIDE12OUT = 513 EXTENDINGBRIDGENEWIDE12 = 514 EXTENDINGBRIDGENWWIDE12OUT = 515 EXTENDINGBRIDGENWWIDE12 = 516 COLLAPSIBLEROCKTOWERDEBRISRAMPRIGHT = 517 COLLAPSIBLEROCKTOWERDEBRISRAMPLEFT = 518 XELNAGA_CAVERNS_DOORE = 519 XELNAGA_CAVERNS_DOOREOPENED = 520 XELNAGA_CAVERNS_DOORN = 521 XELNAGA_CAVERNS_DOORNE = 522 XELNAGA_CAVERNS_DOORNEOPENED = 523 XELNAGA_CAVERNS_DOORNOPENED = 524 XELNAGA_CAVERNS_DOORNW = 525 XELNAGA_CAVERNS_DOORNWOPENED = 526 XELNAGA_CAVERNS_DOORS = 527 XELNAGA_CAVERNS_DOORSE = 528 XELNAGA_CAVERNS_DOORSEOPENED = 529 XELNAGA_CAVERNS_DOORSOPENED = 530 XELNAGA_CAVERNS_DOORSW = 531 XELNAGA_CAVERNS_DOORSWOPENED = 532 XELNAGA_CAVERNS_DOORW = 533 XELNAGA_CAVERNS_DOORWOPENED = 534 XELNAGA_CAVERNS_FLOATING_BRIDGENE8OUT = 535 XELNAGA_CAVERNS_FLOATING_BRIDGENE8 = 536 XELNAGA_CAVERNS_FLOATING_BRIDGENW8OUT = 537 XELNAGA_CAVERNS_FLOATING_BRIDGENW8 = 538 XELNAGA_CAVERNS_FLOATING_BRIDGENE10OUT = 539 XELNAGA_CAVERNS_FLOATING_BRIDGENE10 = 540 XELNAGA_CAVERNS_FLOATING_BRIDGENW10OUT = 541 XELNAGA_CAVERNS_FLOATING_BRIDGENW10 = 542 XELNAGA_CAVERNS_FLOATING_BRIDGENE12OUT = 543 XELNAGA_CAVERNS_FLOATING_BRIDGENE12 = 544 XELNAGA_CAVERNS_FLOATING_BRIDGENW12OUT = 545 XELNAGA_CAVERNS_FLOATING_BRIDGENW12 = 546 XELNAGA_CAVERNS_FLOATING_BRIDGEH8OUT = 547 XELNAGA_CAVERNS_FLOATING_BRIDGEH8 = 548 XELNAGA_CAVERNS_FLOATING_BRIDGEV8OUT = 549 XELNAGA_CAVERNS_FLOATING_BRIDGEV8 = 550 XELNAGA_CAVERNS_FLOATING_BRIDGEH10OUT = 551 XELNAGA_CAVERNS_FLOATING_BRIDGEH10 = 552 XELNAGA_CAVERNS_FLOATING_BRIDGEV10OUT = 553 XELNAGA_CAVERNS_FLOATING_BRIDGEV10 = 554 XELNAGA_CAVERNS_FLOATING_BRIDGEH12OUT = 555 XELNAGA_CAVERNS_FLOATING_BRIDGEH12 = 556 XELNAGA_CAVERNS_FLOATING_BRIDGEV12OUT = 557 XELNAGA_CAVERNS_FLOATING_BRIDGEV12 = 558 COLLAPSIBLETERRANTOWERPUSHUNITRAMPLEFT = 559 COLLAPSIBLETERRANTOWERPUSHUNITRAMPRIGHT = 560 COLLAPSIBLEROCKTOWERPUSHUNIT = 561 COLLAPSIBLETERRANTOWERPUSHUNIT = 562 COLLAPSIBLEROCKTOWERPUSHUNITRAMPRIGHT = 563 COLLAPSIBLEROCKTOWERPUSHUNITRAMPLEFT = 564 DIGESTERCREEPSPRAYTARGETUNIT = 565 DIGESTERCREEPSPRAYUNIT = 566 NYDUSCANALATTACKERWEAPON = 567 VIPERCONSUMESTRUCTUREWEAPON = 568 RESOURCEBLOCKER = 569 TEMPESTWEAPON = 570 YOINKMISSILE = 571 YOINKVIKINGAIRMISSILE = 572 YOINKVIKINGGROUNDMISSILE = 573 YOINKSIEGETANKMISSILE = 574 WARHOUNDWEAPON = 575 EYESTALKWEAPON = 576 WIDOWMINEWEAPON = 577 WIDOWMINEAIRWEAPON = 578 MOTHERSHIPCOREWEAPONWEAPON = 579 TORNADOMISSILEWEAPON = 580 TORNADOMISSILEDUMMYWEAPON = 581 TALONSMISSILEWEAPON = 582 CREEPTUMORMISSILE = 583 LOCUSTMPEGGAMISSILEWEAPON = 584 LOCUSTMPEGGBMISSILEWEAPON = 585 LOCUSTMPWEAPON = 586 REPULSORCANNONWEAPON = 587 COLLAPSIBLEROCKTOWERDIAGONAL = 588 COLLAPSIBLETERRANTOWERDIAGONAL = 589 COLLAPSIBLETERRANTOWERRAMPLEFT = 590 COLLAPSIBLETERRANTOWERRAMPRIGHT = 591 ICE2X2NONCONJOINED = 592 ICEPROTOSSCRATES = 593 PROTOSSCRATES = 594 TOWERMINE = 595 PICKUPPALLETGAS = 596 PICKUPPALLETMINERALS = 597 PICKUPSCRAPSALVAGE1X1 = 598 PICKUPSCRAPSALVAGE2X2 = 599 PICKUPSCRAPSALVAGE3X3 = 600 ROUGHTERRAIN = 601 UNBUILDABLEBRICKSSMALLUNIT = 602 UNBUILDABLEPLATESSMALLUNIT = 603 UNBUILDABLEPLATESUNIT = 604 UNBUILDABLEROCKSSMALLUNIT = 605 XELNAGAHEALINGSHRINE = 606 INVISIBLETARGETDUMMY = 607 PROTOSSVESPENEGEYSER = 608 COLLAPSIBLEROCKTOWER = 609 COLLAPSIBLETERRANTOWER = 610 THORNLIZARD = 611 CLEANINGBOT = 612 DESTRUCTIBLEROCK6X6WEAK = 613 PROTOSSSNAKESEGMENTDEMO = 614 PHYSICSCAPSULE = 615 PHYSICSCUBE = 616 PHYSICSCYLINDER = 617 PHYSICSKNOT = 618 PHYSICSL = 619 PHYSICSPRIMITIVES = 620 PHYSICSSPHERE = 621 PHYSICSSTAR = 622 CREEPBLOCKER4X4 = 623 DESTRUCTIBLECITYDEBRIS2X4VERTICAL = 624 DESTRUCTIBLECITYDEBRIS2X4HORIZONTAL = 625 DESTRUCTIBLECITYDEBRIS2X6VERTICAL = 626 DESTRUCTIBLECITYDEBRIS2X6HORIZONTAL = 627 DESTRUCTIBLECITYDEBRIS4X4 = 628 DESTRUCTIBLECITYDEBRIS6X6 = 629 DESTRUCTIBLECITYDEBRISHUGEDIAGONALBLUR = 630 DESTRUCTIBLECITYDEBRISHUGEDIAGONALULBR = 631 TESTZERG = 632 PATHINGBLOCKERRADIUS1 = 633 DESTRUCTIBLEROCKEX12X4VERTICAL = 634 DESTRUCTIBLEROCKEX12X4HORIZONTAL = 635 DESTRUCTIBLEROCKEX12X6VERTICAL = 636 DESTRUCTIBLEROCKEX12X6HORIZONTAL = 637 DESTRUCTIBLEROCKEX14X4 = 638 DESTRUCTIBLEROCKEX16X6 = 639 DESTRUCTIBLEROCKEX1DIAGONALHUGEULBR = 640 DESTRUCTIBLEROCKEX1DIAGONALHUGEBLUR = 641 DESTRUCTIBLEROCKEX1VERTICALHUGE = 642 DESTRUCTIBLEROCKEX1HORIZONTALHUGE = 643 DESTRUCTIBLEICE2X4VERTICAL = 644 DESTRUCTIBLEICE2X4HORIZONTAL = 645 DESTRUCTIBLEICE2X6VERTICAL = 646 DESTRUCTIBLEICE2X6HORIZONTAL = 647 DESTRUCTIBLEICE4X4 = 648 DESTRUCTIBLEICE6X6 = 649 DESTRUCTIBLEICEDIAGONALHUGEULBR = 650 DESTRUCTIBLEICEDIAGONALHUGEBLUR = 651 DESTRUCTIBLEICEVERTICALHUGE = 652 DESTRUCTIBLEICEHORIZONTALHUGE = 653 DESERTPLANETSEARCHLIGHT = 654 DESERTPLANETSTREETLIGHT = 655 UNBUILDABLEBRICKSUNIT = 656 UNBUILDABLEROCKSUNIT = 657 ZERUSDESTRUCTIBLEARCH = 658 ARTOSILOPE = 659 ANTEPLOTT = 660 LABBOT = 661 CRABEETLE = 662 COLLAPSIBLEROCKTOWERRAMPRIGHT = 663 COLLAPSIBLEROCKTOWERRAMPLEFT = 664 LABMINERALFIELD = 665 LABMINERALFIELD750 = 666 SNOWREFINERY_TERRAN_EXTENDINGBRIDGENESHORT8OUT = 667 SNOWREFINERY_TERRAN_EXTENDINGBRIDGENESHORT8 = 668 SNOWREFINERY_TERRAN_EXTENDINGBRIDGENWSHORT8OUT = 669 SNOWREFINERY_TERRAN_EXTENDINGBRIDGENWSHORT8 = 670 TARSONIS_DOORN = 671 TARSONIS_DOORNLOWERED = 672 TARSONIS_DOORNE = 673 TARSONIS_DOORNELOWERED = 674 TARSONIS_DOORE = 675 TARSONIS_DOORELOWERED = 676 TARSONIS_DOORNW = 677 TARSONIS_DOORNWLOWERED = 678 COMPOUNDMANSION_DOORN = 679 COMPOUNDMANSION_DOORNLOWERED = 680 COMPOUNDMANSION_DOORNE = 681 COMPOUNDMANSION_DOORNELOWERED = 682 COMPOUNDMANSION_DOORE = 683 COMPOUNDMANSION_DOORELOWERED = 684 COMPOUNDMANSION_DOORNW = 685 COMPOUNDMANSION_DOORNWLOWERED = 686 RAVAGERCOCOON = 687 RAVAGER = 688 LIBERATOR = 689 RAVAGERBURROWED = 690 THORAP = 691 CYCLONE = 692 LOCUSTMPFLYING = 693 DISRUPTOR = 694 AIURLIGHTBRIDGENE8OUT = 695 AIURLIGHTBRIDGENE8 = 696 AIURLIGHTBRIDGENE10OUT = 697 AIURLIGHTBRIDGENE10 = 698 AIURLIGHTBRIDGENE12OUT = 699 AIURLIGHTBRIDGENE12 = 700 AIURLIGHTBRIDGENW8OUT = 701 AIURLIGHTBRIDGENW8 = 702 AIURLIGHTBRIDGENW10OUT = 703 AIURLIGHTBRIDGENW10 = 704 AIURLIGHTBRIDGENW12OUT = 705 AIURLIGHTBRIDGENW12 = 706 AIURTEMPLEBRIDGENE8OUT = 707 AIURTEMPLEBRIDGENE10OUT = 708 AIURTEMPLEBRIDGENE12OUT = 709 AIURTEMPLEBRIDGENW8OUT = 710 AIURTEMPLEBRIDGENW10OUT = 711 AIURTEMPLEBRIDGENW12OUT = 712 SHAKURASLIGHTBRIDGENE8OUT = 713 SHAKURASLIGHTBRIDGENE8 = 714 SHAKURASLIGHTBRIDGENE10OUT = 715 SHAKURASLIGHTBRIDGENE10 = 716 SHAKURASLIGHTBRIDGENE12OUT = 717 SHAKURASLIGHTBRIDGENE12 = 718 SHAKURASLIGHTBRIDGENW8OUT = 719 SHAKURASLIGHTBRIDGENW8 = 720 SHAKURASLIGHTBRIDGENW10OUT = 721 SHAKURASLIGHTBRIDGENW10 = 722 SHAKURASLIGHTBRIDGENW12OUT = 723 SHAKURASLIGHTBRIDGENW12 = 724 VOIDMPIMMORTALREVIVECORPSE = 725 GUARDIANCOCOONMP = 726 GUARDIANMP = 727 DEVOURERCOCOONMP = 728 DEVOURERMP = 729 DEFILERMPBURROWED = 730 DEFILERMP = 731 ORACLESTASISTRAP = 732 DISRUPTORPHASED = 733 LIBERATORAG = 734 AIURLIGHTBRIDGEABANDONEDNE8OUT = 735 AIURLIGHTBRIDGEABANDONEDNE8 = 736 AIURLIGHTBRIDGEABANDONEDNE10OUT = 737 AIURLIGHTBRIDGEABANDONEDNE10 = 738 AIURLIGHTBRIDGEABANDONEDNE12OUT = 739 AIURLIGHTBRIDGEABANDONEDNE12 = 740 AIURLIGHTBRIDGEABANDONEDNW8OUT = 741 AIURLIGHTBRIDGEABANDONEDNW8 = 742 AIURLIGHTBRIDGEABANDONEDNW10OUT = 743 AIURLIGHTBRIDGEABANDONEDNW10 = 744 AIURLIGHTBRIDGEABANDONEDNW12OUT = 745 AIURLIGHTBRIDGEABANDONEDNW12 = 746 COLLAPSIBLEPURIFIERTOWERDEBRIS = 747 PORTCITY_BRIDGE_UNITNE8OUT = 748 PORTCITY_BRIDGE_UNITNE8 = 749 PORTCITY_BRIDGE_UNITSE8OUT = 750 PORTCITY_BRIDGE_UNITSE8 = 751 PORTCITY_BRIDGE_UNITNW8OUT = 752 PORTCITY_BRIDGE_UNITNW8 = 753 PORTCITY_BRIDGE_UNITSW8OUT = 754 PORTCITY_BRIDGE_UNITSW8 = 755 PORTCITY_BRIDGE_UNITNE10OUT = 756 PORTCITY_BRIDGE_UNITNE10 = 757 PORTCITY_BRIDGE_UNITSE10OUT = 758 PORTCITY_BRIDGE_UNITSE10 = 759 PORTCITY_BRIDGE_UNITNW10OUT = 760 PORTCITY_BRIDGE_UNITNW10 = 761 PORTCITY_BRIDGE_UNITSW10OUT = 762 PORTCITY_BRIDGE_UNITSW10 = 763 PORTCITY_BRIDGE_UNITNE12OUT = 764 PORTCITY_BRIDGE_UNITNE12 = 765 PORTCITY_BRIDGE_UNITSE12OUT = 766 PORTCITY_BRIDGE_UNITSE12 = 767 PORTCITY_BRIDGE_UNITNW12OUT = 768 PORTCITY_BRIDGE_UNITNW12 = 769 PORTCITY_BRIDGE_UNITSW12OUT = 770 PORTCITY_BRIDGE_UNITSW12 = 771 PORTCITY_BRIDGE_UNITN8OUT = 772 PORTCITY_BRIDGE_UNITN8 = 773 PORTCITY_BRIDGE_UNITS8OUT = 774 PORTCITY_BRIDGE_UNITS8 = 775 PORTCITY_BRIDGE_UNITE8OUT = 776 PORTCITY_BRIDGE_UNITE8 = 777 PORTCITY_BRIDGE_UNITW8OUT = 778 PORTCITY_BRIDGE_UNITW8 = 779 PORTCITY_BRIDGE_UNITN10OUT = 780 PORTCITY_BRIDGE_UNITN10 = 781 PORTCITY_BRIDGE_UNITS10OUT = 782 PORTCITY_BRIDGE_UNITS10 = 783 PORTCITY_BRIDGE_UNITE10OUT = 784 PORTCITY_BRIDGE_UNITE10 = 785 PORTCITY_BRIDGE_UNITW10OUT = 786 PORTCITY_BRIDGE_UNITW10 = 787 PORTCITY_BRIDGE_UNITN12OUT = 788 PORTCITY_BRIDGE_UNITN12 = 789 PORTCITY_BRIDGE_UNITS12OUT = 790 PORTCITY_BRIDGE_UNITS12 = 791 PORTCITY_BRIDGE_UNITE12OUT = 792 PORTCITY_BRIDGE_UNITE12 = 793 PORTCITY_BRIDGE_UNITW12OUT = 794 PORTCITY_BRIDGE_UNITW12 = 795 PURIFIERRICHMINERALFIELD = 796 PURIFIERRICHMINERALFIELD750 = 797 COLLAPSIBLEPURIFIERTOWERPUSHUNIT = 798 LOCUSTMPPRECURSOR = 799 RELEASEINTERCEPTORSBEACON = 800 ADEPTPHASESHIFT = 801 RAVAGERCORROSIVEBILEMISSILE = 802 HYDRALISKIMPALEMISSILE = 803 CYCLONEMISSILELARGEAIR = 804 CYCLONEMISSILE = 805 CYCLONEMISSILELARGE = 806 THORAALANCE = 807 ORACLEWEAPON = 808 TEMPESTWEAPONGROUND = 809 RAVAGERWEAPONMISSILE = 810 SCOUTMPAIRWEAPONLEFT = 811 SCOUTMPAIRWEAPONRIGHT = 812 ARBITERMPWEAPONMISSILE = 813 GUARDIANMPWEAPON = 814 DEVOURERMPWEAPONMISSILE = 815 DEFILERMPDARKSWARMWEAPON = 816 QUEENMPENSNAREMISSILE = 817 QUEENMPSPAWNBROODLINGSMISSILE = 818 LIGHTNINGBOMBWEAPON = 819 HERCPLACEMENT = 820 GRAPPLEWEAPON = 821 CAUSTICSPRAYMISSILE = 822 PARASITICBOMBMISSILE = 823 PARASITICBOMBDUMMY = 824 ADEPTWEAPON = 825 ADEPTUPGRADEWEAPON = 826 LIBERATORMISSILE = 827 LIBERATORDAMAGEMISSILE = 828 LIBERATORAGMISSILE = 829 KD8CHARGE = 830 KD8CHARGEWEAPON = 831 SLAYNELEMENTALGRABWEAPON = 832 SLAYNELEMENTALGRABAIRUNIT = 833 SLAYNELEMENTALGRABGROUNDUNIT = 834 SLAYNELEMENTALWEAPON = 835 DESTRUCTIBLEEXPEDITIONGATE6X6 = 836 DESTRUCTIBLEZERGINFESTATION3X3 = 837 HERC = 838 MOOPY = 839 REPLICANT = 840 SEEKERMISSILE = 841 AIURTEMPLEBRIDGEDESTRUCTIBLENE8OUT = 842 AIURTEMPLEBRIDGEDESTRUCTIBLENE10OUT = 843 AIURTEMPLEBRIDGEDESTRUCTIBLENE12OUT = 844 AIURTEMPLEBRIDGEDESTRUCTIBLENW8OUT = 845 AIURTEMPLEBRIDGEDESTRUCTIBLENW10OUT = 846 AIURTEMPLEBRIDGEDESTRUCTIBLENW12OUT = 847 AIURTEMPLEBRIDGEDESTRUCTIBLESW8OUT = 848 AIURTEMPLEBRIDGEDESTRUCTIBLESW10OUT = 849 AIURTEMPLEBRIDGEDESTRUCTIBLESW12OUT = 850 AIURTEMPLEBRIDGEDESTRUCTIBLESE8OUT = 851 AIURTEMPLEBRIDGEDESTRUCTIBLESE10OUT = 852 AIURTEMPLEBRIDGEDESTRUCTIBLESE12OUT = 853 FLYOVERUNIT = 854 CORSAIRMP = 855 SCOUTMP = 856 ARBITERMP = 857 SCOURGEMP = 858 DEFILERMPPLAGUEWEAPON = 859 QUEENMP = 860 XELNAGADESTRUCTIBLERAMPBLOCKER6S = 861 XELNAGADESTRUCTIBLERAMPBLOCKER6SE = 862 XELNAGADESTRUCTIBLERAMPBLOCKER6E = 863 XELNAGADESTRUCTIBLERAMPBLOCKER6NE = 864 XELNAGADESTRUCTIBLERAMPBLOCKER6N = 865 XELNAGADESTRUCTIBLERAMPBLOCKER6NW = 866 XELNAGADESTRUCTIBLERAMPBLOCKER6W = 867 XELNAGADESTRUCTIBLERAMPBLOCKER6SW = 868 XELNAGADESTRUCTIBLERAMPBLOCKER8S = 869 XELNAGADESTRUCTIBLERAMPBLOCKER8SE = 870 XELNAGADESTRUCTIBLERAMPBLOCKER8E = 871 XELNAGADESTRUCTIBLERAMPBLOCKER8NE = 872 XELNAGADESTRUCTIBLERAMPBLOCKER8N = 873 XELNAGADESTRUCTIBLERAMPBLOCKER8NW = 874 XELNAGADESTRUCTIBLERAMPBLOCKER8W = 875 XELNAGADESTRUCTIBLERAMPBLOCKER8SW = 876 REPTILECRATE = 877 SLAYNSWARMHOSTSPAWNFLYER = 878 SLAYNELEMENTAL = 879 PURIFIERVESPENEGEYSER = 880 SHAKURASVESPENEGEYSER = 881 COLLAPSIBLEPURIFIERTOWERDIAGONAL = 882 CREEPONLYBLOCKER4X4 = 883 PURIFIERMINERALFIELD = 884 PURIFIERMINERALFIELD750 = 885 BATTLESTATIONMINERALFIELD = 886 BATTLESTATIONMINERALFIELD750 = 887 BEACON_NOVA = 888 BEACON_NOVASMALL = 889 URSULA = 890 ELSECARO_COLONIST_HUT = 891 TRANSPORTOVERLORDCOCOON = 892 OVERLORDTRANSPORT = 893 PYLONOVERCHARGED = 894 BYPASSARMORDRONE = 895 ADEPTPIERCINGWEAPON = 896 CORROSIVEPARASITEWEAPON = 897 INFESTEDTERRAN = 898 MERCCOMPOUND = 899 SUPPLYDEPOTDROP = 900 LURKERDEN = 901 D8CHARGE = 902 THORWRECKAGE = 903 GOLIATH = 904 TECHREACTOR = 905 SS_POWERUPBOMB = 906 SS_POWERUPHEALTH = 907 SS_POWERUPSIDEMISSILES = 908 SS_POWERUPSTRONGERMISSILES = 909 LURKEREGG = 910 LURKER = 911 LURKERBURROWED = 912 ARCHIVESEALED = 913 INFESTEDCIVILIAN = 914 FLAMINGBETTY = 915 INFESTEDCIVILIANBURROWED = 916 SELENDISINTERCEPTOR = 917 SIEGEBREAKERSIEGED = 918 SIEGEBREAKER = 919 PERDITIONTURRETUNDERGROUND = 920 PERDITIONTURRET = 921 SENTRYGUNUNDERGROUND = 922 SENTRYGUN = 923 WARPIG = 924 DEVILDOG = 925 SPARTANCOMPANY = 926 HAMMERSECURITY = 927 HELSANGELFIGHTER = 928 DUSKWING = 929 DUKESREVENGE = 930 ODINWRECKAGE = 931 HERONUKE = 932 KERRIGANCHARBURROWED = 933 KERRIGANCHAR = 934 SPIDERMINEBURROWED = 935 SPIDERMINE = 936 ZERATUL = 937 URUN = 938 MOHANDAR = 939 SELENDIS = 940 SCOUT = 941 OMEGALISKBURROWED = 942 OMEGALISK = 943 INFESTEDABOMINATIONBURROWED = 944 INFESTEDABOMINATION = 945 HUNTERKILLERBURROWED = 946 HUNTERKILLER = 947 INFESTEDTERRANCAMPAIGNBURROWED = 948 INFESTEDTERRANCAMPAIGN = 949 CHECKSTATION = 950 CHECKSTATIONDIAGONALBLUR = 951 CHECKSTATIONDIAGONALULBR = 952 CHECKSTATIONVERTICAL = 953 CHECKSTATIONOPENED = 954 CHECKSTATIONDIAGONALBLUROPENED = 955 CHECKSTATIONDIAGONALULBROPENED = 956 CHECKSTATIONVERTICALOPENED = 957 BARRACKSTECHREACTOR = 958 FACTORYTECHREACTOR = 959 STARPORTTECHREACTOR = 960 SPECTRENUKE = 961 COLONISTSHIPFLYING = 962 COLONISTSHIP = 963 BIODOMECOMMANDFLYING = 964 BIODOMECOMMAND = 965 HERCULESLANDERFLYING = 966 HERCULESLANDER = 967 ZHAKULDASLIGHTBRIDGEOFF = 968 ZHAKULDASLIGHTBRIDGE = 969 ZHAKULDASLIBRARYUNITBURROWED = 970 ZHAKULDASLIBRARYUNIT = 971 XELNAGATEMPLEDOORBURROWED = 972 XELNAGATEMPLEDOOR = 973 XELNAGATEMPLEDOORURDLBURROWED = 974 XELNAGATEMPLEDOORURDL = 975 HELSANGELASSAULT = 976 AUTOMATEDREFINERY = 977 BATTLECRUISERHELIOSMORPH = 978 HEALINGPOTIONTESTINSTANT = 979 SPACEPLATFORMCLIFFDOOROPEN0 = 980 SPACEPLATFORMCLIFFDOOR0 = 981 SPACEPLATFORMCLIFFDOOROPEN1 = 982 SPACEPLATFORMCLIFFDOOR1 = 983 DESTRUCTIBLEGATEDIAGONALBLURLOWERED = 984 DESTRUCTIBLEGATEDIAGONALULBRLOWERED = 985 DESTRUCTIBLEGATESTRAIGHTHORIZONTALBFLOWERED = 986 DESTRUCTIBLEGATESTRAIGHTHORIZONTALLOWERED = 987 DESTRUCTIBLEGATESTRAIGHTVERTICALLFLOWERED = 988 DESTRUCTIBLEGATESTRAIGHTVERTICALLOWERED = 989 DESTRUCTIBLEGATEDIAGONALBLUR = 990 DESTRUCTIBLEGATEDIAGONALULBR = 991 DESTRUCTIBLEGATESTRAIGHTHORIZONTALBF = 992 DESTRUCTIBLEGATESTRAIGHTHORIZONTAL = 993 DESTRUCTIBLEGATESTRAIGHTVERTICALLF = 994 DESTRUCTIBLEGATESTRAIGHTVERTICAL = 995 METALGATEDIAGONALBLURLOWERED = 996 METALGATEDIAGONALULBRLOWERED = 997 METALGATESTRAIGHTHORIZONTALBFLOWERED = 998 METALGATESTRAIGHTHORIZONTALLOWERED = 999 METALGATESTRAIGHTVERTICALLFLOWERED = 1000 METALGATESTRAIGHTVERTICALLOWERED = 1001 METALGATEDIAGONALBLUR = 1002 METALGATEDIAGONALULBR = 1003 METALGATESTRAIGHTHORIZONTALBF = 1004 METALGATESTRAIGHTHORIZONTAL = 1005 METALGATESTRAIGHTVERTICALLF = 1006 METALGATESTRAIGHTVERTICAL = 1007 SECURITYGATEDIAGONALBLURLOWERED = 1008 SECURITYGATEDIAGONALULBRLOWERED = 1009 SECURITYGATESTRAIGHTHORIZONTALBFLOWERED = 1010 SECURITYGATESTRAIGHTHORIZONTALLOWERED = 1011 SECURITYGATESTRAIGHTVERTICALLFLOWERED = 1012 SECURITYGATESTRAIGHTVERTICALLOWERED = 1013 SECURITYGATEDIAGONALBLUR = 1014 SECURITYGATEDIAGONALULBR = 1015 SECURITYGATESTRAIGHTHORIZONTALBF = 1016 SECURITYGATESTRAIGHTHORIZONTAL = 1017 SECURITYGATESTRAIGHTVERTICALLF = 1018 SECURITYGATESTRAIGHTVERTICAL = 1019 TERRAZINENODEDEADTERRAN = 1020 TERRAZINENODEHAPPYPROTOSS = 1021 ZHAKULDASLIGHTBRIDGEOFFTOPRIGHT = 1022 ZHAKULDASLIGHTBRIDGETOPRIGHT = 1023 BATTLECRUISERHELIOS = 1024 NUKESILONOVA = 1025 ODIN = 1026 PYGALISKCOCOON = 1027 DEVOURERTISSUEDOODAD = 1028 SS_BATTLECRUISERMISSILELAUNCHER = 1029 SS_TERRATRONMISSILESPINNERMISSILE = 1030 SS_TERRATRONSAW = 1031 SS_BATTLECRUISERHUNTERSEEKERMISSILE = 1032 SS_LEVIATHANBOMB = 1033 DEVOURERTISSUEMISSILE = 1034 SS_INTERCEPTOR = 1035 SS_LEVIATHANBOMBMISSILE = 1036 SS_LEVIATHANSPAWNBOMBMISSILE = 1037 SS_FIGHTERMISSILELEFT = 1038 SS_FIGHTERMISSILERIGHT = 1039 SS_INTERCEPTORSPAWNMISSILE = 1040 SS_CARRIERBOSSMISSILE = 1041 SS_LEVIATHANTENTACLETARGET = 1042 SS_LEVIATHANTENTACLEL2MISSILE = 1043 SS_LEVIATHANTENTACLER1MISSILE = 1044 SS_LEVIATHANTENTACLER2MISSILE = 1045 SS_LEVIATHANTENTACLEL1MISSILE = 1046 SS_TERRATRONMISSILE = 1047 SS_WRAITHMISSILE = 1048 SS_SCOURGEMISSILE = 1049 SS_CORRUPTORMISSILE = 1050 SS_SWARMGUARDIANMISSILE = 1051 SS_STRONGMISSILE1 = 1052 SS_STRONGMISSILE2 = 1053 SS_FIGHTERDRONEMISSILE = 1054 SS_PHOENIXMISSILE = 1055 SS_SCOUTMISSILE = 1056 SS_INTERCEPTORMISSILE = 1057 SS_SCIENCEVESSELMISSILE = 1058 SS_BATTLECRUISERMISSILE = 1059 D8CLUSTERBOMBWEAPON = 1060 D8CLUSTERBOMB = 1061 BROODLORDEGG = 1062 BROODLORDEGGMISSILE = 1063 CIVILIANWEAPON = 1064 BATTLECRUISERHELIOSALMWEAPON = 1065 BATTLECRUISERLOKILMWEAPON = 1066 BATTLECRUISERHELIOSGLMWEAPON = 1067 BIOSTASISMISSILE = 1068 INFESTEDVENTBROODLORDEGG = 1069 INFESTEDVENTCORRUPTOREGG = 1070 TENTACLEAMISSILE = 1071 TENTACLEBMISSILE = 1072 TENTACLECMISSILE = 1073 TENTACLEDMISSILE = 1074 MUTALISKEGG = 1075 INFESTEDVENTMUTALISKEGG = 1076 MUTALISKEGGMISSILE = 1077 INFESTEDVENTEGGMISSILE = 1078 SPORECANNONFIREMISSILE = 1079 EXPERIMENTALPLASMAGUNWEAPON = 1080 BRUTALISKWEAPON = 1081 LOKIHURRICANEMISSILELEFT = 1082 LOKIHURRICANEMISSILERIGHT = 1083 ODINAAWEAPON = 1084 DUSKWINGWEAPON = 1085 KERRIGANWEAPON = 1086 ULTRASONICPULSEWEAPON = 1087 KERRIGANCHARWEAPON = 1088 DEVASTATORMISSILEWEAPON = 1089 SWANNWEAPON = 1090 HAMMERSECURITYLMWEAPON = 1091 CONSUMEDNAFEEDBACKWEAPON = 1092 URUNWEAPONLEFT = 1093 URUNWEAPONRIGHT = 1094 HAILSTORMMISSILESWEAPON = 1095 COLONYINFESTATIONWEAPON = 1096 VOIDSEEKERPHASEMINEBLASTWEAPON = 1097 VOIDSEEKERPHASEMINEBLASTSECONDARYWEAPON = 1098 TOSSGRENADEWEAPON = 1099 TYCHUSGRENADEWEAPON = 1100 VILESTREAMWEAPON = 1101 WRAITHAIRWEAPONRIGHT = 1102 WRAITHAIRWEAPONLEFT = 1103 WRAITHGROUNDWEAPON = 1104 WEAPONHYBRIDD = 1105 KARASSWEAPON = 1106 HYBRIDCPLASMAWEAPON = 1107 WARBOTBMISSILE = 1108 LOKIYAMATOWEAPON = 1109 HYPERIONYAMATOSPECIALWEAPON = 1110 HYPERIONLMWEAPON = 1111 HYPERIONALMWEAPON = 1112 VULTUREWEAPON = 1113 SCOUTAIRWEAPONLEFT = 1114 SCOUTAIRWEAPONRIGHT = 1115 HUNTERKILLERWEAPON = 1116 GOLIATHAWEAPON = 1117 SPARTANCOMPANYAWEAPON = 1118 LEVIATHANSCOURGEMISSILE = 1119 BIOPLASMIDDISCHARGEWEAPON = 1120 VOIDSEEKERWEAPON = 1121 HELSANGELFIGHTERWEAPON = 1122 DRBATTLECRUISERALMWEAPON = 1123 DRBATTLECRUISERGLMWEAPON = 1124 HURRICANEMISSILERIGHT = 1125 HURRICANEMISSILELEFT = 1126 HYBRIDSINGULARITYFEEDBACKWEAPON = 1127 DOMINIONKILLTEAMLMWEAPON = 1128 ITEMGRENADESWEAPON = 1129 ITEMGRAVITYBOMBSWEAPON = 1130 TESTHEROTHROWMISSILE = 1131 TESTHERODEBUGMISSILEABILITY1WEAPON = 1132 TESTHERODEBUGMISSILEABILITY2WEAPON = 1133 SPECTRE = 1134 VULTURE = 1135 LOKI = 1136 WRAITH = 1137 DOMINIONKILLTEAM = 1138 FIREBAT = 1139 DIAMONDBACK = 1140 G4CHARGEWEAPON = 1141 SS_BLACKEDGEBORDER = 1142 DEVOURERTISSUESAMPLETUBE = 1143 MONOLITH = 1144 OBELISK = 1145 ARCHIVE = 1146 ARTIFACTVAULT = 1147 AVERNUSGATECONTROL = 1148 GATECONTROLUNIT = 1149 BLIMPADS = 1150 BLOCKER6X6 = 1151 BLOCKER8X8 = 1152 BLOCKER16X16 = 1153 CARGOTRUCKUNITFLATBED = 1154 CARGOTRUCKUNITTRAILER = 1155 BLIMP = 1156 CASTANARWINDOWLARGEDIAGONALULBRUNIT = 1157 BLOCKER4X4 = 1158 HOMELARGE = 1159 HOMESMALL = 1160 ELEVATORBLOCKER = 1161 QUESTIONMARK = 1162 NYDUSWORMLAVADEATH = 1163 SS_BACKGROUNDSPACELARGE = 1164 SS_BACKGROUNDSPACETERRAN00 = 1165 SS_BACKGROUNDSPACETERRAN02 = 1166 SS_BACKGROUNDSPACEZERG00 = 1167 SS_BACKGROUNDSPACEZERG02 = 1168 SS_CARRIERBOSS = 1169 SS_BATTLECRUISER = 1170 SS_TERRATRONMISSILESPINNERLAUNCHER = 1171 SS_TERRATRONMISSILESPINNER = 1172 SS_TERRATRONBEAMTARGET = 1173 SS_LIGHTNINGPROJECTORFACERIGHT = 1174 SS_SCOURGE = 1175 SS_CORRUPTOR = 1176 SS_TERRATRONMISSILELAUNCHER = 1177 SS_LIGHTNINGPROJECTORFACELEFT = 1178 SS_WRAITH = 1179 SS_SWARMGUARDIAN = 1180 SS_SCOUT = 1181 SS_LEVIATHAN = 1182 SS_SCIENCEVESSEL = 1183 SS_TERRATRON = 1184 SECRETDOCUMENTS = 1185 PREDATOR = 1186 DEFILERBONESAMPLE = 1187 DEVOURERTISSUESAMPLE = 1188 PROTOSSPSIELEMENTS = 1189 TASSADAR = 1190 SCIENCEFACILITY = 1191 INFESTEDCOCOON = 1192 FUSIONREACTOR = 1193 BUBBACOMMERCIAL = 1194 XELNAGAPRISONHEIGHT2 = 1195 XELNAGAPRISON = 1196 XELNAGAPRISONNORTH = 1197 XELNAGAPRISONNORTHHEIGHT2 = 1198 ZERGDROPPODCREEP = 1199 IPISTOLAD = 1200 L800ETC_AD = 1201 NUKENOODLESCOMMERCIAL = 1202 PSIOPSCOMMERCIAL = 1203 SHIPALARM = 1204 SPACEPLATFORMDESTRUCTIBLEJUMBOBLOCKER = 1205 SPACEPLATFORMDESTRUCTIBLELARGEBLOCKER = 1206 SPACEPLATFORMDESTRUCTIBLEMEDIUMBLOCKER = 1207 SPACEPLATFORMDESTRUCTIBLESMALLBLOCKER = 1208 TALDARIMMOTHERSHIP = 1209 PLASMATORPEDOESWEAPON = 1210 PSIDISRUPTOR = 1211 HIVEMINDEMULATOR = 1212 RAYNOR01 = 1213 SCIENCEVESSEL = 1214 SCOURGE = 1215 SPACEPLATFORMREACTORPATHINGBLOCKER = 1216 TAURENOUTHOUSE = 1217 TYCHUSEJECTMISSILE = 1218 FEEDERLING = 1219 ULAANSMOKEBRIDGE = 1220 TALDARIMPRISONCRYSTAL = 1221 SPACEDIABLO = 1222 MURLOCMARINE = 1223 XELNAGAPRISONCONSOLE = 1224 TALDARIMPRISON = 1225 ADJUTANTCAPSULE = 1226 XELNAGAVAULT = 1227 HOLDINGPEN = 1228 SCRAPHUGE = 1229 PRISONERCIVILIAN = 1230 BIODOMEHALFBUILT = 1231 BIODOME = 1232 DESTRUCTIBLEKORHALFLAG = 1233 DESTRUCTIBLEKORHALPODIUM = 1234 DESTRUCTIBLEKORHALTREE = 1235 DESTRUCTIBLEKORHALFOLIAGE = 1236 DESTRUCTIBLESANDBAGS = 1237 CASTANARWINDOWLARGEDIAGONALBLURUNIT = 1238 CARGOTRUCKUNITBARRELS = 1239 SPORECANNON = 1240 STETMANN = 1241 BRIDGEBLOCKER4X12 = 1242 CIVILIANSHIPWRECKED = 1243 SWANN = 1244 DRAKKENLASERDRILL = 1245 MINDSIPHONRETURNWEAPON = 1246 KERRIGANEGG = 1247 CHRYSALISEGG = 1248 PRISONERSPECTRE = 1249 PRISONZEALOT = 1250 SCRAPSALVAGE1X1 = 1251 SCRAPSALVAGE2X2 = 1252 SCRAPSALVAGE3X3 = 1253 RAYNORCOMMANDO = 1254 OVERMIND = 1255 OVERMINDREMAINS = 1256 INFESTEDMERCHAVEN = 1257 MONLYTHARTIFACTFORCEFIELD = 1258 MONLYTHFORCEFIELDSTATUE = 1259 VIROPHAGE = 1260 PSISHOCKWEAPON = 1261 TYCHUSCOMMANDO = 1262 BRUTALISK = 1263 PYGALISK = 1264 VALHALLABASEDESTRUCTIBLEDOORDEAD = 1265 VALHALLABASEDESTRUCTIBLEDOOR = 1266 VOIDSEEKER = 1267 MINDSIPHONWEAPON = 1268 WARBOT = 1269 PLATFORMCONNECTOR = 1270 ARTANIS = 1271 TERRAZINECANISTER = 1272 HERCULES = 1273 MERCENARYFORTRESS = 1274 RAYNOR = 1275 ARTIFACTPIECE1 = 1276 ARTIFACTPIECE2 = 1277 ARTIFACTPIECE4 = 1278 ARTIFACTPIECE3 = 1279 ARTIFACTPIECE5 = 1280 RIPFIELDGENERATOR = 1281 RIPFIELDGENERATORSMALL = 1282 XELNAGAWORLDSHIPVAULT = 1283 TYCHUSCHAINGUN = 1284 ARTIFACT = 1285 CELLBLOCKB = 1286 GHOSTLASERLINES = 1287 MAINCELLBLOCK = 1288 KERRIGAN = 1289 DATACORE = 1290 SPECIALOPSDROPSHIP = 1291 TOSH = 1292 CASTANARULTRALISKSHACKLEDUNIT = 1293 KARASS = 1294 INVISIBLEPYLON = 1295 MAAR = 1296 HYBRIDDESTROYER = 1297 HYBRIDREAVER = 1298 HYBRID = 1299 TERRAZINENODE = 1300 TRANSPORTTRUCK = 1301 WALLOFFIRE = 1302 WEAPONHYBRIDC = 1303 XELNAGATEMPLE = 1304 EXPLODINGBARRELLARGE = 1305 SUPERWARPGATE = 1306 TERRAZINETANK = 1307 XELNAGASHRINE = 1308 SMCAMERABRIDGE = 1309 SMMARSARABARTYCHUSCAMERAS = 1310 SMHYPERIONBRIDGESTAGE1HANSONCAMERAS = 1311 SMHYPERIONBRIDGESTAGE1HORNERCAMERAS = 1312 SMHYPERIONBRIDGESTAGE1TYCHUSCAMERAS = 1313 SMHYPERIONBRIDGESTAGE1TOSHCAMERAS = 1314 SMHYPERIONARMORYSTAGE1SWANNCAMERAS = 1315 SMHYPERIONCANTINATOSHCAMERAS = 1316 SMHYPERIONCANTINATYCHUSCAMERAS = 1317 SMHYPERIONCANTINAYBARRACAMERAS = 1318 SMHYPERIONLABADJUTANTCAMERAS = 1319 SMHYPERIONLABCOWINCAMERAS = 1320 SMHYPERIONLABHANSONCAMERAS = 1321 SMHYPERIONBRIDGETRAYNOR03BRIEFINGCAMERA = 1322 SMTESTCAMERA = 1323 SMCAMERATERRAN01 = 1324 SMCAMERATERRAN02A = 1325 SMCAMERATERRAN02B = 1326 SMCAMERATERRAN03 = 1327 SMCAMERATERRAN04 = 1328 SMCAMERATERRAN04A = 1329 SMCAMERATERRAN04B = 1330 SMCAMERATERRAN05 = 1331 SMCAMERATERRAN06A = 1332 SMCAMERATERRAN06B = 1333 SMCAMERATERRAN06C = 1334 SMCAMERATERRAN07 = 1335 SMCAMERATERRAN08 = 1336 SMCAMERATERRAN09 = 1337 SMCAMERATERRAN10 = 1338 SMCAMERATERRAN11 = 1339 SMCAMERATERRAN12 = 1340 SMCAMERATERRAN13 = 1341 SMCAMERATERRAN14 = 1342 SMCAMERATERRAN15 = 1343 SMCAMERATERRAN16 = 1344 SMCAMERATERRAN17 = 1345 SMCAMERATERRAN20 = 1346 SMFIRSTOFFICER = 1347 SMHYPERIONBRIDGEBRIEFINGLEFT = 1348 SMHYPERIONBRIDGEBRIEFINGRIGHT = 1349 SMHYPERIONMEDLABBRIEFING = 1350 SMHYPERIONMEDLABBRIEFINGCENTER = 1351 SMHYPERIONMEDLABBRIEFINGLEFT = 1352 SMHYPERIONMEDLABBRIEFINGRIGHT = 1353 SMTOSHSHUTTLESET = 1354 SMKERRIGANPHOTO = 1355 SMTOSHSHUTTLESET2 = 1356 SMMARSARABARJUKEBOXHS = 1357 SMMARSARABARKERRIGANPHOTOHS = 1358 SMVALERIANFLAGSHIPCORRIDORSSET = 1359 SMVALERIANFLAGSHIPCORRIDORSSET2 = 1360 SMVALERIANFLAGSHIPCORRIDORSSET3 = 1361 SMVALERIANFLAGSHIPCORRIDORSSET4 = 1362 SMVALERIANOBSERVATORYSET = 1363 SMVALERIANOBSERVATORYSET2 = 1364 SMVALERIANOBSERVATORYSET3 = 1365 SMVALERIANOBSERVATORYPAINTINGHS = 1366 SMCHARBATTLEZONEFLAG = 1367 SMUNNSET = 1368 SMTERRANREADYROOMSET = 1369 SMCHARBATTLEZONESET = 1370 SMCHARBATTLEZONESET2 = 1371 SMCHARBATTLEZONESET3 = 1372 SMCHARBATTLEZONESET4 = 1373 SMCHARBATTLEZONESET5 = 1374 SMCHARBATTLEZONEARTIFACTHS = 1375 SMCHARBATTLEZONERADIOHS = 1376 SMCHARBATTLEZONEDROPSHIPHS = 1377 SMCHARBATTLEZONEBRIEFCASEHS = 1378 SMCHARBATTLEZONEBRIEFINGSET = 1379 SMCHARBATTLEZONEBRIEFINGSET2 = 1380 SMCHARBATTLEZONEBRIEFINGSETLEFT = 1381 SMCHARBATTLEZONEBRIEFINGSETRIGHT = 1382 SMMARSARABARBADGEHS = 1383 SMHYPERIONCANTINABADGEHS = 1384 SMHYPERIONCANTINAPOSTER1HS = 1385 SMHYPERIONCANTINAPOSTER2HS = 1386 SMHYPERIONCANTINAPOSTER3HS = 1387 SMHYPERIONCANTINAPOSTER4HS = 1388 SMHYPERIONCANTINAPOSTER5HS = 1389 SMFLY = 1390 SMBRIDGEWINDOWSPACE = 1391 SMBRIDGEPLANETSPACE = 1392 SMBRIDGEPLANETSPACEASTEROIDS = 1393 SMBRIDGEPLANETAGRIA = 1394 SMBRIDGEPLANETAIUR = 1395 SMBRIDGEPLANETAVERNUS = 1396 SMBRIDGEPLANETBELSHIR = 1397 SMBRIDGEPLANETCASTANAR = 1398 SMBRIDGEPLANETCHAR = 1399 SMBRIDGEPLANETHAVEN = 1400 SMBRIDGEPLANETKORHAL = 1401 SMBRIDGEPLANETMEINHOFF = 1402 SMBRIDGEPLANETMONLYTH = 1403 SMBRIDGEPLANETNEWFOLSOM = 1404 SMBRIDGEPLANETPORTZION = 1405 SMBRIDGEPLANETREDSTONE = 1406 SMBRIDGEPLANETSHAKURAS = 1407 SMBRIDGEPLANETTARSONIS = 1408 SMBRIDGEPLANETTYPHON = 1409 SMBRIDGEPLANETTYRADOR = 1410 SMBRIDGEPLANETULAAN = 1411 SMBRIDGEPLANETULNAR = 1412 SMBRIDGEPLANETVALHALLA = 1413 SMBRIDGEPLANETXIL = 1414 SMBRIDGEPLANETZHAKULDAS = 1415 SMMARSARAPLANET = 1416 SMNOVA = 1417 SMHAVENPLANET = 1418 SMHYPERIONBRIDGEBRIEFING = 1419 SMHYPERIONBRIDGEBRIEFINGCENTER = 1420 SMCHARBATTLEFIELDENDPROPS = 1421 SMCHARBATTLEZONETURRET = 1422 SMTERRAN01FX = 1423 SMTERRAN03FX = 1424 SMTERRAN05FX = 1425 SMTERRAN05FXMUTALISKS = 1426 SMTERRAN05PROPS = 1427 SMTERRAN06AFX = 1428 SMTERRAN06BFX = 1429 SMTERRAN06CFX = 1430 SMTERRAN12FX = 1431 SMTERRAN14FX = 1432 SMTERRAN15FX = 1433 SMTERRAN06APROPS = 1434 SMTERRAN06BPROPS = 1435 SMTERRAN07PROPS = 1436 SMTERRAN07FX = 1437 SMTERRAN08PROPS = 1438 SMTERRAN09FX = 1439 SMTERRAN09PROPS = 1440 SMTERRAN11FX = 1441 SMTERRAN11FXMISSILES = 1442 SMTERRAN11FXEXPLOSIONS = 1443 SMTERRAN11FXBLOOD = 1444 SMTERRAN11FXDEBRIS = 1445 SMTERRAN11FXDEBRIS1 = 1446 SMTERRAN11FXDEBRIS2 = 1447 SMTERRAN11PROPS = 1448 SMTERRAN11PROPSBURROWROCKS = 1449 SMTERRAN11PROPSRIFLESHELLS = 1450 SMTERRAN12PROPS = 1451 SMTERRAN13PROPS = 1452 SMTERRAN14PROPS = 1453 SMTERRAN15PROPS = 1454 SMTERRAN16FX = 1455 SMTERRAN16FXFLAK = 1456 SMTERRAN17PROPS = 1457 SMTERRAN17FX = 1458 SMMARSARABARPROPS = 1459 SMHYPERIONCORRIDORPROPS = 1460 ZERATULCRYSTALCHARGE = 1461 SMRAYNORHANDS = 1462 SMPRESSROOMPROPS = 1463 SMRAYNORGUN = 1464 SMMARINERIFLE = 1465 SMTOSHKNIFE = 1466 SMTOSHSHUTTLEPROPS = 1467 SMHYPERIONEXTERIOR = 1468 SMHYPERIONEXTERIORLOW = 1469 SMHYPERIONEXTERIORHOLOGRAM = 1470 SMCHARCUTSCENES00 = 1471 SMCHARCUTSCENES01 = 1472 SMCHARCUTSCENES02 = 1473 SMCHARCUTSCENES03 = 1474 SMMARSARABARBRIEFINGSET = 1475 SMMARSARABARBRIEFINGSET2 = 1476 SMMARSARABARBRIEFINGSETLEFT = 1477 SMMARSARABARBRIEFINGSETRIGHT = 1478 SMMARSARABARBRIEFINGTVMAIN = 1479 SMMARSARABARBRIEFINGTVMAIN2 = 1480 SMMARSARABARBRIEFINGTVMAIN3 = 1481 SMMARSARABARBRIEFINGTVPORTRAIT1 = 1482 SMMARSARABARBRIEFINGTVPORTRAIT2 = 1483 SMMARSARABARBRIEFINGTVPORTRAIT3 = 1484 SMMARSARABARBRIEFINGTVPORTRAIT4 = 1485 SMMARSARABARBRIEFINGTVPORTRAIT5 = 1486 SMMARSARABARSET = 1487 SMMARSARABARSET2 = 1488 SMMARSARABARSTARMAPHS = 1489 SMMARSARABARTVHS = 1490 SMMARSARABARHYDRALISKSKULLHS = 1491 SMMARSARABARCORKBOARDHS = 1492 SMMARSARABARCORKBOARDBACKGROUND = 1493 SMMARSARABARCORKBOARDITEM1HS = 1494 SMMARSARABARCORKBOARDITEM2HS = 1495 SMMARSARABARCORKBOARDITEM3HS = 1496 SMMARSARABARCORKBOARDITEM4HS = 1497 SMMARSARABARCORKBOARDITEM5HS = 1498 SMMARSARABARCORKBOARDITEM6HS = 1499 SMMARSARABARCORKBOARDITEM7HS = 1500 SMMARSARABARCORKBOARDITEM8HS = 1501 SMMARSARABARCORKBOARDITEM9HS = 1502 SMMARSARABARBOTTLESHS = 1503 SMVALERIANOBSERVATORYPROPS = 1504 SMVALERIANOBSERVATORYSTARMAP = 1505 SMBANSHEE = 1506 SMVIKING = 1507 SMARMORYBANSHEE = 1508 SMARMORYDROPSHIP = 1509 SMARMORYTANK = 1510 SMARMORYVIKING = 1511 SMARMORYSPIDERMINE = 1512 SMARMORYGHOSTCRATE = 1513 SMARMORYSPECTRECRATE = 1514 SMARMORYBANSHEEPHCRATE = 1515 SMARMORYDROPSHIPPHCRATE = 1516 SMARMORYTANKPHCRATE = 1517 SMARMORYVIKINGPHCRATE = 1518 SMARMORYSPIDERMINEPHCRATE = 1519 SMARMORYGHOSTCRATEPHCRATE = 1520 SMARMORYSPECTRECRATEPHCRATE = 1521 SMARMORYRIFLE = 1522 SMDROPSHIP = 1523 SMDROPSHIPBLUE = 1524 SMHYPERIONARMORYVIKING = 1525 SMCHARGATLINGGUN = 1526 SMBOUNTYHUNTER = 1527 SMCIVILIAN = 1528 SMZERGEDHANSON = 1529 SMLABASSISTANT = 1530 SMHYPERIONARMORER = 1531 SMUNNSCREEN = 1532 NEWSARCTURUSINTERVIEWSET = 1533 NEWSARCTURUSPRESSROOM = 1534 SMDONNYVERMILLIONSET = 1535 NEWSMEINHOFFREFUGEECENTER = 1536 NEWSRAYNORLOGO = 1537 NEWSTVEFFECT = 1538 SMUNNCAMERA = 1539 SMLEEKENOSET = 1540 SMTVSTATIC = 1541 SMDONNYVERMILLION = 1542 SMDONNYVERMILLIONDEATH = 1543 SMLEEKENO = 1544 SMKATELOCKWELL = 1545 SMMIKELIBERTY = 1546 SMTERRANREADYROOMLEFTTV = 1547 SMTERRANREADYROOMMAINTV = 1548 SMTERRANREADYROOMRIGHTTV = 1549 SMHYPERIONARMORYSTAGE1SET = 1550 SMHYPERIONARMORYSTAGE1SET01 = 1551 SMHYPERIONARMORYSTAGE1SET02 = 1552 SMHYPERIONARMORYSTAGE1SET03 = 1553 SMHYPERIONARMORYSPACELIGHTING = 1554 SMHYPERIONARMORYSTAGE1TECHNOLOGYCONSOLEHS = 1555 SMHYPERIONBRIDGESTAGE1BOW = 1556 SMHYPERIONBRIDGESTAGE1SET = 1557 SMHYPERIONBRIDGESTAGE1SET2 = 1558 SMHYPERIONBRIDGESTAGE1SET3 = 1559 SMHYPERIONBRIDGEHOLOMAP = 1560 SMHYPERIONCANTINASTAGE1SET = 1561 SMHYPERIONCANTINASTAGE1SET2 = 1562 SMHYPERIONCANTINASTAGE1WALLPIECE = 1563 SMHYPERIONBRIDGEPROPS = 1564 SMHYPERIONCANTINAPROPS = 1565 SMHYPERIONMEDLABPROPS = 1566 SMHYPERIONMEDLABPROTOSSCRYOTUBE0HS = 1567 SMHYPERIONMEDLABPROTOSSCRYOTUBE1HS = 1568 SMHYPERIONMEDLABPROTOSSCRYOTUBE2HS = 1569 SMHYPERIONMEDLABPROTOSSCRYOTUBE3HS = 1570 SMHYPERIONMEDLABPROTOSSCRYOTUBE4HS = 1571 SMHYPERIONMEDLABPROTOSSCRYOTUBE5HS = 1572 SMHYPERIONMEDLABZERGCRYOTUBE0HS = 1573 SMHYPERIONMEDLABZERGCRYOTUBE1HS = 1574 SMHYPERIONMEDLABZERGCRYOTUBE2HS = 1575 SMHYPERIONMEDLABZERGCRYOTUBE3HS = 1576 SMHYPERIONMEDLABZERGCRYOTUBE4HS = 1577 SMHYPERIONMEDLABZERGCRYOTUBE5HS = 1578 SMHYPERIONMEDLABCRYOTUBEA = 1579 SMHYPERIONMEDLABCRYOTUBEB = 1580 SMHYPERIONCANTINASTAGE1EXITHS = 1581 SMHYPERIONCANTINASTAGE1STAIRCASEHS = 1582 SMHYPERIONCANTINASTAGE1TVHS = 1583 SMHYPERIONCANTINASTAGE1ARCADEGAMEHS = 1584 SMHYPERIONCANTINASTAGE1JUKEBOXHS = 1585 SMHYPERIONCANTINASTAGE1CORKBOARDHS = 1586 SMHYPERIONCANTINAPROGRESSFRAME = 1587 SMHYPERIONCANTINAHYDRACLAWSHS = 1588 SMHYPERIONCANTINAMERCCOMPUTERHS = 1589 SMHYPERIONCANTINASTAGE1PROGRESS1HS = 1590 SMHYPERIONCANTINASTAGE1PROGRESS2HS = 1591 SMHYPERIONCANTINASTAGE1PROGRESS3HS = 1592 SMHYPERIONCANTINASTAGE1PROGRESS4HS = 1593 SMHYPERIONCANTINASTAGE1PROGRESS5HS = 1594 SMHYPERIONCANTINASTAGE1PROGRESS6HS = 1595 SMHYPERIONCORRIDORSET = 1596 SMHYPERIONBRIDGESTAGE1BATTLEREPORTSHS = 1597 SMHYPERIONBRIDGESTAGE1CENTERCONSOLEHS = 1598 SMHYPERIONBRIDGESTAGE1BATTLECOMMANDHS = 1599 SMHYPERIONBRIDGESTAGE1CANTINAHS = 1600 SMHYPERIONBRIDGESTAGE1WINDOWHS = 1601 SMHYPERIONMEDLABSTAGE1SET = 1602 SMHYPERIONMEDLABSTAGE1SET2 = 1603 SMHYPERIONMEDLABSTAGE1SETLIGHTS = 1604 SMHYPERIONMEDLABSTAGE1CONSOLEHS = 1605 SMHYPERIONMEDLABSTAGE1DOORHS = 1606 SMHYPERIONMEDLABSTAGE1CRYSTALHS = 1607 SMHYPERIONMEDLABSTAGE1ARTIFACTHS = 1608 SMHYPERIONLABARTIFACTPART1HS = 1609 SMHYPERIONLABARTIFACTPART2HS = 1610 SMHYPERIONLABARTIFACTPART3HS = 1611 SMHYPERIONLABARTIFACTPART4HS = 1612 SMHYPERIONLABARTIFACTBASEHS = 1613 SMSHADOWBOX = 1614 SMCHARBATTLEZONESHADOWBOX = 1615 SMCHARINTERACTIVESKYPARALLAX = 1616 SMCHARINTERACTIVE02SKYPARALLAX = 1617 SMRAYNORCOMMANDER = 1618 SMADJUTANT = 1619 SMADJUTANTHOLOGRAM = 1620 SMMARAUDER = 1621 SMFIREBAT = 1622 SMMARAUDERPHCRATE = 1623 SMFIREBATPHCRATE = 1624 SMRAYNORMARINE = 1625 SMMARINE01 = 1626 SMMARINE02 = 1627 SMMARINE02AOD = 1628 SMMARINE03 = 1629 SMMARINE04 = 1630 SMCADE = 1631 SMHALL = 1632 SMBRALIK = 1633 SMANNABELLE = 1634 SMEARL = 1635 SMKACHINSKY = 1636 SMGENERICMALEGREASEMONKEY01 = 1637 SMGENERICMALEGREASEMONKEY02 = 1638 SMGENERICMALEOFFICER01 = 1639 SMGENERICMALEOFFICER02 = 1640 SMSTETMANN = 1641 SMCOOPER = 1642 SMHILL = 1643 SMYBARRA = 1644 SMVALERIANMENGSK = 1645 SMARCTURUSMENGSK = 1646 SMARCTURUSHOLOGRAM = 1647 SMZERATUL = 1648 SMHYDRALISK = 1649 SMHYDRALISKDEAD = 1650 SMMUTALISK = 1651 SMZERGLING = 1652 SCIENTIST = 1653 MINERMALE = 1654 CIVILIAN = 1655 COLONIST = 1656 CIVILIANFEMALE = 1657 COLONISTFEMALE = 1658 HUT = 1659 COLONISTHUT = 1660 INFESTABLEHUT = 1661 INFESTABLECOLONISTHUT = 1662 XELNAGASHRINEXIL = 1663 PROTOSSRELIC = 1664 PICKUPGRENADES = 1665 PICKUPPLASMAGUN = 1666 PICKUPPLASMAROUNDS = 1667 PICKUPMEDICRECHARGE = 1668 PICKUPMANARECHARGE = 1669 PICKUPRESTORATIONCHARGE = 1670 PICKUPCHRONORIFTDEVICE = 1671 PICKUPCHRONORIFTCHARGE = 1672 GASCANISTER = 1673 GASCANISTERPROTOSS = 1674 GASCANISTERZERG = 1675 MINERALCRYSTAL = 1676 PALLETGAS = 1677 PALLETMINERALS = 1678 NATURALGAS = 1679 NATURALMINERALS = 1680 NATURALMINERALSRED = 1681 PICKUPHEALTH25 = 1682 PICKUPHEALTH50 = 1683 PICKUPHEALTH100 = 1684 PICKUPHEALTHFULL = 1685 PICKUPENERGY25 = 1686 PICKUPENERGY50 = 1687 PICKUPENERGY100 = 1688 PICKUPENERGYFULL = 1689 PICKUPMINES = 1690 PICKUPPSISTORM = 1691 CIVILIANCARSUNIT = 1692 CRUISERBIKE = 1693 TERRANBUGGY = 1694 COLONISTVEHICLEUNIT = 1695 COLONISTVEHICLEUNIT01 = 1696 DUMPTRUCK = 1697 TANKERTRUCK = 1698 FLATBEDTRUCK = 1699 COLONISTSHIPTHANSON02A = 1700 PURIFIER = 1701 INFESTEDARMORY = 1702 INFESTEDBARRACKS = 1703 INFESTEDBUNKER = 1704 INFESTEDCC = 1705 INFESTEDENGBAY = 1706 INFESTEDFACTORY = 1707 INFESTEDREFINERY = 1708 INFESTEDSTARPORT = 1709 INFESTEDMISSILETURRET = 1710 LOGISTICSHEADQUARTERS = 1711 INFESTEDSUPPLY = 1712 TARSONISENGINE = 1713 TARSONISENGINEFAST = 1714 FREIGHTCAR = 1715 CABOOSE = 1716 HYPERION = 1717 MENGSKHOLOGRAMBILLBOARD = 1718 TRAYNOR01SIGNSDESTRUCTIBLE1 = 1719 ABANDONEDBUILDING = 1720 NOVA = 1721 FOOD1000 = 1722 PSIINDOCTRINATOR = 1723 JORIUMSTOCKPILE = 1724 ZERGDROPPOD = 1725 TERRANDROPPOD = 1726 COLONISTBIODOME = 1727 COLONISTBIODOMEHALFBUILT = 1728 INFESTABLEBIODOME = 1729 INFESTABLECOLONISTBIODOME = 1730 MEDIC = 1731 VIKINGSKY_UNIT = 1732 SS_FIGHTER = 1733 SS_PHOENIX = 1734 SS_CARRIER = 1735 SS_BACKGROUNDZERG01 = 1736 SS_BACKGROUNDSPACE00 = 1737 SS_BACKGROUNDSPACE01 = 1738 SS_BACKGROUNDSPACE02 = 1739 SS_BACKGROUNDSPACEPROT00 = 1740 SS_BACKGROUNDSPACEPROT01 = 1741 SS_BACKGROUNDSPACEPROT02 = 1742 SS_BACKGROUNDSPACEPROT03 = 1743 SS_BACKGROUNDSPACEPROT04 = 1744 SS_BACKGROUNDSPACEPROTOSSLARGE = 1745 SS_BACKGROUNDSPACEZERGLARGE = 1746 SS_BACKGROUNDSPACETERRANLARGE = 1747 SS_BACKGROUNDSPACEZERG01 = 1748 SS_BACKGROUNDSPACETERRAN01 = 1749 BREACHINGCHARGE = 1750 INFESTATIONSPIRE = 1751 SPACEPLATFORMVENTSUNIT = 1752 STONEZEALOT = 1753 PRESERVERPRISON = 1754 PORTJUNKER = 1755 LEVIATHAN = 1756 SWARMLING = 1757 VALHALLADESTRUCTIBLEWALL = 1758 NEWFOLSOMPRISONENTRANCE = 1759 ODINBUILD = 1760 NUKEPACK = 1761 CHARDESTRUCTIBLEROCKCOVER = 1762 CHARDESTRUCTIBLEROCKCOVERV = 1763 CHARDESTRUCTIBLEROCKCOVERULDR = 1764 CHARDESTRUCTIBLEROCKCOVERURDL = 1765 MAARWARPINUNIT = 1766 EGGPURPLE = 1767 TRUCKFLATBEDUNIT = 1768 TRUCKSEMIUNIT = 1769 TRUCKUTILITYUNIT = 1770 INFESTEDCOLONISTSHIP = 1771 CASTANARDESTRUCTIBLEDEBRIS = 1772 COLONISTTRANSPORT = 1773 PRESERVERBASE = 1774 PRESERVERA = 1775 PRESERVERB = 1776 PRESERVERC = 1777 TAURENSPACEMARINE = 1778 MARSARABRIDGEBLUR = 1779 MARSARABRIDGEBRUL = 1780 SHORTBRIDGEVERTICAL = 1781 SHORTBRIDGEHORIZONTAL = 1782 TESTHERO = 1783 TESTSHOP = 1784 HEALINGPOTIONTESTTARGET = 1785 _4SLOTBAG = 1786 _6SLOTBAG = 1787 _8SLOTBAG = 1788 _10SLOTBAG = 1789 _12SLOTBAG = 1790 _14SLOTBAG = 1791 _16SLOTBAG = 1792 _18SLOTBAG = 1793 _20SLOTBAG = 1794 _22SLOTBAG = 1795 _24SLOTBAG = 1796 REPULSERFIELD6 = 1797 REPULSERFIELD8 = 1798 REPULSERFIELD10 = 1799 REPULSERFIELD12 = 1800 DESTRUCTIBLEWALLCORNER45ULBL = 1801 DESTRUCTIBLEWALLCORNER45ULUR = 1802 DESTRUCTIBLEWALLCORNER45URBR = 1803 DESTRUCTIBLEWALLCORNER45 = 1804 DESTRUCTIBLEWALLCORNER45UR90L = 1805 DESTRUCTIBLEWALLCORNER45UL90B = 1806 DESTRUCTIBLEWALLCORNER45BL90R = 1807 DESTRUCTIBLEWALLCORNER45BR90T = 1808 DESTRUCTIBLEWALLCORNER90L45BR = 1809 DESTRUCTIBLEWALLCORNER90T45BL = 1810 DESTRUCTIBLEWALLCORNER90R45UL = 1811 DESTRUCTIBLEWALLCORNER90B45UR = 1812 DESTRUCTIBLEWALLCORNER90TR = 1813 DESTRUCTIBLEWALLCORNER90BR = 1814 DESTRUCTIBLEWALLCORNER90LB = 1815 DESTRUCTIBLEWALLCORNER90LT = 1816 DESTRUCTIBLEWALLDIAGONALBLUR = 1817 DESTRUCTIBLEWALLDIAGONALBLURLF = 1818 DESTRUCTIBLEWALLDIAGONALULBRLF = 1819 DESTRUCTIBLEWALLDIAGONALULBR = 1820 DESTRUCTIBLEWALLSTRAIGHTVERTICAL = 1821 DESTRUCTIBLEWALLVERTICALLF = 1822 DESTRUCTIBLEWALLSTRAIGHTHORIZONTAL = 1823 DESTRUCTIBLEWALLSTRAIGHTHORIZONTALBF = 1824 DEFENSEWALLE = 1825 DEFENSEWALLS = 1826 DEFENSEWALLW = 1827 DEFENSEWALLN = 1828 DEFENSEWALLNE = 1829 DEFENSEWALLSW = 1830 DEFENSEWALLNW = 1831 DEFENSEWALLSE = 1832 WRECKEDBATTLECRUISERHELIOSFINAL = 1833 FIREWORKSBLUE = 1834 FIREWORKSRED = 1835 FIREWORKSYELLOW = 1836 PURIFIERBLASTMARKUNIT = 1837 ITEMGRAVITYBOMBS = 1838 ITEMGRENADES = 1839 ITEMMEDKIT = 1840 ITEMMINES = 1841 REAPERPLACEMENT = 1842 QUEENZAGARAACGLUESCREENDUMMY = 1843 OVERSEERZAGARAACGLUESCREENDUMMY = 1844 STUKOVINFESTEDCIVILIANACGLUESCREENDUMMY = 1845 STUKOVINFESTEDMARINEACGLUESCREENDUMMY = 1846 STUKOVINFESTEDSIEGETANKACGLUESCREENDUMMY = 1847 STUKOVINFESTEDDIAMONDBACKACGLUESCREENDUMMY = 1848 STUKOVINFESTEDBANSHEEACGLUESCREENDUMMY = 1849 SILIBERATORACGLUESCREENDUMMY = 1850 STUKOVINFESTEDBUNKERACGLUESCREENDUMMY = 1851 STUKOVINFESTEDMISSILETURRETACGLUESCREENDUMMY = 1852 STUKOVBROODQUEENACGLUESCREENDUMMY = 1853 ZEALOTFENIXACGLUESCREENDUMMY = 1854 SENTRYFENIXACGLUESCREENDUMMY = 1855 ADEPTFENIXACGLUESCREENDUMMY = 1856 IMMORTALFENIXACGLUESCREENDUMMY = 1857 COLOSSUSFENIXACGLUESCREENDUMMY = 1858 DISRUPTORACGLUESCREENDUMMY = 1859 OBSERVERFENIXACGLUESCREENDUMMY = 1860 SCOUTACGLUESCREENDUMMY = 1861 CARRIERFENIXACGLUESCREENDUMMY = 1862 PHOTONCANNONFENIXACGLUESCREENDUMMY = 1863 PRIMALZERGLINGACGLUESCREENDUMMY = 1864 RAVASAURACGLUESCREENDUMMY = 1865 PRIMALROACHACGLUESCREENDUMMY = 1866 FIREROACHACGLUESCREENDUMMY = 1867 PRIMALGUARDIANACGLUESCREENDUMMY = 1868 PRIMALHYDRALISKACGLUESCREENDUMMY = 1869 PRIMALMUTALISKACGLUESCREENDUMMY = 1870 PRIMALIMPALERACGLUESCREENDUMMY = 1871 PRIMALSWARMHOSTACGLUESCREENDUMMY = 1872 CREEPERHOSTACGLUESCREENDUMMY = 1873 PRIMALULTRALISKACGLUESCREENDUMMY = 1874 TYRANNOZORACGLUESCREENDUMMY = 1875 PRIMALWURMACGLUESCREENDUMMY = 1876 HHREAPERACGLUESCREENDUMMY = 1877 HHWIDOWMINEACGLUESCREENDUMMY = 1878 HHHELLIONTANKACGLUESCREENDUMMY = 1879 HHWRAITHACGLUESCREENDUMMY = 1880 HHVIKINGACGLUESCREENDUMMY = 1881 HHBATTLECRUISERACGLUESCREENDUMMY = 1882 HHRAVENACGLUESCREENDUMMY = 1883 HHBOMBERPLATFORMACGLUESCREENDUMMY = 1884 HHMERCSTARPORTACGLUESCREENDUMMY = 1885 HHMISSILETURRETACGLUESCREENDUMMY = 1886 HIGHTEMPLARSKINPREVIEW = 1887 WARPPRISMSKINPREVIEW = 1888 SIEGETANKSKINPREVIEW = 1889 LIBERATORSKINPREVIEW = 1890 VIKINGSKINPREVIEW = 1891 STUKOVINFESTEDTROOPERACGLUESCREENDUMMY = 1892 XELNAGADESTRUCTIBLEBLOCKER6S = 1893 XELNAGADESTRUCTIBLEBLOCKER6SE = 1894 XELNAGADESTRUCTIBLEBLOCKER6E = 1895 XELNAGADESTRUCTIBLEBLOCKER6NE = 1896 XELNAGADESTRUCTIBLEBLOCKER6N = 1897 XELNAGADESTRUCTIBLEBLOCKER6NW = 1898 XELNAGADESTRUCTIBLEBLOCKER6W = 1899 XELNAGADESTRUCTIBLEBLOCKER6SW = 1900 XELNAGADESTRUCTIBLEBLOCKER8S = 1901 XELNAGADESTRUCTIBLEBLOCKER8SE = 1902 XELNAGADESTRUCTIBLEBLOCKER8E = 1903 XELNAGADESTRUCTIBLEBLOCKER8NE = 1904 XELNAGADESTRUCTIBLEBLOCKER8N = 1905 XELNAGADESTRUCTIBLEBLOCKER8NW = 1906 XELNAGADESTRUCTIBLEBLOCKER8W = 1907 XELNAGADESTRUCTIBLEBLOCKER8SW = 1908 SNOWGLAZESTARTERMP = 1909 SHIELDBATTERY = 1910 OBSERVERSIEGEMODE = 1911 OVERSEERSIEGEMODE = 1912 RAVENREPAIRDRONE = 1913 HIGHTEMPLARWEAPONMISSILE = 1914 CYCLONEMISSILELARGEAIRALTERNATIVE = 1915 RAVENSCRAMBLERMISSILE = 1916 RAVENREPAIRDRONERELEASEWEAPON = 1917 RAVENSHREDDERMISSILEWEAPON = 1918 INFESTEDACIDSPINESWEAPON = 1919 INFESTORENSNAREATTACKMISSILE = 1920 SNARE_PLACEHOLDER = 1921 TYCHUSREAPERACGLUESCREENDUMMY = 1922 TYCHUSFIREBATACGLUESCREENDUMMY = 1923 TYCHUSSPECTREACGLUESCREENDUMMY = 1924 TYCHUSMEDICACGLUESCREENDUMMY = 1925 TYCHUSMARAUDERACGLUESCREENDUMMY = 1926 TYCHUSWARHOUNDACGLUESCREENDUMMY = 1927 TYCHUSHERCACGLUESCREENDUMMY = 1928 TYCHUSGHOSTACGLUESCREENDUMMY = 1929 TYCHUSSCVAUTOTURRETACGLUESCREENDUMMY = 1930 ZERATULSTALKERACGLUESCREENDUMMY = 1931 ZERATULSENTRYACGLUESCREENDUMMY = 1932 ZERATULDARKTEMPLARACGLUESCREENDUMMY = 1933 ZERATULIMMORTALACGLUESCREENDUMMY = 1934 ZERATULOBSERVERACGLUESCREENDUMMY = 1935 ZERATULDISRUPTORACGLUESCREENDUMMY = 1936 ZERATULWARPPRISMACGLUESCREENDUMMY = 1937 ZERATULPHOTONCANNONACGLUESCREENDUMMY = 1938 RENEGADELONGBOLTMISSILEWEAPON = 1939 VIKING = 1940 RENEGADEMISSILETURRET = 1941 PARASITICBOMBRELAYDUMMY = 1942 REFINERYRICH = 1943 MECHAZERGLINGACGLUESCREENDUMMY = 1944 MECHABANELINGACGLUESCREENDUMMY = 1945 MECHAHYDRALISKACGLUESCREENDUMMY = 1946 MECHAINFESTORACGLUESCREENDUMMY = 1947 MECHACORRUPTORACGLUESCREENDUMMY = 1948 MECHAULTRALISKACGLUESCREENDUMMY = 1949 MECHAOVERSEERACGLUESCREENDUMMY = 1950 MECHALURKERACGLUESCREENDUMMY = 1951 MECHABATTLECARRIERLORDACGLUESCREENDUMMY = 1952 MECHASPINECRAWLERACGLUESCREENDUMMY = 1953 MECHASPORECRAWLERACGLUESCREENDUMMY = 1954 TROOPERMENGSKACGLUESCREENDUMMY = 1955 MEDIVACMENGSKACGLUESCREENDUMMY = 1956 BLIMPMENGSKACGLUESCREENDUMMY = 1957 MARAUDERMENGSKACGLUESCREENDUMMY = 1958 GHOSTMENGSKACGLUESCREENDUMMY = 1959 SIEGETANKMENGSKACGLUESCREENDUMMY = 1960 THORMENGSKACGLUESCREENDUMMY = 1961 VIKINGMENGSKACGLUESCREENDUMMY = 1962 BATTLECRUISERMENGSKACGLUESCREENDUMMY = 1963 BUNKERDEPOTMENGSKACGLUESCREENDUMMY = 1964 MISSILETURRETMENGSKACGLUESCREENDUMMY = 1965 ARTILLERYMENGSKACGLUESCREENDUMMY = 1966 LOADOUTSPRAY1 = 1967 LOADOUTSPRAY2 = 1968 LOADOUTSPRAY3 = 1969 LOADOUTSPRAY4 = 1970 LOADOUTSPRAY5 = 1971 LOADOUTSPRAY6 = 1972 LOADOUTSPRAY7 = 1973 LOADOUTSPRAY8 = 1974 LOADOUTSPRAY9 = 1975 LOADOUTSPRAY10 = 1976 LOADOUTSPRAY11 = 1977 LOADOUTSPRAY12 = 1978 LOADOUTSPRAY13 = 1979 LOADOUTSPRAY14 = 1980 PREVIEWBUNKERUPGRADED = 1981 INHIBITORZONESMALL = 1982 INHIBITORZONEMEDIUM = 1983 INHIBITORZONELARGE = 1984 ACCELERATIONZONESMALL = 1985 ACCELERATIONZONEMEDIUM = 1986 ACCELERATIONZONELARGE = 1987 ACCELERATIONZONEFLYINGSMALL = 1988 ACCELERATIONZONEFLYINGMEDIUM = 1989 ACCELERATIONZONEFLYINGLARGE = 1990 INHIBITORZONEFLYINGSMALL = 1991 INHIBITORZONEFLYINGMEDIUM = 1992 INHIBITORZONEFLYINGLARGE = 1993 ASSIMILATORRICH = 1994 EXTRACTORRICH = 1995 MINERALFIELD450 = 1996 MINERALFIELDOPAQUE = 1997 MINERALFIELDOPAQUE900 = 1998 COLLAPSIBLEROCKTOWERDEBRISRAMPLEFTGREEN = 1999 COLLAPSIBLEROCKTOWERDEBRISRAMPRIGHTGREEN = 2000 COLLAPSIBLEROCKTOWERPUSHUNITRAMPLEFTGREEN = 2001 COLLAPSIBLEROCKTOWERPUSHUNITRAMPRIGHTGREEN = 2002 COLLAPSIBLEROCKTOWERRAMPLEFTGREEN = 2003 COLLAPSIBLEROCKTOWERRAMPRIGHTGREEN = 2004 def __repr__(self) -> str: return f"UnitTypeId.{self.name}" for item in UnitTypeId: globals()[item.name] = item ================================================ FILE: sc2/ids/upgrade_id.py ================================================ from __future__ import annotations # DO NOT EDIT! # This file was automatically generated by "generate_ids.py" import enum class UpgradeId(enum.Enum): NULL = 0 CARRIERLAUNCHSPEEDUPGRADE = 1 GLIALRECONSTITUTION = 2 TUNNELINGCLAWS = 3 CHITINOUSPLATING = 4 HISECAUTOTRACKING = 5 TERRANBUILDINGARMOR = 6 TERRANINFANTRYWEAPONSLEVEL1 = 7 TERRANINFANTRYWEAPONSLEVEL2 = 8 TERRANINFANTRYWEAPONSLEVEL3 = 9 NEOSTEELFRAME = 10 TERRANINFANTRYARMORSLEVEL1 = 11 TERRANINFANTRYARMORSLEVEL2 = 12 TERRANINFANTRYARMORSLEVEL3 = 13 REAPERSPEED = 14 STIMPACK = 15 SHIELDWALL = 16 PUNISHERGRENADES = 17 SIEGETECH = 18 HIGHCAPACITYBARRELS = 19 BANSHEECLOAK = 20 MEDIVACCADUCEUSREACTOR = 21 RAVENCORVIDREACTOR = 22 HUNTERSEEKER = 23 DURABLEMATERIALS = 24 PERSONALCLOAKING = 25 GHOSTMOEBIUSREACTOR = 26 TERRANVEHICLEARMORSLEVEL1 = 27 TERRANVEHICLEARMORSLEVEL2 = 28 TERRANVEHICLEARMORSLEVEL3 = 29 TERRANVEHICLEWEAPONSLEVEL1 = 30 TERRANVEHICLEWEAPONSLEVEL2 = 31 TERRANVEHICLEWEAPONSLEVEL3 = 32 TERRANSHIPARMORSLEVEL1 = 33 TERRANSHIPARMORSLEVEL2 = 34 TERRANSHIPARMORSLEVEL3 = 35 TERRANSHIPWEAPONSLEVEL1 = 36 TERRANSHIPWEAPONSLEVEL2 = 37 TERRANSHIPWEAPONSLEVEL3 = 38 PROTOSSGROUNDWEAPONSLEVEL1 = 39 PROTOSSGROUNDWEAPONSLEVEL2 = 40 PROTOSSGROUNDWEAPONSLEVEL3 = 41 PROTOSSGROUNDARMORSLEVEL1 = 42 PROTOSSGROUNDARMORSLEVEL2 = 43 PROTOSSGROUNDARMORSLEVEL3 = 44 PROTOSSSHIELDSLEVEL1 = 45 PROTOSSSHIELDSLEVEL2 = 46 PROTOSSSHIELDSLEVEL3 = 47 OBSERVERGRAVITICBOOSTER = 48 GRAVITICDRIVE = 49 EXTENDEDTHERMALLANCE = 50 HIGHTEMPLARKHAYDARINAMULET = 51 PSISTORMTECH = 52 ZERGMELEEWEAPONSLEVEL1 = 53 ZERGMELEEWEAPONSLEVEL2 = 54 ZERGMELEEWEAPONSLEVEL3 = 55 ZERGGROUNDARMORSLEVEL1 = 56 ZERGGROUNDARMORSLEVEL2 = 57 ZERGGROUNDARMORSLEVEL3 = 58 ZERGMISSILEWEAPONSLEVEL1 = 59 ZERGMISSILEWEAPONSLEVEL2 = 60 ZERGMISSILEWEAPONSLEVEL3 = 61 OVERLORDSPEED = 62 OVERLORDTRANSPORT = 63 BURROW = 64 ZERGLINGATTACKSPEED = 65 ZERGLINGMOVEMENTSPEED = 66 HYDRALISKSPEED = 67 ZERGFLYERWEAPONSLEVEL1 = 68 ZERGFLYERWEAPONSLEVEL2 = 69 ZERGFLYERWEAPONSLEVEL3 = 70 ZERGFLYERARMORSLEVEL1 = 71 ZERGFLYERARMORSLEVEL2 = 72 ZERGFLYERARMORSLEVEL3 = 73 INFESTORENERGYUPGRADE = 74 CENTRIFICALHOOKS = 75 BATTLECRUISERENABLESPECIALIZATIONS = 76 BATTLECRUISERBEHEMOTHREACTOR = 77 PROTOSSAIRWEAPONSLEVEL1 = 78 PROTOSSAIRWEAPONSLEVEL2 = 79 PROTOSSAIRWEAPONSLEVEL3 = 80 PROTOSSAIRARMORSLEVEL1 = 81 PROTOSSAIRARMORSLEVEL2 = 82 PROTOSSAIRARMORSLEVEL3 = 83 WARPGATERESEARCH = 84 HALTECH = 85 CHARGE = 86 BLINKTECH = 87 ANABOLICSYNTHESIS = 88 OBVERSEINCUBATION = 89 VIKINGJOTUNBOOSTERS = 90 ORGANICCARAPACE = 91 INFESTORPERISTALSIS = 92 ABDOMINALFORTITUDE = 93 HYDRALISKSPEEDUPGRADE = 94 BANELINGBURROWMOVE = 95 COMBATDRUGS = 96 STRIKECANNONS = 97 TRANSFORMATIONSERVOS = 98 PHOENIXRANGEUPGRADE = 99 TEMPESTRANGEUPGRADE = 100 NEURALPARASITE = 101 LOCUSTLIFETIMEINCREASE = 102 ULTRALISKBURROWCHARGEUPGRADE = 103 ORACLEENERGYUPGRADE = 104 RESTORESHIELDS = 105 PROTOSSHEROSHIPWEAPON = 106 PROTOSSHEROSHIPDETECTOR = 107 PROTOSSHEROSHIPSPELL = 108 REAPERJUMP = 109 INCREASEDRANGE = 110 ZERGBURROWMOVE = 111 ANIONPULSECRYSTALS = 112 TERRANVEHICLEANDSHIPWEAPONSLEVEL1 = 113 TERRANVEHICLEANDSHIPWEAPONSLEVEL2 = 114 TERRANVEHICLEANDSHIPWEAPONSLEVEL3 = 115 TERRANVEHICLEANDSHIPARMORSLEVEL1 = 116 TERRANVEHICLEANDSHIPARMORSLEVEL2 = 117 TERRANVEHICLEANDSHIPARMORSLEVEL3 = 118 FLYINGLOCUSTS = 119 ROACHSUPPLY = 120 IMMORTALREVIVE = 121 DRILLCLAWS = 122 CYCLONELOCKONRANGEUPGRADE = 123 CYCLONEAIRUPGRADE = 124 LIBERATORMORPH = 125 ADEPTSHIELDUPGRADE = 126 LURKERRANGE = 127 IMMORTALBARRIER = 128 ADEPTKILLBOUNCE = 129 ADEPTPIERCINGATTACK = 130 CINEMATICMODE = 131 CURSORDEBUG = 132 MAGFIELDLAUNCHERS = 133 EVOLVEGROOVEDSPINES = 134 EVOLVEMUSCULARAUGMENTS = 135 BANSHEESPEED = 136 MEDIVACRAPIDDEPLOYMENT = 137 RAVENRECALIBRATEDEXPLOSIVES = 138 MEDIVACINCREASESPEEDBOOST = 139 LIBERATORAGRANGEUPGRADE = 140 DARKTEMPLARBLINKUPGRADE = 141 RAVAGERRANGE = 142 RAVENDAMAGEUPGRADE = 143 CYCLONELOCKONDAMAGEUPGRADE = 144 ARESCLASSWEAPONSSYSTEMVIKING = 145 AUTOHARVESTER = 146 HYBRIDCPLASMAUPGRADEHARD = 147 HYBRIDCPLASMAUPGRADEINSANE = 148 INTERCEPTORLIMIT4 = 149 INTERCEPTORLIMIT6 = 150 _330MMBARRAGECANNONS = 151 NOTPOSSIBLESIEGEMODE = 152 NEOSTEELFRAME_2 = 153 NEOSTEELANDSHRIKETURRETICONUPGRADE = 154 OCULARIMPLANTS = 155 CROSSSPECTRUMDAMPENERS = 156 ORBITALSTRIKE = 157 CLUSTERBOMB = 158 SHAPEDHULL = 159 SPECTRETOOLTIPUPGRADE = 160 ULTRACAPACITORS = 161 VANADIUMPLATING = 162 COMMANDCENTERREACTOR = 163 REGENERATIVEBIOSTEEL = 164 CELLULARREACTORS = 165 BANSHEECLOAKEDDAMAGE = 166 DISTORTIONBLASTERS = 167 EMPTOWER = 168 SUPPLYDEPOTDROP = 169 HIVEMINDEMULATOR = 170 FORTIFIEDBUNKERCARAPACE = 171 PREDATOR = 172 SCIENCEVESSEL = 173 DUALFUSIONWELDERS = 174 ADVANCEDCONSTRUCTION = 175 ADVANCEDMEDICTRAINING = 176 PROJECTILEACCELERATORS = 177 REINFORCEDSUPERSTRUCTURE = 178 MULE = 179 ORBITALRELAY = 180 RAZORWIRE = 181 ADVANCEDHEALINGAI = 182 TWINLINKEDFLAMETHROWERS = 183 NANOCONSTRUCTOR = 184 CERBERUSMINES = 185 HYPERFLUXOR = 186 TRILITHIUMPOWERCELLS = 187 PERMANENTCLOAKGHOST = 188 PERMANENTCLOAKSPECTRE = 189 ULTRASONICPULSE = 190 SURVIVALPODS = 191 ENERGYSTORAGE = 192 FULLBORECANISTERAMMO = 193 CAMPAIGNJOTUNBOOSTERS = 194 MICROFILTERING = 195 PARTICLECANNONAIR = 196 VULTUREAUTOREPAIR = 197 PSIDISRUPTOR = 198 SCIENCEVESSELENERGYMANIPULATION = 199 SCIENCEVESSELPLASMAWEAPONRY = 200 SHOWGATLINGGUN = 201 TECHREACTOR = 202 TECHREACTORAI = 203 TERRANDEFENSERANGEBONUS = 204 X88TNAPALMUPGRADE = 205 HURRICANEMISSILES = 206 MECHANICALREBIRTH = 207 MARINESTIMPACK = 208 DARKTEMPLARTACTICS = 209 CLUSTERWARHEADS = 210 CLOAKDISTORTIONFIELD = 211 DEVASTATORMISSILES = 212 DISTORTIONTHRUSTERS = 213 DYNAMICPOWERROUTING = 214 IMPALERROUNDS = 215 KINETICFIELDS = 216 BURSTCAPACITORS = 217 HAILSTORMMISSILEPODS = 218 RAPIDDEPLOYMENT = 219 REAPERSTIMPACK = 220 REAPERD8CHARGE = 221 TYCHUS05BATTLECRUISERPENETRATION = 222 VIRALPLASMA = 223 FIREBATJUGGERNAUTPLATING = 224 MULTILOCKTARGETINGSYSTEMS = 225 TURBOCHARGEDENGINES = 226 DISTORTIONSENSORS = 227 INFERNALPREIGNITERS = 228 HELLIONCAMPAIGNINFERNALPREIGNITER = 229 NAPALMFUELTANKS = 230 AUXILIARYMEDBOTS = 231 JUGGERNAUTPLATING = 232 MARAUDERLIFEBOOST = 233 COMBATSHIELD = 234 REAPERU238ROUNDS = 235 MAELSTROMROUNDS = 236 SIEGETANKSHAPEDBLAST = 237 TUNGSTENSPIKES = 238 BEARCLAWNOZZLES = 239 NANOBOTINJECTORS = 240 STABILIZERMEDPACKS = 241 HALOROCKETS = 242 SCAVENGINGSYSTEMS = 243 EXTRAMINES = 244 ARESCLASSWEAPONSSYSTEM = 245 WHITENAPALM = 246 VIRALMUNITIONS = 247 JACKHAMMERCONCUSSIONGRENADES = 248 FIRESUPPRESSIONSYSTEMS = 249 FLARERESEARCH = 250 MODULARCONSTRUCTION = 251 EXPANDEDHULL = 252 SHRIKETURRET = 253 MICROFUSIONREACTORS = 254 WRAITHCLOAK = 255 SINGULARITYCHARGE = 256 GRAVITICTHRUSTERS = 257 YAMATOCANNON = 258 DEFENSIVEMATRIX = 259 DARKPROTOSS = 260 TERRANINFANTRYWEAPONSULTRACAPACITORSLEVEL1 = 261 TERRANINFANTRYWEAPONSULTRACAPACITORSLEVEL2 = 262 TERRANINFANTRYWEAPONSULTRACAPACITORSLEVEL3 = 263 TERRANINFANTRYARMORSVANADIUMPLATINGLEVEL1 = 264 TERRANINFANTRYARMORSVANADIUMPLATINGLEVEL2 = 265 TERRANINFANTRYARMORSVANADIUMPLATINGLEVEL3 = 266 TERRANVEHICLEWEAPONSULTRACAPACITORSLEVEL1 = 267 TERRANVEHICLEWEAPONSULTRACAPACITORSLEVEL2 = 268 TERRANVEHICLEWEAPONSULTRACAPACITORSLEVEL3 = 269 TERRANVEHICLEARMORSVANADIUMPLATINGLEVEL1 = 270 TERRANVEHICLEARMORSVANADIUMPLATINGLEVEL2 = 271 TERRANVEHICLEARMORSVANADIUMPLATINGLEVEL3 = 272 TERRANSHIPWEAPONSULTRACAPACITORSLEVEL1 = 273 TERRANSHIPWEAPONSULTRACAPACITORSLEVEL2 = 274 TERRANSHIPWEAPONSULTRACAPACITORSLEVEL3 = 275 TERRANSHIPARMORSVANADIUMPLATINGLEVEL1 = 276 TERRANSHIPARMORSVANADIUMPLATINGLEVEL2 = 277 TERRANSHIPARMORSVANADIUMPLATINGLEVEL3 = 278 HIREKELMORIANMINERSPH = 279 HIREDEVILDOGSPH = 280 HIRESPARTANCOMPANYPH = 281 HIREHAMMERSECURITIESPH = 282 HIRESIEGEBREAKERSPH = 283 HIREHELSANGELSPH = 284 HIREDUSKWINGPH = 285 HIREDUKESREVENGE = 286 TOSHEASYMODE = 287 VOIDRAYSPEEDUPGRADE = 288 SMARTSERVOS = 289 ARMORPIERCINGROCKETS = 290 CYCLONERAPIDFIRELAUNCHERS = 291 RAVENENHANCEDMUNITIONS = 292 DIGGINGCLAWS = 293 CARRIERCARRIERCAPACITY = 294 CARRIERLEASHRANGEUPGRADE = 295 HURRICANETHRUSTERS = 296 TEMPESTGROUNDATTACKUPGRADE = 297 FRENZY = 298 MICROBIALSHROUD = 299 INTERFERENCEMATRIX = 300 SUNDERINGIMPACT = 301 AMPLIFIEDSHIELDING = 302 PSIONICAMPLIFIERS = 303 SECRETEDCOATING = 304 ENHANCEDSHOCKWAVES = 305 def __repr__(self) -> str: return f"UpgradeId.{self.name}" for item in UpgradeId: globals()[item.name] = item ================================================ FILE: sc2/main.py ================================================ from __future__ import annotations import asyncio import json import platform import signal import sys from contextlib import suppress from dataclasses import dataclass from io import BytesIO from pathlib import Path from typing import Any import mpyq import portpicker from aiohttp import ClientSession, ClientWebSocketResponse from loguru import logger from s2clientprotocol import sc2api_pb2 as sc_pb from sc2.bot_ai import BotAI from sc2.client import Client from sc2.controller import Controller from sc2.data import CreateGameError, Result, Status from sc2.game_state import GameState from sc2.maps import Map from sc2.observer_ai import ObserverAI from sc2.player import AbstractPlayer, Bot, BotProcess, Computer, Human from sc2.portconfig import Portconfig from sc2.protocol import ConnectionAlreadyClosedError, ProtocolError from sc2.proxy import Proxy from sc2.sc2process import KillSwitch, SC2Process # Set the global logging level logger.remove() logger.add(sys.stdout, level="INFO") @dataclass class GameMatch: """Dataclass for hosting a match of SC2. This contains all of the needed information for RequestCreateGame. :param sc2_config: dicts of arguments to unpack into sc2process's construction, one per player second sc2_config will be ignored if only one sc2_instance is spawned e.g. sc2_args=[{"fullscreen": True}, {}]: only player 1's sc2instance will be fullscreen :param game_time_limit: The time (in seconds) until a match is artificially declared a Tie """ map_sc2: Map players: list[AbstractPlayer] realtime: bool = False random_seed: int | None = None disable_fog: bool | None = None sc2_config: list[dict] | None = None game_time_limit: int | None = None def __post_init__(self) -> None: # avoid players sharing names if ( len(self.players) > 1 and self.players[0].name is not None and self.players[1].name is not None and self.players[0].name == self.players[1].name ): self.players[1].name += "2" if self.sc2_config is not None: if isinstance(self.sc2_config, dict): self.sc2_config = [self.sc2_config] if len(self.sc2_config) == 0: self.sc2_config = [{}] while len(self.sc2_config) < len(self.players): self.sc2_config += self.sc2_config self.sc2_config = self.sc2_config[: len(self.players)] @property def needed_sc2_count(self) -> int: return sum(player.needs_sc2 for player in self.players) @property def host_game_kwargs(self) -> dict[str, Any]: return { "map_settings": self.map_sc2, "players": self.players, "realtime": self.realtime, "random_seed": self.random_seed, "disable_fog": self.disable_fog, } def __repr__(self) -> str: p1 = self.players[0] p1 = p1.name if p1.name else p1 p2 = self.players[1] p2 = p2.name if p2.name else p2 return f"Map: {self.map_sc2.name}, {p1} vs {p2}, realtime={self.realtime}, seed={self.random_seed}" async def _play_game_human(client, player_id, realtime, game_time_limit): while True: state = await client.observation() if client._game_result: return client._game_result[player_id] if game_time_limit and state.observation.observation.game_loop / 22.4 > game_time_limit: logger.info(state.observation.game_loop, state.observation.game_loop / 22.4) return Result.Tie if not realtime: await client.step() async def _play_game_ai( client: Client, player_id: int, ai: BotAI, realtime: bool, game_time_limit: int | None ) -> Result: # pyrefly: ignore gs: GameState = None async def initialize_first_step() -> Result | None: nonlocal gs ai._initialize_variables() game_data = await client.get_game_data() game_info = await client.get_game_info() ping_response = await client.ping() # This game_data will become self.game_data in botAI ai._prepare_start( client, player_id, game_info, game_data, realtime=realtime, base_build=ping_response.ping.base_build ) state = await client.observation() # check game result every time we get the observation if client._game_result: await ai.on_end(client._game_result[player_id]) return client._game_result[player_id] gs = GameState(state.observation) proto_game_info = await client._execute(game_info=sc_pb.RequestGameInfo()) try: ai._prepare_step(gs, proto_game_info) await ai.on_before_start() ai._prepare_first_step() await ai.on_start() # TODO Catching too general exception Exception (broad-except) except Exception as e: logger.exception(f"Caught unknown exception in AI on_start: {e}") logger.error("Resigning due to previous error") await ai.on_end(Result.Defeat) return Result.Defeat result = await initialize_first_step() if result is not None: return result async def run_bot_iteration(iteration: int): nonlocal gs logger.debug(f"Running AI step, it={iteration} {gs.game_loop / 22.4:.2f}s") # Issue event like unit created or unit destroyed await ai.issue_events() # In on_step various errors can occur - log properly try: await ai.on_step(iteration) except (AttributeError,) as e: logger.exception(f"Caught exception: {e}") raise except Exception as e: logger.exception(f"Caught unknown exception: {e}") raise await ai._after_step() logger.debug("Running AI step: done") # Only used in realtime=True previous_state_observation = None for iteration in range(10**10): if realtime and gs: # On realtime=True, might get an error here: sc2.protocol.ProtocolError: ['Not in a game'] with suppress(ProtocolError): requested_step = gs.game_loop + client.game_step state = await client.observation(requested_step) # If the bot took too long in the previous observation, request another observation one frame after if state.observation.observation.game_loop > requested_step: logger.debug("Skipped a step in realtime=True") previous_state_observation = state.observation state = await client.observation(state.observation.observation.game_loop + 1) else: state = await client.observation() # check game result every time we get the observation if client._game_result: await ai.on_end(client._game_result[player_id]) return client._game_result[player_id] gs = GameState(state.observation, previous_state_observation) previous_state_observation = None logger.debug(f"Score: {gs.score.score}") if game_time_limit and gs.game_loop / 22.4 > game_time_limit: await ai.on_end(Result.Tie) return Result.Tie proto_game_info = await client._execute(game_info=sc_pb.RequestGameInfo()) ai._prepare_step(gs, proto_game_info) await run_bot_iteration(iteration) # Main bot loop if not realtime: if not client.in_game: # Client left (resigned) the game await ai.on_end(client._game_result[player_id]) return client._game_result[player_id] # TODO: In bot vs bot, if the other bot ends the game, this bot gets stuck in requesting an observation when using main.py:run_multiple_games await client.step() return Result.Undecided async def _play_game( player: Human | Bot, client: Client, realtime: bool, portconfig: Portconfig | None = None, game_time_limit: int | None = None, rgb_render_config: dict[str, Any] | None = None, ) -> Result: assert isinstance(realtime, bool), repr(realtime) player_id = await client.join_game( player.name, player.race, portconfig=portconfig, rgb_render_config=rgb_render_config ) logger.info(f"Player {player_id} - {player.name if player.name else str(player)}") if isinstance(player, Human): result = await _play_game_human(client, player_id, realtime, game_time_limit) else: result = await _play_game_ai(client, player_id, player.ai, realtime, game_time_limit) logger.info( f"Result for player {player_id} - {player.name if player.name else str(player)}: " f"{result._name_ if isinstance(result, Result) else result}" ) return result async def _play_replay(client: Client, ai, realtime: bool = False, player_id: int = 0): ai._initialize_variables() game_data = await client.get_game_data() game_info = await client.get_game_info() ping_response = await client.ping() client.game_step = 1 # This game_data will become self._game_data in botAI ai._prepare_start( client, player_id, game_info, game_data, realtime=realtime, base_build=ping_response.ping.base_build ) state = await client.observation() # Check game result every time we get the observation if client._game_result: await ai.on_end(client._game_result[player_id]) return client._game_result[player_id] gs = GameState(state.observation) proto_game_info = await client._execute(game_info=sc_pb.RequestGameInfo()) ai._prepare_step(gs, proto_game_info) ai._prepare_first_step() try: await ai.on_start() # TODO Catching too general exception Exception (broad-except) except Exception as e: logger.exception(f"Caught unknown exception in AI replay on_start: {e}") await ai.on_end(Result.Defeat) return Result.Defeat iteration = 0 while True: if iteration != 0: if realtime: # TODO: check what happens if a bot takes too long to respond, so that the requested # game_loop might already be in the past state = await client.observation(gs.game_loop + client.game_step) else: state = await client.observation() # check game result every time we get the observation if client._game_result: try: await ai.on_end(client._game_result[player_id]) except TypeError: return client._game_result[player_id] return client._game_result[player_id] gs = GameState(state.observation) logger.debug(f"Score: {gs.score.score}") proto_game_info = await client._execute(game_info=sc_pb.RequestGameInfo()) ai._prepare_step(gs, proto_game_info) logger.debug(f"Running AI step, it={iteration} {gs.game_loop * 0.725 * (1 / 16):.2f}s") try: # Issue event like unit created or unit destroyed await ai.issue_events() await ai.on_step(iteration) await ai._after_step() # TODO Catching too general exception Exception (broad-except) except Exception as e: if isinstance(e, ProtocolError) and e.is_game_over_error: if realtime: return None await ai.on_end(Result.Victory) return None # NOTE: this message is caught by pytest suite logger.exception("AI step threw an error") # DO NOT EDIT! logger.error(f"Error: {e}") logger.error("Resigning due to previous error") try: await ai.on_end(Result.Defeat) except TypeError: return Result.Defeat return Result.Defeat logger.debug("Running AI step: done") if not realtime and not client.in_game: # Client left (resigned) the game await ai.on_end(Result.Victory) return Result.Victory await client.step() # unindent one line to work in realtime iteration += 1 async def _setup_host_game( server: Controller, map_settings, players, realtime, random_seed=None, disable_fog=None, save_replay_as=None ): r = await server.create_game(map_settings, players, realtime, random_seed, disable_fog) if r.create_game.HasField("error"): err = f"Could not create game: {CreateGameError(r.create_game.error)}" if r.create_game.HasField("error_details"): err += f": {r.create_game.error_details}" logger.critical(err) raise RuntimeError(err) return Client(server._ws, save_replay_as) async def _host_game( map_settings: Map, players: list[Human | Bot | Computer] | list[Human | Bot], realtime: bool = False, portconfig: Portconfig | None = None, save_replay_as: str | None = None, game_time_limit: int | None = None, rgb_render_config: dict[str, Any] | None = None, random_seed: int | None = None, sc2_version: str | None = None, disable_fog: bool = False, ): assert players, "Can't create a game without players" assert any((isinstance(p, (Human, Bot))) for p in players) assert isinstance(players[0], (Human, Bot)), "First player needs to be a Human or a Bot" async with SC2Process( fullscreen=players[0].fullscreen, render=rgb_render_config is not None, sc2_version=sc2_version ) as server: await server.ping() client = await _setup_host_game( server, map_settings, players, realtime, random_seed, disable_fog, save_replay_as ) # Bot can decide if it wants to launch with 'raw_affects_selection=True' if isinstance(players[0], Bot) and getattr(players[0].ai, "raw_affects_selection", None) is not None: client.raw_affects_selection = players[0].ai.raw_affects_selection result = await _play_game(players[0], client, realtime, portconfig, game_time_limit, rgb_render_config) if client.save_replay_path is not None: await client.save_replay(client.save_replay_path) try: await client.leave() except ConnectionAlreadyClosedError: logger.error("Connection was closed before the game ended") await client.quit() return result async def _host_game_aiter( map_settings, players, realtime, portconfig, save_replay_as=None, game_time_limit=None, ): assert players, "Can't create a game without players" assert any(isinstance(p, (Human, Bot)) for p in players) async with SC2Process() as server: while True: await server.ping() client = await _setup_host_game(server, map_settings, players, realtime) if not isinstance(players[0], Human) and getattr(players[0].ai, "raw_affects_selection", None) is not None: client.raw_affects_selection = players[0].ai.raw_affects_selection try: result = await _play_game(players[0], client, realtime, portconfig, game_time_limit) if save_replay_as is not None: await client.save_replay(save_replay_as) await client.leave() except ConnectionAlreadyClosedError: logger.error("Connection was closed before the game ended") return new_players = yield result if new_players is not None: players = new_players def _host_game_iter(*args, **kwargs): game = _host_game_aiter(*args, **kwargs) new_playerconfig = None while True: new_playerconfig = yield asyncio.get_event_loop().run_until_complete(game.asend(new_playerconfig)) async def _join_game( players: list[Human | Bot], realtime: bool, portconfig: Portconfig, save_replay_as: str | None = None, game_time_limit: int | None = None, sc2_version: str | None = None, ): async with SC2Process(fullscreen=players[1].fullscreen, sc2_version=sc2_version) as server: await server.ping() client = Client(server._ws) # Bot can decide if it wants to launch with 'raw_affects_selection=True' if isinstance(players[1], Bot) and getattr(players[1].ai, "raw_affects_selection", None) is not None: client.raw_affects_selection = players[1].ai.raw_affects_selection result = await _play_game(players[1], client, realtime, portconfig, game_time_limit) if save_replay_as is not None: await client.save_replay(save_replay_as) try: await client.leave() except ConnectionAlreadyClosedError: logger.error("Connection was closed before the game ended") await client.quit() return result async def _setup_replay(server, replay_path, realtime, observed_id): await server.start_replay(replay_path, realtime, observed_id) return Client(server._ws) async def _host_replay( replay_path, ai: ObserverAI, realtime: bool, _portconfig: Portconfig, base_build, data_version, observed_id ): async with SC2Process(fullscreen=False, base_build=base_build, data_hash=data_version) as server: client = await _setup_replay(server, replay_path, realtime, observed_id) result = await _play_replay(client, ai, realtime) return result def get_replay_version(replay_path: str | Path) -> tuple[str, str]: with Path(replay_path).open("rb") as f: replay_data = f.read() replay_io = BytesIO() replay_io.write(replay_data) replay_io.seek(0) archive = mpyq.MPQArchive(replay_io).extract() # pyrefly: ignore metadata = json.loads(archive[b"replay.gamemetadata.json"].decode("utf-8")) return metadata["BaseBuild"], metadata["DataVersion"] # TODO Deprecate run_game function in favor of run_multiple_games def run_game( map_settings: Map, players: list[Human | Bot | Computer], realtime: bool, portconfig: Portconfig | None = None, save_replay_as: str | None = None, game_time_limit: int | None = None, rgb_render_config: dict[str, Any] | None = None, random_seed: int | None = None, sc2_version: str | None = None, disable_fog: bool = False, ) -> Result | list[Result | None]: """ Returns a single Result enum if the game was against the built-in computer. Returns a list of two Result enums if the game was "Human vs Bot" or "Bot vs Bot". """ result: Result | list[Result | None] if sum(isinstance(p, (Human, Bot)) for p in players) > 1: portconfig = Portconfig() players_non_computer: list[Human | Bot] = [p for p in players if isinstance(p, (Human, Bot))] async def run_host_and_join(): return await asyncio.gather( _host_game( map_settings, players_non_computer, realtime=realtime, portconfig=portconfig, save_replay_as=save_replay_as, game_time_limit=game_time_limit, rgb_render_config=rgb_render_config, random_seed=random_seed, sc2_version=sc2_version, disable_fog=disable_fog, ), _join_game( players_non_computer, realtime=realtime, portconfig=portconfig, save_replay_as=save_replay_as, game_time_limit=game_time_limit, sc2_version=sc2_version, ), return_exceptions=True, ) # pyrefly: ignore result = asyncio.run(run_host_and_join()) assert isinstance(result, list) assert all(isinstance(r, Result) for r in result) else: result = asyncio.run( _host_game( map_settings, players, realtime=realtime, portconfig=portconfig, save_replay_as=save_replay_as, game_time_limit=game_time_limit, rgb_render_config=rgb_render_config, random_seed=random_seed, sc2_version=sc2_version, disable_fog=disable_fog, ) ) assert isinstance(result, Result) return result def run_replay(ai: ObserverAI, replay_path: Path | str, realtime: bool = False, observed_id: int = 0): portconfig = Portconfig() assert Path(replay_path).is_file(), f"Replay does not exist at the given path: {replay_path}" assert Path(replay_path).is_absolute(), ( f'Replay path has to be an absolute path, e.g. "C:/replays/my_replay.SC2Replay" but given path was "{replay_path}"' ) base_build, data_version = get_replay_version(replay_path) result = asyncio.get_event_loop().run_until_complete( _host_replay(replay_path, ai, realtime, portconfig, base_build, data_version, observed_id) ) return result async def play_from_websocket( ws_connection: str | ClientWebSocketResponse, player: Human | Bot, realtime: bool, portconfig: Portconfig, save_replay_as: str | None = None, game_time_limit: int | None = None, should_close: bool = True, ): """Use this to play when the match is handled externally e.g. for bot ladder games. Portconfig MUST be specified if not playing vs Computer. :param ws_connection: either a string("ws://{address}:{port}/sc2api") or a ClientWebSocketResponse object :param should_close: closes the connection if True. Use False if something else will reuse the connection e.g. ladder usage: play_from_websocket("ws://127.0.0.1:5162/sc2api", MyBot, False, portconfig=my_PC) """ session = None try: if isinstance(ws_connection, str): session = ClientSession() # pyrefly: ignore ws_connection = await session.ws_connect(ws_connection, timeout=120) should_close = True client = Client(ws_connection) result = await _play_game(player, client, realtime, portconfig, game_time_limit=game_time_limit) if save_replay_as is not None: await client.save_replay(save_replay_as) except ConnectionAlreadyClosedError: logger.error("Connection was closed before the game ended") return None finally: if should_close: await ws_connection.close() if session: await session.close() return result async def run_match(controllers: list[Controller], match: GameMatch, close_ws: bool = True): await _setup_host_game(controllers[0], **match.host_game_kwargs) # Setup portconfig beforehand, so all players use the same ports startport = None portconfig: Portconfig = None # pyrefly: ignore if match.needed_sc2_count > 1: if any(isinstance(player, BotProcess) for player in match.players): portconfig = Portconfig.contiguous_ports() # Most ladder bots generate their server and client ports as [s+2, s+3], [s+4, s+5] startport = portconfig.server[0] - 2 else: portconfig = Portconfig() proxies = [] coros = [] players_that_need_sc2 = filter(lambda lambda_player: lambda_player.needs_sc2, match.players) for i, player in enumerate(players_that_need_sc2): if isinstance(player, BotProcess): pport = portpicker.pick_unused_port() p = Proxy(controllers[i], player, pport, match.game_time_limit, match.realtime) proxies.append(p) coros.append(p.play_with_proxy(startport)) else: coros.append( play_from_websocket( controllers[i]._ws, player, match.realtime, portconfig, should_close=close_ws, game_time_limit=match.game_time_limit, ) ) async_results = await asyncio.gather(*coros, return_exceptions=True) for i, a in enumerate(async_results): if isinstance(a, Exception): logger.error(f"Exception[{a}] thrown by {[p for p in match.players if p.needs_sc2][i]}") # TODO async_results may contain exceptions # pyrefly: ignore return process_results(match.players, async_results) def process_results(players: list[AbstractPlayer], async_results: list[Result]) -> dict[AbstractPlayer, Result]: opp_res = {Result.Victory: Result.Defeat, Result.Defeat: Result.Victory, Result.Tie: Result.Tie} result: dict[AbstractPlayer, Result] = {} i = 0 for player in players: if player.needs_sc2: if sum(r == Result.Victory for r in async_results) <= 1: result[player] = async_results[i] else: result[player] = Result.Undecided i += 1 else: # Computer other_result = async_results[0] result[player] = Result.Undecided if other_result in opp_res: result[player] = opp_res[other_result] return result async def maintain_SCII_count(count: int, controllers: list[Controller], proc_args: list[dict] | None = None) -> None: """Modifies the given list of controllers to reflect the desired amount of SCII processes""" # kill unhealthy ones. if controllers: to_remove = [] alive = await asyncio.wait_for( # pyrefly: ignore asyncio.gather(*(c.ping() for c in controllers if not c._ws.closed), return_exceptions=True), timeout=20, ) i = 0 # for alive for controller in controllers: if controller._ws.closed: if controller._process._session is not None and not controller._process._session.closed: await controller._process._session.close() to_remove.append(controller) else: if not isinstance(alive[i], sc_pb.Response): try: await controller._process._close_connection() finally: to_remove.append(controller) i += 1 for c in to_remove: c._process._clean(verbose=False) if c._process in KillSwitch._to_kill: KillSwitch._to_kill.remove(c._process) controllers.remove(c) # spawn more if len(controllers) < count: needed = count - len(controllers) if proc_args: index = len(controllers) % len(proc_args) else: proc_args = [{} for _ in range(needed)] index = 0 extra = [SC2Process(**proc_args[(index + _) % len(proc_args)]) for _ in range(needed)] logger.info(f"Creating {needed} more SC2 Processes") for _ in range(3): if platform.system() == "Linux": # Works on linux: start one client after the other new_controllers = [await asyncio.wait_for(sc.__aenter__(), timeout=50) for sc in extra] else: # Doesnt seem to work on linux: starting 2 clients nearly at the same time new_controllers = await asyncio.wait_for( # pyrefly: ignore asyncio.gather(*[sc.__aenter__() for sc in extra], return_exceptions=True), timeout=50, ) controllers.extend(c for c in new_controllers if isinstance(c, Controller)) if len(controllers) == count: # pyrefly: ignore await asyncio.wait_for(asyncio.gather(*(c.ping() for c in controllers)), timeout=20) break extra = [ extra[i] for i, result in enumerate(new_controllers) if not isinstance(new_controllers, Controller) ] else: logger.critical("Could not launch sufficient SC2") raise RuntimeError # kill excess while len(controllers) > count: proc = controllers.pop() proc = proc._process logger.info(f"Removing SCII listening to {proc._port}") await proc._close_connection() proc._clean(verbose=False) if proc in KillSwitch._to_kill: KillSwitch._to_kill.remove(proc) def run_multiple_games(matches: list[GameMatch]): return asyncio.get_event_loop().run_until_complete(a_run_multiple_games(matches)) # TODO Catching too general exception Exception (broad-except) async def a_run_multiple_games(matches: list[GameMatch]) -> list[dict[AbstractPlayer, Result]]: """Run multiple matches. Non-python bots are supported. When playing bot vs bot, this is less likely to fatally crash than repeating run_game() """ if not matches: return [] results: list[dict[AbstractPlayer, Result]] = [] controllers: list[Controller] = [] for m in matches: result = None dont_restart = m.needed_sc2_count == 2 try: await maintain_SCII_count(m.needed_sc2_count, controllers, m.sc2_config) result = await run_match(controllers, m, close_ws=dont_restart) except SystemExit as e: logger.info(f"Game exit'ed as {e} during match {m}") except Exception as e: logger.exception(f"Caught unknown exception: {e}") logger.info(f"Exception {e} thrown in match {m}") finally: if dont_restart: # Keeping them alive after a non-computer match can cause crashes await maintain_SCII_count(0, controllers, m.sc2_config) if result is not None: results.append(result) KillSwitch.kill_all() return results # TODO Catching too general exception Exception (broad-except) async def a_run_multiple_games_nokill(matches: list[GameMatch]) -> list[dict[AbstractPlayer, Result]]: """Run multiple matches while reusing SCII processes. Prone to crashes and stalls """ # FIXME: check whether crashes between bot-vs-bot are avoidable or not if not matches: return [] # Start the matches results: list[dict[AbstractPlayer, Result]] = [] controllers: list[Controller] = [] for m in matches: logger.info(f"Starting match {1 + len(results)} / {len(matches)}: {m}") result = None try: await maintain_SCII_count(m.needed_sc2_count, controllers, m.sc2_config) result = await run_match(controllers, m, close_ws=False) except SystemExit as e: logger.critical(f"Game sys.exit'ed as {e} during match {m}") except Exception as e: logger.exception(f"Caught unknown exception: {e}") logger.info(f"Exception {e} thrown in match {m}") finally: for c in controllers: try: await c.ping() if c._status != Status.launched: await c._execute(leave_game=sc_pb.RequestLeaveGame()) except Exception as e: logger.exception(f"Caught unknown exception: {e}") if not (isinstance(e, ProtocolError) and e.is_game_over_error): logger.info(f"controller {c.__dict__} threw {e}") if result is not None: results.append(result) # Fire the killswitch manually, instead of letting the winning player fire it. # pyrefly: ignore await asyncio.wait_for(asyncio.gather(*(c._process._close_connection() for c in controllers)), timeout=50) KillSwitch.kill_all() signal.signal(signal.SIGINT, signal.SIG_DFL) return results ================================================ FILE: sc2/maps.py ================================================ from __future__ import annotations from pathlib import Path from loguru import logger from sc2.paths import Paths def get(name: str) -> Map: # Iterate through 2 folder depths for map_dir in (p for p in Paths.MAPS.iterdir()): if map_dir.is_dir(): for map_file in (p for p in map_dir.iterdir()): if Map.matches_target_map_name(map_file, name): return Map(map_file) elif Map.matches_target_map_name(map_dir, name): return Map(map_dir) raise KeyError(f"Map '{name}' was not found. Please put the map file in \"/StarCraft II/Maps/\".") class Map: def __init__(self, path: Path) -> None: self.path = path if self.path.is_absolute(): try: self.relative_path = self.path.relative_to(Paths.MAPS) except ValueError: # path not relative to basedir logger.warning(f"Using absolute path: {self.path}") self.relative_path = self.path else: self.relative_path = self.path @property def name(self) -> str: return self.path.stem @property def data(self) -> bytes: with Path(self.path).open("rb") as f: return f.read() def __repr__(self) -> str: return f"Map({self.path})" @classmethod def is_map_file(cls, file: Path) -> bool: return file.is_file() and file.suffix == ".SC2Map" @classmethod def matches_target_map_name(cls, file: Path, name: str) -> bool: return cls.is_map_file(file) and file.stem == name ================================================ FILE: sc2/observer_ai.py ================================================ """ This class is very experimental and probably not up to date and needs to be refurbished. If it works, you can watch replays with it. """ from __future__ import annotations from sc2.bot_ai_internal import BotAIInternal from sc2.data import Alert, Result from sc2.ids.ability_id import AbilityId from sc2.ids.upgrade_id import UpgradeId from sc2.position import Point2 from sc2.unit import Unit from sc2.units import Units class ObserverAI(BotAIInternal): """Base class for bots.""" @property def time(self) -> float: """Returns time in seconds, assumes the game is played on 'faster'""" return self.state.game_loop / 22.4 # / (1/1.4) * (1/16) @property def time_formatted(self) -> str: """Returns time as string in min:sec format""" t = self.time return f"{int(t // 60):02}:{int(t % 60):02}" def alert(self, alert_code: Alert) -> bool: """ Check if alert is triggered in the current step. Possible alerts are listed here https://github.com/Blizzard/s2client-proto/blob/e38efed74c03bec90f74b330ea1adda9215e655f/s2clientprotocol/sc2api.proto#L679-L702 Example use: from sc2.data import Alert if self.alert(Alert.AddOnComplete): print("Addon Complete") Alert codes:: AlertError AddOnComplete BuildingComplete BuildingUnderAttack LarvaHatched MergeComplete MineralsExhausted MorphComplete MothershipComplete MULEExpired NuclearLaunchDetected NukeComplete NydusWormDetected ResearchComplete TrainError TrainUnitComplete TrainWorkerComplete TransformationComplete UnitUnderAttack UpgradeComplete VespeneExhausted WarpInComplete :param alert_code: """ assert isinstance(alert_code, Alert), f"alert_code {alert_code} is no Alert" return alert_code.value in self.state.alerts @property def start_location(self) -> Point2: """ Returns the spawn location of the bot, using the position of the first created townhall. This will be None if the bot is run on an arcade or custom map that does not feature townhalls at game start. """ return self.game_info.player_start_location @property def enemy_start_locations(self) -> list[Point2]: """Possible start locations for enemies.""" return self.game_info.start_locations async def get_available_abilities( self, units: list[Unit] | Units, ignore_resource_requirements: bool = False ) -> list[list[AbilityId]]: """Returns available abilities of one or more units. Right now only checks cooldown, energy cost, and whether the ability has been researched. Examples:: units_abilities = await self.get_available_abilities(self.units) or:: units_abilities = await self.get_available_abilities([self.units.random]) :param units: :param ignore_resource_requirements:""" return await self.client.query_available_abilities(units, ignore_resource_requirements) async def on_unit_destroyed(self, unit_tag: int) -> None: """ Override this in your bot class. This will event will be called when a unit (or structure, friendly or enemy) dies. For enemy units, this only works if the enemy unit was in vision on death. :param unit_tag: """ async def on_unit_created(self, unit: Unit) -> None: """Override this in your bot class. This function is called when a unit is created. :param unit:""" async def on_building_construction_started(self, unit: Unit) -> None: """ Override this in your bot class. This function is called when a building construction has started. :param unit: """ async def on_building_construction_complete(self, unit: Unit) -> None: """ Override this in your bot class. This function is called when a building construction is completed. :param unit: """ async def on_upgrade_complete(self, upgrade: UpgradeId) -> None: """ Override this in your bot class. This function is called with the upgrade id of an upgrade that was not finished last step and is now. :param upgrade: """ async def on_start(self) -> None: """ Override this in your bot class. This function is called after "on_start". At this point, game_data, game_info and the first iteration of game_state (self.state) are available. """ async def on_step(self, iteration: int): """ You need to implement this function! Override this in your bot class. This function is called on every game step (looped in realtime mode). :param iteration: """ raise NotImplementedError async def on_end(self, game_result: Result) -> None: """Override this in your bot class. This function is called at the end of a game. :param game_result:""" ================================================ FILE: sc2/paths.py ================================================ from __future__ import annotations import os import platform import re import sys from contextlib import suppress from pathlib import Path from loguru import logger from sc2 import wsl BASEDIR = { "Windows": "C:/Program Files (x86)/StarCraft II", "WSL1": "/mnt/c/Program Files (x86)/StarCraft II", "WSL2": "/mnt/c/Program Files (x86)/StarCraft II", "Darwin": "/Applications/StarCraft II", "Linux": "~/StarCraftII", "WineLinux": "~/.wine/drive_c/Program Files (x86)/StarCraft II", } USERPATH: dict[str, str | None] = { "Windows": "Documents\\StarCraft II\\ExecuteInfo.txt", "WSL1": "Documents/StarCraft II/ExecuteInfo.txt", "WSL2": "Documents/StarCraft II/ExecuteInfo.txt", "Darwin": "Library/Application Support/Blizzard/StarCraft II/ExecuteInfo.txt", "Linux": None, "WineLinux": None, } BINPATH = { "Windows": "SC2_x64.exe", "WSL1": "SC2_x64.exe", "WSL2": "SC2_x64.exe", "Darwin": "SC2.app/Contents/MacOS/SC2", "Linux": "SC2_x64", "WineLinux": "SC2_x64.exe", } CWD: dict[str, str | None] = { "Windows": "Support64", "WSL1": "Support64", "WSL2": "Support64", "Darwin": None, "Linux": None, "WineLinux": "Support64", } def platform_detect(): pf = os.environ.get("SC2PF", platform.system()) if pf == "Linux": return wsl.detect() or pf return pf PF: str = platform_detect() def get_home(): """Get home directory of user, using Windows home directory for WSL.""" if PF in {"WSL1", "WSL2"}: return wsl.get_wsl_home() or Path.home().expanduser() return Path.home().expanduser() def get_user_sc2_install(): """Attempts to find a user's SC2 install if their OS has ExecuteInfo.txt""" if USERPATH[PF]: einfo = str(get_home() / Path(USERPATH[PF])) # pyrefly: ignore if Path(einfo).is_file(): with Path(einfo).open() as f: content = f.read() if content: base = re.search(r" = (.*)Versions", content).group(1) # pyrefly: ignore if PF in {"WSL1", "WSL2"}: base = str(wsl.win_path_to_wsl_path(base)) if Path(base).exists(): return base return None def get_env() -> None: # TODO: Linux env conf from: https://github.com/deepmind/pysc2/blob/master/pysc2/run_configs/platforms.py return None def get_runner_args(cwd): wine_path = os.environ.get("WINE") if wine_path is not None: runner_file = Path(wine_path) runner_file = runner_file if runner_file.is_file() else runner_file / "wine" """ TODO Is converting linux path really necessary? That would convert '/home/burny/Games/battlenet/drive_c/Program Files (x86)/StarCraft II/Support64' to 'Z:\\home\\burny\\Games\\battlenet\\drive_c\\Program Files (x86)\\StarCraft II\\Support64' """ return [runner_file, "start", "/d", cwd, "/unix"] return [] def latest_executeble(versions_dir, base_build=None): latest = None if base_build is not None: with suppress(ValueError): latest = ( int(base_build[4:]), max(p for p in versions_dir.iterdir() if p.is_dir() and p.name.startswith(str(base_build))), ) if base_build is None or latest is None: latest = max((int(p.name[4:]), p) for p in versions_dir.iterdir() if p.is_dir() and p.name.startswith("Base")) version, path = latest if version < 55958: logger.critical("Your SC2 binary is too old. Upgrade to 3.16.1 or newer.") sys.exit(1) return path / BINPATH[PF] class _MetaPaths(type): """ "Lazily loads paths to allow importing the library even if SC2 isn't installed.""" def __setup(cls): if PF not in BASEDIR: logger.critical(f"Unsupported platform '{PF}'") sys.exit(1) try: base = os.environ.get("SC2PATH") or get_user_sc2_install() or BASEDIR[PF] cls.BASE = Path(base).expanduser() # pyrefly: ignore cls.EXECUTABLE = latest_executeble(cls.BASE / "Versions") cls.CWD = cls.BASE / CWD[PF] if CWD[PF] else None # pyrefly: ignore cls.REPLAYS = cls.BASE / "Replays" # pyrefly: ignore if (cls.BASE / "maps").exists(): cls.MAPS = cls.BASE / "maps" # pyrefly: ignore else: cls.MAPS = cls.BASE / "Maps" # pyrefly: ignore except FileNotFoundError as e: logger.critical(f"SC2 installation not found: File '{e.filename}' does not exist.") sys.exit(1) def __getattr__(cls, attr): cls.__setup() return getattr(cls, attr) class Paths(metaclass=_MetaPaths): """Paths for SC2 folders, lazily loaded using the above metaclass.""" ================================================ FILE: sc2/pixel_map.py ================================================ from __future__ import annotations from collections.abc import Callable from pathlib import Path import numpy as np from s2clientprotocol.common_pb2 import ImageData from sc2.position import Point2, _PointLike class PixelMap: def __init__(self, proto: ImageData, in_bits: bool = False) -> None: """ :param proto: :param in_bits: """ self._proto = proto # Used for copying pixelmaps self._in_bits: bool = in_bits assert self.width * self.height == (8 if in_bits else 1) * len(self._proto.data), ( f"{self.width * self.height} {(8 if in_bits else 1) * len(self._proto.data)}" ) buffer_data = np.frombuffer(self._proto.data, dtype=np.uint8) if in_bits: buffer_data = np.unpackbits(buffer_data) self.data_numpy = buffer_data.reshape(self._proto.size.y, self._proto.size.x) @property def width(self) -> int: return self._proto.size.x @property def height(self) -> int: return self._proto.size.y @property def bits_per_pixel(self) -> int: return self._proto.bits_per_pixel @property def bytes_per_pixel(self) -> int: return self._proto.bits_per_pixel // 8 def __getitem__(self, pos: _PointLike) -> int: """Example usage: is_pathable = self._game_info.pathing_grid[Point2((20, 20))] != 0""" assert 0 <= pos[0] < self.width, f"x is {pos[0]}, self.width is {self.width}" assert 0 <= pos[1] < self.height, f"y is {pos[1]}, self.height is {self.height}" # pyrefly: ignore return int(self.data_numpy[pos[1], pos[0]]) def __setitem__(self, pos: _PointLike, value: int) -> None: """Example usage: self._game_info.pathing_grid[Point2((20, 20))] = 255""" assert 0 <= pos[0] < self.width, f"x is {pos[0]}, self.width is {self.width}" assert 0 <= pos[1] < self.height, f"y is {pos[1]}, self.height is {self.height}" assert 0 <= value <= 254 * self._in_bits + 1, ( f"value is {value}, it should be between 0 and {254 * self._in_bits + 1}" ) assert isinstance(value, int), f"value is of type {type(value)}, it should be an integer" # pyrefly: ignore self.data_numpy[pos[1], pos[0]] = value def is_set(self, p: tuple[int, int]) -> bool: return self[p] != 0 def is_empty(self, p: tuple[int, int]) -> bool: return not self.is_set(p) def copy(self) -> PixelMap: return PixelMap(self._proto, in_bits=self._in_bits) def flood_fill(self, start_point: Point2, pred: Callable[[int], bool]) -> set[Point2]: nodes: set[Point2] = set() # pyrefly: ignore queue: list[tuple[int, int]] = [start_point] while queue: x, y = queue.pop() if not (0 <= x < self.width and 0 <= y < self.height): continue if Point2((x, y)) in nodes: continue if pred(self[x, y]): nodes.add(Point2((x, y))) queue += [(x + a, y + b) for a in [-1, 0, 1] for b in [-1, 0, 1] if not (a == 0 and b == 0)] return nodes def flood_fill_all(self, pred: Callable[[int], bool]) -> set[frozenset[Point2]]: groups: set[frozenset[Point2]] = set() for x in range(self.width): for y in range(self.height): if any((x, y) in g for g in groups): continue if pred(self[x, y]): groups.add(frozenset(self.flood_fill(Point2((x, y)), pred))) return groups def print(self, wide: bool = False) -> None: for y in range(self.height): for x in range(self.width): print("#" if self.is_set((x, y)) else " ", end=(" " if wide else "")) print("") def save_image(self, filename: str | Path) -> None: data = [(0, 0, self[x, y]) for y in range(self.height) for x in range(self.width)] from PIL import Image im = Image.new("RGB", (self.width, self.height)) im.putdata(data) im.save(filename) def plot(self) -> None: import matplotlib.pyplot as plt plt.imshow(self.data_numpy, origin="lower") plt.show() ================================================ FILE: sc2/player.py ================================================ from __future__ import annotations from abc import ABC from pathlib import Path from s2clientprotocol import sc2api_pb2 from sc2.bot_ai import BotAI from sc2.data import AIBuild, Difficulty, PlayerType, Race class AbstractPlayer(ABC): def __init__( self, p_type: PlayerType, race: Race | None = None, name: str | None = None, difficulty: Difficulty | None = None, ai_build: AIBuild | None = None, fullscreen: bool = False, ) -> None: assert isinstance(p_type, PlayerType), f"p_type is of type {type(p_type)}" assert name is None or isinstance(name, str), f"name is of type {type(name)}" self.name = name self.type = p_type self.fullscreen = fullscreen if race is not None: self.race = race if p_type == PlayerType.Computer: assert isinstance(difficulty, Difficulty), f"difficulty is of type {type(difficulty)}" # Workaround, proto information does not carry ai_build info # We cant set that in the Player classmethod assert ai_build is None or isinstance(ai_build, AIBuild), f"ai_build is of type {type(ai_build)}" self.difficulty = difficulty self.ai_build = ai_build elif p_type == PlayerType.Observer: assert race is None assert difficulty is None assert ai_build is None else: assert isinstance(race, Race), f"race is of type {type(race)}" assert difficulty is None assert ai_build is None @property def needs_sc2(self) -> bool: return not isinstance(self, Computer) class Human(AbstractPlayer): def __init__(self, race: Race, name: str | None = None, fullscreen: bool = False) -> None: super().__init__(PlayerType.Participant, race, name=name, fullscreen=fullscreen) def __str__(self) -> str: if self.name is not None: return f"Human({self.race._name_}, name={self.name!r})" return f"Human({self.race._name_})" class Bot(AbstractPlayer): def __init__(self, race: Race, ai: BotAI, name: str | None = None, fullscreen: bool = False) -> None: """ AI can be None if this player object is just used to inform the server about player types. """ assert isinstance(ai, BotAI) or ai is None, f"ai is of type {type(ai)}, inherit BotAI from bot_ai.py" super().__init__(PlayerType.Participant, race, name=name, fullscreen=fullscreen) self.ai: BotAI = ai def __str__(self) -> str: if self.name is not None: return f"Bot {self.ai.__class__.__name__}({self.race._name_}), name={self.name!r})" return f"Bot {self.ai.__class__.__name__}({self.race._name_})" class Computer(AbstractPlayer): def __init__( self, race: Race, difficulty: Difficulty = Difficulty.Easy, ai_build: AIBuild = AIBuild.RandomBuild ) -> None: super().__init__(PlayerType.Computer, race, difficulty=difficulty, ai_build=ai_build) def __str__(self) -> str: if self.ai_build is not None: return f"Computer {self.difficulty._name_}({self.race._name_}, {self.ai_build.name})" return f"Computer {self.difficulty._name_}({self.race._name_})" class Observer(AbstractPlayer): def __init__(self) -> None: super().__init__(PlayerType.Observer) def __str__(self) -> str: return "Observer" class Player(AbstractPlayer): def __init__( self, player_id: int, p_type: PlayerType, # None in case of observer requested_race: Race | None, difficulty: Difficulty | None = None, actual_race: Race | None = None, name: str | None = None, ai_build: AIBuild | None = None, ) -> None: super().__init__(p_type, requested_race, difficulty=difficulty, name=name, ai_build=ai_build) self.id: int = player_id self.actual_race: Race | None = actual_race @classmethod def from_proto(cls, proto: sc2api_pb2.PlayerInfo) -> Player: if PlayerType(proto.type) == PlayerType.Observer: return cls(proto.player_id, PlayerType(proto.type), None, None, None) return cls( proto.player_id, PlayerType(proto.type), Race(proto.race_requested), Difficulty(proto.difficulty) if proto.HasField("difficulty") else None, Race(proto.race_actual) if proto.HasField("race_actual") else None, proto.player_name if proto.HasField("player_name") else None, ) class BotProcess(AbstractPlayer): """ Class for handling bots launched externally, including non-python bots. Default parameters comply with sc2ai and aiarena ladders. :param path: the executable file's path :param launch_list: list of strings that launches the bot e.g. ["python", "run.py"] or ["run.exe"] :param race: bot's race :param name: bot's name :param sc2port_arg: the accepted argument name for the port of the sc2 instance to listen to :param hostaddress_arg: the accepted argument name for the address of the sc2 instance to listen to :param match_arg: the accepted argument name for the starting port to generate a portconfig from :param realtime_arg: the accepted argument name for specifying realtime :param other_args: anything else that is needed e.g. to call a bot capable of running on the bot ladders: BotProcess(os.getcwd(), "python run.py", Race.Terran, "INnoVation") """ def __init__( self, path: str | Path, launch_list: list[str], race: Race, name: str | None = None, sc2port_arg: str = "--GamePort", hostaddress_arg: str = "--LadderServer", match_arg: str = "--StartPort", realtime_arg: str = "--RealTime", other_args: str | None = None, stdout: str | None = None, ) -> None: super().__init__(PlayerType.Participant, race, name=name) assert Path(path).exists() self.path = path self.launch_list = launch_list self.sc2port_arg = sc2port_arg self.match_arg = match_arg self.hostaddress_arg = hostaddress_arg self.realtime_arg = realtime_arg self.other_args = other_args self.stdout = stdout def __repr__(self) -> str: if self.name is not None: return f"Bot {self.name}({self.race.name} from {self.launch_list})" return f"Bot({self.race.name} from {self.launch_list})" def cmd_line( self, sc2port: int | str, matchport: int | str | None, hostaddress: str, realtime: bool = False ) -> list[str]: """ :param sc2port: the port that the launched sc2 instance listens to :param matchport: some starting port that both bots use to generate identical portconfigs. Note: This will not be sent if playing vs computer :param hostaddress: the address the sc2 instances used :param realtime: 1 or 0, indicating whether the match is played in realtime or not :return: string that will be used to start the bot's process """ cmd_line = [ *self.launch_list, self.sc2port_arg, str(sc2port), self.hostaddress_arg, hostaddress, ] if matchport is not None: cmd_line.extend([self.match_arg, str(matchport)]) if self.other_args is not None: cmd_line.append(self.other_args) if realtime: cmd_line.extend([self.realtime_arg]) return cmd_line ================================================ FILE: sc2/portconfig.py ================================================ from __future__ import annotations import json # pyre-fixme[21] import portpicker class Portconfig: """ A data class for ports used by participants to join a match. EVERY participant joining the match must send the same sets of ports to join successfully. SC2 needs 2 ports per connection (one for data, one as a 'header'), which is why the ports come in pairs. :param guests: number of non-hosting participants in a match (i.e. 1 less than the number of participants) :param server_ports: [int portA, int portB] :param player_ports: [[int port1A, int port1B], [int port2A, int port2B], ... ] .shared is deprecated, and should TODO be removed soon (once ladderbots' __init__.py doesnt specify them). .server contains the pair of ports used by the participant 'hosting' the match .players contains a pair of ports for every 'guest' (non-hosting participants) in the match E.g. for 1v1, there will be only 1 guest. For 2v2 (coming soonTM), there would be 3 guests. """ def __init__( self, guests: int = 1, server_ports: list[int] | None = None, player_ports: list[list[int]] | None = None ) -> None: self.shared = None self._picked_ports: list[int] = [] if server_ports: self.server: list[int] = server_ports else: self.server = [portpicker.pick_unused_port() for _ in range(2)] self._picked_ports.extend(self.server) if player_ports: self.players: list[list[int]] = player_ports else: self.players = [[portpicker.pick_unused_port() for _ in range(2)] for _ in range(guests)] self._picked_ports.extend([port for player in self.players for port in player]) def clean(self) -> None: while self._picked_ports: portpicker.return_port(self._picked_ports.pop()) def __str__(self) -> str: return f"Portconfig(shared={self.shared}, server={self.server}, players={self.players})" @property def as_json(self) -> str: return json.dumps({"shared": self.shared, "server": self.server, "players": self.players}) @classmethod def contiguous_ports(cls, guests: int = 1, attempts: int = 40) -> Portconfig: """Returns a Portconfig with adjacent ports""" for _ in range(attempts): start = portpicker.pick_unused_port() others = [start + j for j in range(1, 2 + guests * 2)] if all(portpicker.is_port_free(p) for p in others): server_ports = [start, others.pop(0)] player_ports = [] while others: player_ports.append([others.pop(0), others.pop(0)]) pc = cls(server_ports=server_ports, player_ports=player_ports) pc._picked_ports.append(start) return pc raise portpicker.NoFreePortFoundError() @classmethod def from_json(cls, json_data: bytearray | bytes | str) -> Portconfig: data = json.loads(json_data) return cls(server_ports=data["server"], player_ports=data["players"]) ================================================ FILE: sc2/position.py ================================================ from __future__ import annotations import itertools import math import random from collections.abc import Iterable from typing import ( Any, Protocol, SupportsFloat, SupportsIndex, TypeVar, Union, ) from s2clientprotocol import common_pb2 as common_pb class HasPosition2D(Protocol): @property def position(self) -> Point2: ... _PointLike = Union[tuple[float, float], tuple[float, float], tuple[float, ...]] _PosLike = Union[HasPosition2D, _PointLike] _TPosLike = TypeVar("_TPosLike", bound=_PosLike) EPSILON: float = 10**-8 def _sign(num: SupportsFloat | SupportsIndex) -> float: return math.copysign(1, num) class Pointlike(tuple[float, ...]): T = TypeVar("T", bound="Pointlike") @property def position(self: T) -> T: return self def distance_to(self, target: _PosLike) -> float: """Calculate a single distance from a point or unit to another point or unit :param target:""" # pyrefly: ignore position: tuple[float, ...] = target if isinstance(target, tuple) else target.position return math.hypot(self[0] - position[0], self[1] - position[1]) def distance_to_point2(self, p: _PointLike) -> float: """Same as the function above, but should be a bit faster because of the dropped asserts and conversion. :param p:""" return math.hypot(self[0] - p[0], self[1] - p[1]) def _distance_squared(self, p2: _PointLike) -> float: """Function used to not take the square root as the distances will stay proportionally the same. This is to speed up the sorting process. :param p2:""" return (self[0] - p2[0]) ** 2 + (self[1] - p2[1]) ** 2 def sort_by_distance(self, ps: Iterable[_TPosLike]) -> list[_TPosLike]: """This returns the target points sorted as list. You should not pass a set or dict since those are not sortable. If you want to sort your units towards a point, use 'units.sorted_by_distance_to(point)' instead. :param ps:""" return sorted(ps, key=lambda p: self.distance_to_point2(p if isinstance(p, tuple) else p.position)) def closest(self, ps: Iterable[_TPosLike]) -> _TPosLike: """This function assumes the 2d distance is meant :param ps:""" assert ps, "ps is empty" return min(ps, key=lambda p: self.distance_to_point2(p if isinstance(p, tuple) else p.position)) def distance_to_closest(self, ps: Iterable[_TPosLike]) -> float: """This function assumes the 2d distance is meant :param ps:""" assert ps, "ps is empty" closest_distance = math.inf for p in ps: # pyrefly: ignore p2: tuple[float, ...] = p if isinstance(p, tuple) else p.position distance = self.distance_to_point2(p2) if distance <= closest_distance: closest_distance = distance return closest_distance def furthest(self, ps: Iterable[_TPosLike]) -> _TPosLike: """This function assumes the 2d distance is meant :param ps: Units object, or iterable of Unit or Point2""" assert ps, "ps is empty" return max(ps, key=lambda p: self.distance_to_point2(p if isinstance(p, tuple) else p.position)) def distance_to_furthest(self, ps: Iterable[_PosLike]) -> float: """This function assumes the 2d distance is meant :param ps:""" assert ps, "ps is empty" furthest_distance = -math.inf for p in ps: # pyrefly: ignore p2: tuple[float, ...] = p if isinstance(p, tuple) else p.position distance = self.distance_to_point2(p2) if distance >= furthest_distance: furthest_distance = distance return furthest_distance def offset(self: T, p: _PointLike) -> T: """ :param p: """ return self.__class__(a + b for a, b in itertools.zip_longest(self, p[: len(self)], fillvalue=0)) def unit_axes_towards(self: T, p: _PointLike) -> T: """ :param p: """ return self.__class__(_sign(b - a) for a, b in itertools.zip_longest(self, p[: len(self)], fillvalue=0)) def towards(self: T, p: _PosLike, distance: float = 1, limit: bool = False) -> T: """ :param p: :param distance: :param limit: """ # pyrefly: ignore p2: tuple[float, ...] = p if isinstance(p, tuple) else p.position # assert self != p, f"self is {self}, p is {p}" # TODO test and fix this if statement if self == p2: return self # end of test d = self.distance_to_point2(p2) if limit: distance = min(d, distance) return self.__class__( a + (b - a) / d * distance for a, b in itertools.zip_longest(self, p2[: len(self)], fillvalue=0) ) def __eq__(self, other: Any) -> bool: try: return all(abs(a - b) <= EPSILON for a, b in itertools.zip_longest(self, other, fillvalue=0)) except TypeError: return False def __hash__(self) -> int: return hash(tuple(self)) class Point2(Pointlike): T = TypeVar("T", bound="Point2") @classmethod def from_proto( cls, data: common_pb.Point | common_pb.Point2D | common_pb.Size2DI | common_pb.PointI | Point2 | Point3 ) -> Point2: """ :param data: """ return cls((data.x, data.y)) @property def as_Point2D(self) -> common_pb.Point2D: return common_pb.Point2D(x=self.x, y=self.y) @property def as_PointI(self) -> common_pb.PointI: """Represents points on the minimap. Values must be between 0 and 64.""" return common_pb.PointI(x=int(self[0]), y=int(self[1])) @property def rounded(self) -> Point2: return Point2((math.floor(self[0]), math.floor(self[1]))) @property def length(self) -> float: """This property exists in case Point2 is used as a vector.""" return math.hypot(self[0], self[1]) @property def normalized(self: Point2 | Point3) -> Point2: """This property exists in case Point2 is used as a vector.""" length = self.length # Cannot normalize if length is zero assert length return Point2((self[0] / length, self[1] / length)) @property def x(self) -> float: return self[0] @property def y(self) -> float: return self[1] @property def to2(self) -> Point2: return Point2(self[:2]) @property def to3(self) -> Point3: return Point3((*self, 0)) def round(self, decimals: int) -> Point2: """Rounds each number in the tuple to the amount of given decimals.""" return Point2((round(self[0], decimals), round(self[1], decimals))) def offset(self: T, p: _PointLike) -> T: return self.__class__((self[0] + p[0], self[1] + p[1])) def random_on_distance(self, distance: float | tuple[float, float] | list[float]) -> Point2: if isinstance(distance, (tuple, list)): # interval dist = distance[0] + random.random() * (distance[1] - distance[0]) else: dist = distance assert dist > 0, "Distance is not greater than 0" angle = random.random() * 2 * math.pi dx, dy = math.cos(angle), math.sin(angle) return Point2((self.x + dx * dist, self.y + dy * dist)) def towards_with_random_angle( self, p: Point2 | Point3, distance: int | float = 1, max_difference: int | float = (math.pi / 4), ) -> Point2: tx, ty = self.to2.towards(p.to2, 1) angle = math.atan2(ty - self.y, tx - self.x) angle = (angle - max_difference) + max_difference * 2 * random.random() return Point2((self.x + math.cos(angle) * distance, self.y + math.sin(angle) * distance)) def circle_intersection(self, p: Point2, r: float) -> set[Point2]: """self is point1, p is point2, r is the radius for circles originating in both points Used in ramp finding :param p: :param r:""" assert self != p, "self is equal to p" distance_between_points = self.distance_to(p) assert r >= distance_between_points / 2 # remaining distance from center towards the intersection, using pythagoras remaining_distance_from_center = (r**2 - (distance_between_points / 2) ** 2) ** 0.5 # center of both points offset_to_center = Point2(((p.x - self.x) / 2, (p.y - self.y) / 2)) center = self.offset(offset_to_center) # stretch offset vector in the ratio of remaining distance from center to intersection vector_stretch_factor = remaining_distance_from_center / (distance_between_points / 2) v = offset_to_center offset_to_center_stretched = Point2((v.x * vector_stretch_factor, v.y * vector_stretch_factor)) # rotate vector by 90° and -90° vector_rotated_1 = Point2((offset_to_center_stretched.y, -offset_to_center_stretched.x)) vector_rotated_2 = Point2((-offset_to_center_stretched.y, offset_to_center_stretched.x)) intersect1 = center.offset(vector_rotated_1) intersect2 = center.offset(vector_rotated_2) return {intersect1, intersect2} @property def neighbors4(self: T) -> set[T]: return { self.__class__((self[0] - 1, self[1])), self.__class__((self[0] + 1, self[1])), self.__class__((self[0], self[1] - 1)), self.__class__((self[0], self[1] + 1)), } @property def neighbors8(self: T) -> set[T]: return self.neighbors4 | { self.__class__((self[0] - 1, self[1] - 1)), self.__class__((self[0] - 1, self[1] + 1)), self.__class__((self[0] + 1, self[1] - 1)), self.__class__((self[0] + 1, self[1] + 1)), } def negative_offset(self: T, other: Point2) -> T: return self.__class__((self[0] - other[0], self[1] - other[1])) def __add__(self, other: Point2) -> Point2: return self.offset(other) def __sub__(self, other: Point2) -> Point2: return self.negative_offset(other) def __neg__(self: T) -> T: return self.__class__(-a for a in self) def __abs__(self) -> float: return math.hypot(self[0], self[1]) def __bool__(self) -> bool: return self[0] != 0 or self[1] != 0 def __mul__(self, other: _PointLike | float) -> Point2: if isinstance(other, (int, float)): return Point2((self[0] * other, self[1] * other)) return Point2((self[0] * other[0], self[1] * other[1])) def __rmul__(self, other: _PointLike | float) -> Point2: return self.__mul__(other) def __truediv__(self, other: float | Point2) -> Point2: if isinstance(other, (int, float)): return self.__class__((self[0] / other, self[1] / other)) return self.__class__((self[0] / other[0], self[1] / other[1])) def is_same_as(self, other: Point2, dist: float = 0.001) -> bool: return self.distance_to_point2(other) <= dist def direction_vector(self, other: Point2) -> Point2: """Converts a vector to a direction that can face vertically, horizontally or diagonal or be zero, e.g. (0, 0), (1, -1), (1, 0)""" return self.__class__((_sign(other[0] - self[0]), _sign(other[1] - self[1]))) def manhattan_distance(self, other: Point2) -> float: """ :param other: """ return abs(other[0] - self[0]) + abs(other[1] - self[1]) @staticmethod def center(points: list[Point2]) -> Point2: """Returns the central point for points in list :param points:""" s = Point2((0, 0)) for p in points: s += p return s / len(points) class Point3(Point2): @classmethod def from_proto(cls, data: common_pb.Point | Point3) -> Point3: """ :param data: """ return cls((data.x, data.y, data.z)) @property def as_Point(self) -> common_pb.Point: return common_pb.Point(x=self.x, y=self.y, z=self.z) @property def rounded(self) -> Point3: return Point3((math.floor(self[0]), math.floor(self[1]), math.floor(self[2]))) @property def z(self) -> float: return self[2] @property def to3(self) -> Point3: return Point3(self) def __add__(self, other: Point2 | Point3) -> Point3: if not isinstance(other, Point3): return Point3((self[0] + other[0], self[1] + other[1], self[2])) return Point3((self[0] + other[0], self[1] + other[1], self[2] + other[2])) class Size(Point2): @classmethod def from_proto( cls, data: common_pb.Point | common_pb.Point2D | common_pb.Size2DI | common_pb.PointI | Point2 ) -> Size: """ :param data: """ return cls((data.x, data.y)) @property def width(self) -> float: return self[0] @property def height(self) -> float: return self[1] class Rect(Point2): @classmethod def from_proto(cls, data: common_pb.RectangleI) -> Rect: """ :param data: """ assert data.p0.x < data.p1.x and data.p0.y < data.p1.y return cls((data.p0.x, data.p0.y, data.p1.x - data.p0.x, data.p1.y - data.p0.y)) @property def x(self) -> float: return self[0] @property def y(self) -> float: return self[1] @property def width(self) -> float: return self[2] @property def height(self) -> float: return self[3] @property def right(self) -> float: """Returns the x-coordinate of the rectangle of its right side.""" return self.x + self.width @property def top(self) -> float: """Returns the y-coordinate of the rectangle of its top side.""" return self.y + self.height @property def size(self) -> Size: return Size((self[2], self[3])) @property def center(self) -> Point2: return Point2((self.x + self.width / 2, self.y + self.height / 2)) def offset(self, p: _PointLike) -> Rect: return self.__class__((self[0] + p[0], self[1] + p[1], self[2], self[3])) ================================================ FILE: sc2/power_source.py ================================================ from __future__ import annotations from dataclasses import dataclass from s2clientprotocol import raw_pb2 from sc2.position import Point2 @dataclass class PowerSource: position: Point2 radius: float unit_tag: int def __post_init__(self) -> None: assert self.radius > 0 @classmethod def from_proto(cls, proto: raw_pb2.PowerSource) -> PowerSource: return PowerSource(Point2.from_proto(proto.pos), proto.radius, proto.tag) def covers(self, position: Point2) -> bool: return self.position.distance_to(position) <= self.radius def __repr__(self) -> str: return f"PowerSource({self.position}, {self.radius})" @dataclass class PsionicMatrix: sources: list[PowerSource] @classmethod def from_proto(cls, proto: list[raw_pb2.PowerSource]) -> PsionicMatrix: return PsionicMatrix([PowerSource.from_proto(p) for p in proto]) def covers(self, position: Point2) -> bool: return any(source.covers(position) for source in self.sources) ================================================ FILE: sc2/protocol.py ================================================ from __future__ import annotations import asyncio import sys from contextlib import suppress from typing import overload from aiohttp.client_ws import ClientWebSocketResponse from loguru import logger # pyre-fixme[21] from s2clientprotocol import sc2api_pb2 as sc_pb from s2clientprotocol.query_pb2 import RequestQuery from sc2.data import Status class ProtocolError(Exception): @property def is_game_over_error(self) -> bool: return self.args[0] in ["['Game has already ended']", "['Not supported if game has already ended']"] class ConnectionAlreadyClosedError(ProtocolError): pass class Protocol: def __init__(self, ws: ClientWebSocketResponse) -> None: """ A class for communicating with an SCII application. :param ws: the websocket (type: aiohttp.ClientWebSocketResponse) used to communicate with a specific SCII app """ assert ws self._ws: ClientWebSocketResponse = ws # pyre-fixme[11] self._status: Status | None = None async def __request(self, request: sc_pb.Request) -> sc_pb.Response: logger.debug(f"Sending request: {request!r}") try: await self._ws.send_bytes(request.SerializeToString()) except TypeError as exc: logger.exception("Cannot send: Connection already closed.") raise ConnectionAlreadyClosedError("Connection already closed.") from exc logger.debug("Request sent") response = sc_pb.Response() try: response_bytes = await self._ws.receive_bytes() except TypeError as exc: if self._status == Status.ended: logger.info("Cannot receive: Game has already ended.") raise ConnectionAlreadyClosedError("Game has already ended") from exc logger.error("Cannot receive: Connection already closed.") raise ConnectionAlreadyClosedError("Connection already closed.") from exc except asyncio.CancelledError: # If request is sent, the response must be received before reraising cancel try: await self._ws.receive_bytes() except asyncio.CancelledError: logger.critical("Requests must not be cancelled multiple times") sys.exit(2) raise response.ParseFromString(response_bytes) logger.debug("Response received") return response @overload async def _execute(self, create_game: sc_pb.RequestCreateGame) -> sc_pb.Response: ... @overload async def _execute(self, join_game: sc_pb.RequestJoinGame) -> sc_pb.Response: ... @overload async def _execute(self, restart_game: sc_pb.RequestRestartGame) -> sc_pb.Response: ... @overload async def _execute(self, start_replay: sc_pb.RequestStartReplay) -> sc_pb.Response: ... @overload async def _execute(self, leave_game: sc_pb.RequestLeaveGame) -> sc_pb.Response: ... @overload async def _execute(self, quick_save: sc_pb.RequestQuickSave) -> sc_pb.Response: ... @overload async def _execute(self, quick_load: sc_pb.RequestQuickLoad) -> sc_pb.Response: ... @overload async def _execute(self, quit: sc_pb.RequestQuit) -> sc_pb.Response: ... @overload async def _execute(self, game_info: sc_pb.RequestGameInfo) -> sc_pb.Response: ... @overload async def _execute(self, action: sc_pb.RequestAction) -> sc_pb.Response: ... @overload async def _execute(self, observation: sc_pb.RequestObservation) -> sc_pb.Response: ... @overload async def _execute(self, obs_action: sc_pb.RequestObserverAction) -> sc_pb.Response: ... @overload async def _execute(self, step: sc_pb.RequestStep) -> sc_pb.Response: ... @overload async def _execute(self, data: sc_pb.RequestData) -> sc_pb.Response: ... @overload async def _execute(self, query: RequestQuery) -> sc_pb.Response: ... @overload async def _execute(self, save_replay: sc_pb.RequestSaveReplay) -> sc_pb.Response: ... @overload async def _execute(self, map_command: sc_pb.RequestMapCommand) -> sc_pb.Response: ... @overload async def _execute(self, replay_info: sc_pb.RequestReplayInfo) -> sc_pb.Response: ... @overload async def _execute(self, available_maps: sc_pb.RequestAvailableMaps) -> sc_pb.Response: ... @overload async def _execute(self, save_map: sc_pb.RequestSaveMap) -> sc_pb.Response: ... @overload async def _execute(self, ping: sc_pb.RequestPing) -> sc_pb.Response: ... @overload async def _execute(self, debug: sc_pb.RequestDebug) -> sc_pb.Response: ... async def _execute(self, **kwargs) -> sc_pb.Response: assert len(kwargs) == 1, "Only one request allowed by the API" response: sc_pb.Response = await self.__request(sc_pb.Request(**kwargs)) new_status = Status(response.status) if new_status != self._status: logger.info(f"Client status changed to {new_status} (was {self._status})") self._status = new_status if response.error: logger.debug(f"Response contained an error: {response.error}") raise ProtocolError(f"{response.error}") return response async def ping(self): result = await self._execute(ping=sc_pb.RequestPing()) return result async def quit(self) -> None: with suppress(ConnectionAlreadyClosedError, ConnectionResetError): await self._execute(quit=sc_pb.RequestQuit()) ================================================ FILE: sc2/proxy.py ================================================ from __future__ import annotations import asyncio import os import platform import subprocess import sys import time import traceback from pathlib import Path from aiohttp import WSMsgType, web from aiohttp.web_ws import WebSocketResponse from loguru import logger # pyre-fixme[21] from s2clientprotocol import sc2api_pb2 as sc_pb from sc2.controller import Controller from sc2.data import Result, Status from sc2.player import BotProcess class Proxy: """ Class for handling communication between sc2 and an external bot. This "middleman" is needed for enforcing time limits, collecting results, and closing things properly. """ def __init__( self, controller: Controller, player: BotProcess, proxyport: int, game_time_limit: int | None = None, realtime: bool = False, ) -> None: self.controller = controller self.player = player self.port = proxyport self.timeout_loop = game_time_limit * 22.4 if game_time_limit else None self.realtime = realtime logger.debug( f"Proxy Inited with ctrl {controller}({controller._process._port}), player {player}, proxyport {proxyport}, lim {game_time_limit}" ) self.result = None self.player_id: int | None = None self.done = False async def parse_request(self, msg) -> None: request = sc_pb.Request() request.ParseFromString(msg.data) if request.HasField("quit"): request = sc_pb.Request(leave_game=sc_pb.RequestLeaveGame()) if request.HasField("leave_game"): if self.controller._status == Status.in_game: logger.info(f"Proxy: player {self.player.name}({self.player_id}) surrenders") self.result = {self.player_id: Result.Defeat} elif self.controller._status == Status.ended: await self.get_response() elif request.HasField("join_game") and not request.join_game.HasField("player_name"): if self.player.name is not None: request.join_game.player_name = self.player.name await self.controller._ws.send_bytes(request.SerializeToString()) # TODO Catching too general exception Exception (broad-except) async def get_response(self): response_bytes = None try: response_bytes = await self.controller._ws.receive_bytes() except TypeError as e: logger.exception("Cannot receive: SC2 Connection already closed.") tb = traceback.format_exc() logger.error(f"Exception {e}: {tb}") except asyncio.CancelledError: logger.info(f"Proxy({self.player.name}), caught receive from sc2") try: x = await self.controller._ws.receive_bytes() if response_bytes is None: response_bytes = x except (asyncio.CancelledError, asyncio.TimeoutError, Exception) as e: logger.exception(f"Exception {e}") except Exception as e: logger.exception(f"Caught unknown exception: {e}") return response_bytes async def parse_response(self, response_bytes): response = sc_pb.Response() response.ParseFromString(response_bytes) if not response.HasField("status"): logger.critical("Proxy: RESPONSE HAS NO STATUS {response}") else: new_status = Status(response.status) if new_status != self.controller._status: logger.info(f"Controller({self.player.name}): {self.controller._status}->{new_status}") self.controller._status = new_status if self.player_id is None and response.HasField("join_game"): self.player_id = response.join_game.player_id logger.info(f"Proxy({self.player.name}): got join_game for {self.player_id}") if self.result is None and response.HasField("observation"): obs: sc_pb.ResponseObservation = response.observation if obs.player_result: self.result = {pr.player_id: Result(pr.result) for pr in obs.player_result} elif self.timeout_loop and obs.HasField("observation") and obs.observation.game_loop > self.timeout_loop: self.result = {i: Result.Tie for i in range(1, 3)} # noqa: C420 logger.info(f"Proxy({self.player.name}) timing out") act = [sc_pb.Action(action_chat=sc_pb.ActionChat(message="Proxy: Timing out"))] await self.controller._execute(action=sc_pb.RequestAction(actions=act)) return response async def get_result(self) -> None: try: res = await self.controller.ping() if res.status in {Status.in_game, Status.in_replay, Status.ended}: res = await self.controller._execute(observation=sc_pb.RequestObservation()) if res.HasField("observation") and res.observation.player_result: self.result = {pr.player_id: Result(pr.result) for pr in res.observation.player_result} # TODO Catching too general exception Exception (broad-except) except Exception as e: logger.exception(f"Caught unknown exception: {e}") async def proxy_handler(self, request) -> WebSocketResponse: bot_ws = web.WebSocketResponse(receive_timeout=30) await bot_ws.prepare(request) try: async for msg in bot_ws: if msg.data is None: raise TypeError(f"data is None, {msg}") if msg.data and msg.type == WSMsgType.BINARY: await self.parse_request(msg) response_bytes = await self.get_response() if response_bytes is None: raise ConnectionError("Could not get response_bytes") new_response = await self.parse_response(response_bytes) await bot_ws.send_bytes(new_response.SerializeToString()) elif msg.type == WSMsgType.CLOSED: logger.error("Client shutdown") else: logger.error("Incorrect message type") # TODO Catching too general exception Exception (broad-except) except Exception as e: logger.exception(f"Caught unknown exception: {e}") ignored_errors = {ConnectionError, asyncio.CancelledError} if not any(isinstance(e, E) for E in ignored_errors): tb = traceback.format_exc() logger.info(f"Proxy({self.player.name}): Caught {e} traceback: {tb}") finally: try: if self.controller._status in {Status.in_game, Status.in_replay}: await self.controller._execute(leave_game=sc_pb.RequestLeaveGame()) await bot_ws.close() # TODO Catching too general exception Exception (broad-except) except Exception as e: logger.exception(f"Caught unknown exception during surrender: {e}") self.done = True return bot_ws async def play_with_proxy(self, startport): logger.info(f"Proxy({self.port}): Starting app") app = web.Application() app.router.add_route("GET", "/sc2api", self.proxy_handler) apprunner = web.AppRunner(app, access_log=None) await apprunner.setup() appsite = web.TCPSite(apprunner, self.controller._process._host, self.port) await appsite.start() subproc_args = {"cwd": str(self.player.path), "stderr": subprocess.STDOUT} if platform.system() == "Linux": # pyrefly: ignore subproc_args["preexec_fn"] = os.setpgrp elif platform.system() == "Windows" and sys.platform == "win32": subproc_args["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP player_command_line = self.player.cmd_line(self.port, startport, self.controller._process._host, self.realtime) logger.info(f"Starting bot with command: {' '.join(player_command_line)}") if self.player.stdout is None: bot_process = subprocess.Popen(player_command_line, stdout=subprocess.DEVNULL, **subproc_args) else: with Path(self.player.stdout).open("w+") as out: bot_process = subprocess.Popen(player_command_line, stdout=out, **subproc_args) while self.result is None: bot_alive = bot_process and bot_process.poll() is None sc2_alive = self.controller.running if self.done or not (bot_alive and sc2_alive): logger.info( f"Proxy({self.port}): {self.player.name} died, " f"bot{(not bot_alive) * ' not'} alive, sc2{(not sc2_alive) * ' not'} alive" ) # Maybe its still possible to retrieve a result if sc2_alive and not self.done: await self.get_response() logger.info(f"Proxy({self.port}): breaking, result {self.result}") break await asyncio.sleep(5) # cleanup logger.info(f"({self.port}): cleaning up {self.player!r}") for _i in range(3): if isinstance(bot_process, subprocess.Popen): if bot_process.stdout and not bot_process.stdout.closed: # should not run anymore logger.info(f"==================output for player {self.player.name}") for line in bot_process.stdout.readlines(): logger.opt(raw=True).info(line.decode("utf-8")) bot_process.stdout.close() logger.info("==================") bot_process.terminate() bot_process.wait() time.sleep(0.5) if not bot_process or bot_process.poll() is not None: break else: bot_process.terminate() bot_process.wait() try: await apprunner.cleanup() # TODO Catching too general exception Exception (broad-except) except Exception as e: logger.exception(f"Caught unknown exception during cleaning: {e}") if isinstance(self.result, dict): self.result[None] = None return self.result[self.player_id] return self.result ================================================ FILE: sc2/py.typed ================================================ # Required by https://peps.python.org/pep-0561/#packaging-type-information ================================================ FILE: sc2/renderer.py ================================================ from __future__ import annotations import datetime from typing import TYPE_CHECKING from s2clientprotocol import score_pb2 as score_pb from s2clientprotocol.sc2api_pb2 import ResponseObservation from sc2.position import Point2 if TYPE_CHECKING: from sc2.client import Client from pyglet.image import ImageData from pyglet.text import Label from pyglet.window import Window class Renderer: def __init__(self, client: Client, map_size: tuple[float, float], minimap_size: tuple[float, float]) -> None: self._client = client self._window: Window = None # pyrefly: ignore self._map_size = map_size self._map_image: ImageData = None # pyrefly: ignore self._minimap_size = minimap_size self._minimap_image: ImageData = None # pyrefly: ignore self._mouse_x, self._mouse_y = None, None self._text_supply: Label = None # pyrefly: ignore self._text_vespene: Label = None # pyrefly: ignore self._text_minerals: Label = None # pyrefly: ignore self._text_score: Label = None # pyrefly: ignore self._text_time: Label = None # pyrefly: ignore async def render(self, observation: ResponseObservation) -> None: render_data = observation.observation.render_data map_size = render_data.map.size map_data = render_data.map.data minimap_size = render_data.minimap.size minimap_data = render_data.minimap.data map_width, map_height = map_size.x, map_size.y map_pitch = -map_width * 3 minimap_width, minimap_height = minimap_size.x, minimap_size.y minimap_pitch = -minimap_width * 3 if not self._window: from pyglet.image import ImageData from pyglet.text import Label from pyglet.window import Window self._window = Window(width=map_width, height=map_height) # pyrefly: ignore self._window.on_mouse_press = self._on_mouse_press # pyrefly: ignore self._window.on_mouse_release = self._on_mouse_release # pyrefly: ignore self._window.on_mouse_drag = self._on_mouse_drag self._map_image = ImageData(map_width, map_height, "RGB", map_data, map_pitch) self._minimap_image = ImageData(minimap_width, minimap_height, "RGB", minimap_data, minimap_pitch) self._text_supply = Label( "", font_name="Arial", font_size=16, anchor_x="right", anchor_y="top", x=self._map_size[0] - 10, y=self._map_size[1] - 10, color=(200, 200, 200, 255), ) self._text_vespene = Label( "", font_name="Arial", font_size=16, anchor_x="right", anchor_y="top", x=self._map_size[0] - 130, y=self._map_size[1] - 10, color=(28, 160, 16, 255), ) self._text_minerals = Label( "", font_name="Arial", font_size=16, anchor_x="right", anchor_y="top", x=self._map_size[0] - 200, y=self._map_size[1] - 10, color=(68, 140, 255, 255), ) self._text_score = Label( "", font_name="Arial", font_size=16, anchor_x="left", anchor_y="top", x=10, y=self._map_size[1] - 10, color=(219, 30, 30, 255), ) self._text_time = Label( "", font_name="Arial", font_size=16, anchor_x="right", anchor_y="bottom", x=self._minimap_size[0] - 10, y=self._minimap_size[1] + 10, color=(255, 255, 255, 255), ) else: self._map_image.set_data("RGB", map_pitch, map_data) self._minimap_image.set_data("RGB", minimap_pitch, minimap_data) self._text_time.text = str(datetime.timedelta(seconds=(observation.observation.game_loop * 0.725) // 16)) if observation.observation.HasField("player_common"): self._text_supply.text = f"{observation.observation.player_common.food_used} / {observation.observation.player_common.food_cap}" self._text_vespene.text = str(observation.observation.player_common.vespene) self._text_minerals.text = str(observation.observation.player_common.minerals) if observation.observation.HasField("score"): # pyrefly: ignore self._text_score.text = f"{score_pb._SCORE_SCORETYPE.values_by_number[observation.observation.score.score_type].name} score: {observation.observation.score.score}" await self._update_window() if self._client.in_game and (not observation.player_result) and self._mouse_x and self._mouse_y: await self._client.move_camera_spatial(Point2((self._mouse_x, self._minimap_size[0] - self._mouse_y))) self._mouse_x, self._mouse_y = None, None async def _update_window(self) -> None: self._window.switch_to() self._window.dispatch_events() self._window.clear() self._map_image.blit(0, 0) self._minimap_image.blit(0, 0) self._text_time.draw() self._text_score.draw() self._text_minerals.draw() self._text_vespene.draw() self._text_supply.draw() self._window.flip() def _on_mouse_press(self, x, y, button, _modifiers) -> None: if button != 1: # 1: mouse.LEFT return if x > self._minimap_size[0] or y > self._minimap_size[1]: return self._mouse_x, self._mouse_y = x, y def _on_mouse_release(self, x, y, button, _modifiers) -> None: if button != 1: # 1: mouse.LEFT return if x > self._minimap_size[0] or y > self._minimap_size[1]: return self._mouse_x, self._mouse_y = x, y def _on_mouse_drag(self, x, y, _dx, _dy, buttons, _modifiers) -> None: if not buttons & 1: # 1: mouse.LEFT return if x > self._minimap_size[0] or y > self._minimap_size[1]: return self._mouse_x, self._mouse_y = x, y ================================================ FILE: sc2/sc2process.py ================================================ from __future__ import annotations import asyncio import os import shutil import signal import subprocess import sys import tempfile import time from contextlib import suppress from pathlib import Path from typing import Any import aiohttp import portpicker from aiohttp.client_ws import ClientWebSocketResponse from loguru import logger from sc2 import paths, wsl from sc2.controller import Controller from sc2.paths import Paths from sc2.versions import VERSIONS class KillSwitch: _to_kill: list[Any] = [] @classmethod def add(cls, value) -> None: logger.debug("kill_switch: Add switch") cls._to_kill.append(value) @classmethod def kill_all(cls) -> None: logger.info(f"kill_switch: Process cleanup for {len(cls._to_kill)} processes") for p in cls._to_kill: p._clean(verbose=False) class SC2Process: """ A class for handling SCII applications. :param host: hostname for the url the SCII application will listen to :param port: the websocket port the SCII application will listen to :param fullscreen: whether to launch the SCII application in fullscreen or not, defaults to False :param resolution: (window width, window height) in pixels, defaults to (1024, 768) :param placement: (x, y) the distances of the SCII app's top left corner from the top left corner of the screen e.g. (20, 30) is 20 to the right of the screen's left border, and 30 below the top border :param render: :param sc2_version: :param base_build: :param data_hash: """ def __init__( self, host: str | None = None, port: int | None = None, fullscreen: bool = False, resolution: list[int] | tuple[int, int] | None = None, placement: list[int] | tuple[int, int] | None = None, render: bool = False, sc2_version: str | None = None, base_build: str | None = None, data_hash: str | None = None, ) -> None: assert isinstance(host, str) or host is None assert isinstance(port, int) or port is None self._render = render self._arguments: dict[str, str] = {"-displayMode": str(int(fullscreen))} if not fullscreen: if resolution and len(resolution) == 2: self._arguments["-windowwidth"] = str(resolution[0]) self._arguments["-windowheight"] = str(resolution[1]) if placement and len(placement) == 2: self._arguments["-windowx"] = str(placement[0]) self._arguments["-windowy"] = str(placement[1]) self._host = host or os.environ.get("SC2CLIENTHOST", "127.0.0.1") self._serverhost = os.environ.get("SC2SERVERHOST", self._host) if port is None: self._port = portpicker.pick_unused_port() else: self._port = port self._used_portpicker = bool(port is None) self._tmp_dir = tempfile.mkdtemp(prefix="SC2_") self._process: subprocess.Popen | None = None self._session = None self._ws = None self._sc2_version = sc2_version self._base_build = base_build self._data_hash = data_hash async def __aenter__(self) -> Controller: KillSwitch.add(self) def signal_handler(*_args): # unused arguments: signal handling library expects all signal # callback handlers to accept two positional arguments KillSwitch.kill_all() signal.signal(signal.SIGINT, signal_handler) try: self._process = self._launch() self._ws = await self._connect() except: await self._close_connection() self._clean() raise return Controller(self._ws, self) async def __aexit__(self, *args) -> None: await self._close_connection() KillSwitch.kill_all() signal.signal(signal.SIGINT, signal.SIG_DFL) @property def ws_url(self) -> str: return f"ws://{self._host}:{self._port}/sc2api" @property def versions(self): """Opens the versions.json file which origins from https://github.com/Blizzard/s2client-proto/blob/master/buildinfo/versions.json""" return VERSIONS def find_data_hash(self, target_sc2_version: str) -> str | None: """Returns the data hash from the matching version string.""" for version in self.versions: if version["label"] == target_sc2_version: # pyrefly: ignore return version["data-hash"] return None def find_base_dir(self, target_sc2_version: str) -> str | None: """Returns the base directory from the matching version string.""" for version in self.versions: if version["label"] == target_sc2_version: return "Base" + str(version["base-version"]) return None def _launch(self): if self._sc2_version and not self._base_build: self._base_build = self.find_base_dir(self._sc2_version) if self._base_build: executable = str(paths.latest_executeble(Paths.BASE / "Versions", self._base_build)) else: executable = str(Paths.EXECUTABLE) if self._port == -1: self._port = portpicker.pick_unused_port() self._used_portpicker = True args = paths.get_runner_args(Paths.CWD) + [ executable, "-listen", self._serverhost, "-port", str(self._port), "-dataDir", str(Paths.BASE), "-tempDir", self._tmp_dir, ] for arg, value in self._arguments.items(): args.append(arg) args.append(value) if self._sc2_version: def special_match(strg: str): """Tests if the specified version is in the versions.py dict.""" return any(version["label"] == strg for version in self.versions) valid_version_string = special_match(self._sc2_version) if valid_version_string: self._data_hash = self.find_data_hash(self._sc2_version) assert self._data_hash is not None, ( f"StarCraft 2 Client version ({self._sc2_version}) was not found inside sc2/versions.py file. Please check your spelling or check the versions.py file." ) else: logger.warning( f'The submitted version string in sc2.rungame() function call (sc2_version="{self._sc2_version}") was not found in versions.py. Running latest version instead.' ) if self._data_hash: args.extend(["-dataVersion", self._data_hash]) if self._render: args.extend(["-eglpath", "libEGL.so"]) # if logger.getEffectiveLevel() <= logging.DEBUG: args.append("-verbose") sc2_cwd = str(Paths.CWD) if Paths.CWD else None if paths.PF in {"WSL1", "WSL2"}: return wsl.run(args, sc2_cwd) return subprocess.Popen( args, cwd=sc2_cwd, # Suppress Wine error messages stderr=subprocess.DEVNULL, # , env=run_config.env ) async def _connect(self) -> ClientWebSocketResponse: # How long it waits for SC2 to start (in seconds) for i in range(180): if self._process is None: # The ._clean() was called, clearing the process logger.debug("Process cleanup complete, exit") sys.exit() await asyncio.sleep(1) try: self._session = aiohttp.ClientSession() # pyrefly: ignore ws = await self._session.ws_connect(self.ws_url, timeout=120) # FIXME fix deprecation warning in for future aiohttp version # ws = await self._session.ws_connect( # self.ws_url, timeout=aiohttp.client_ws.ClientWSTimeout(ws_close=120) # ) logger.debug("Websocket connection ready") return ws except aiohttp.ClientConnectorError: if self._session is not None: await self._session.close() if i > 15: logger.debug("Connection refused (startup not complete (yet))") logger.debug("Websocket connection to SC2 process timed out") raise TimeoutError("Websocket") async def _close_connection(self) -> None: logger.info(f"Closing connection at {self._port}...") if self._ws is not None: await self._ws.close() if self._session is not None: await self._session.close() def _clean(self, verbose: bool = True) -> None: if verbose: logger.info("Cleaning up...") if self._process is not None: assert isinstance(self._process, subprocess.Popen) if paths.PF in {"WSL1", "WSL2"}: if wsl.kill(self._process): logger.error("KILLED") elif self._process.poll() is None: for _ in range(3): self._process.terminate() time.sleep(0.5) if not self._process or self._process.poll() is not None: break else: self._process.kill() self._process.wait() logger.error("KILLED") # Try to kill wineserver on linux # pyrefly: ignore if paths.PF in {"Linux", "WineLinux"}: # Command wineserver not detected with suppress(FileNotFoundError), subprocess.Popen(["wineserver", "-k"]) as p: p.wait() if Path(self._tmp_dir).exists(): shutil.rmtree(self._tmp_dir) self._process = None self._ws = None if self._used_portpicker and self._port is not None: portpicker.return_port(self._port) self._port = -1 if verbose: logger.info("Cleanup complete") ================================================ FILE: sc2/score.py ================================================ from __future__ import annotations from s2clientprotocol import score_pb2 class ScoreDetails: """Accessable in self.state.score during step function For more information, see https://github.com/Blizzard/s2client-proto/blob/master/s2clientprotocol/score.proto """ def __init__(self, proto: score_pb2.Score) -> None: self._data = proto self._proto = proto.score_details @property def summary(self) -> list[list[float]]: """ TODO this is super ugly, how can we improve this summary? Print summary to file with: In on_step: with open("stats.txt", "w+") as file: for stat in self.state.score.summary: file.write(f"{stat[0]:<35} {float(stat[1]):>35.3f}\n") """ values = [ "score_type", "score", "idle_production_time", "idle_worker_time", "total_value_units", "total_value_structures", "killed_value_units", "killed_value_structures", "collected_minerals", "collected_vespene", "collection_rate_minerals", "collection_rate_vespene", "spent_minerals", "spent_vespene", "food_used_none", "food_used_army", "food_used_economy", "food_used_technology", "food_used_upgrade", "killed_minerals_none", "killed_minerals_army", "killed_minerals_economy", "killed_minerals_technology", "killed_minerals_upgrade", "killed_vespene_none", "killed_vespene_army", "killed_vespene_economy", "killed_vespene_technology", "killed_vespene_upgrade", "lost_minerals_none", "lost_minerals_army", "lost_minerals_economy", "lost_minerals_technology", "lost_minerals_upgrade", "lost_vespene_none", "lost_vespene_army", "lost_vespene_economy", "lost_vespene_technology", "lost_vespene_upgrade", "friendly_fire_minerals_none", "friendly_fire_minerals_army", "friendly_fire_minerals_economy", "friendly_fire_minerals_technology", "friendly_fire_minerals_upgrade", "friendly_fire_vespene_none", "friendly_fire_vespene_army", "friendly_fire_vespene_economy", "friendly_fire_vespene_technology", "friendly_fire_vespene_upgrade", "used_minerals_none", "used_minerals_army", "used_minerals_economy", "used_minerals_technology", "used_minerals_upgrade", "used_vespene_none", "used_vespene_army", "used_vespene_economy", "used_vespene_technology", "used_vespene_upgrade", "total_used_minerals_none", "total_used_minerals_army", "total_used_minerals_economy", "total_used_minerals_technology", "total_used_minerals_upgrade", "total_used_vespene_none", "total_used_vespene_army", "total_used_vespene_economy", "total_used_vespene_technology", "total_used_vespene_upgrade", "total_damage_dealt_life", "total_damage_dealt_shields", "total_damage_dealt_energy", "total_damage_taken_life", "total_damage_taken_shields", "total_damage_taken_energy", "total_healed_life", "total_healed_shields", "total_healed_energy", "current_apm", "current_effective_apm", ] # pyrefly: ignore return [[value, getattr(self, value)] for value in values] @property def score_type(self): return self._data.score_type @property def score(self): return self._data.score @property def idle_production_time(self): return self._proto.idle_production_time @property def idle_worker_time(self): return self._proto.idle_worker_time @property def total_value_units(self): return self._proto.total_value_units @property def total_value_structures(self): return self._proto.total_value_structures @property def killed_value_units(self): return self._proto.killed_value_units @property def killed_value_structures(self): return self._proto.killed_value_structures @property def collected_minerals(self): return self._proto.collected_minerals @property def collected_vespene(self): return self._proto.collected_vespene @property def collection_rate_minerals(self): return self._proto.collection_rate_minerals @property def collection_rate_vespene(self): return self._proto.collection_rate_vespene @property def spent_minerals(self): return self._proto.spent_minerals @property def spent_vespene(self): return self._proto.spent_vespene @property def food_used_none(self): return self._proto.food_used.none @property def food_used_army(self): return self._proto.food_used.army @property def food_used_economy(self): return self._proto.food_used.economy @property def food_used_technology(self): return self._proto.food_used.technology @property def food_used_upgrade(self): return self._proto.food_used.upgrade @property def killed_minerals_none(self): return self._proto.killed_minerals.none @property def killed_minerals_army(self): return self._proto.killed_minerals.army @property def killed_minerals_economy(self): return self._proto.killed_minerals.economy @property def killed_minerals_technology(self): return self._proto.killed_minerals.technology @property def killed_minerals_upgrade(self): return self._proto.killed_minerals.upgrade @property def killed_vespene_none(self): return self._proto.killed_vespene.none @property def killed_vespene_army(self): return self._proto.killed_vespene.army @property def killed_vespene_economy(self): return self._proto.killed_vespene.economy @property def killed_vespene_technology(self): return self._proto.killed_vespene.technology @property def killed_vespene_upgrade(self): return self._proto.killed_vespene.upgrade @property def lost_minerals_none(self): return self._proto.lost_minerals.none @property def lost_minerals_army(self): return self._proto.lost_minerals.army @property def lost_minerals_economy(self): return self._proto.lost_minerals.economy @property def lost_minerals_technology(self): return self._proto.lost_minerals.technology @property def lost_minerals_upgrade(self): return self._proto.lost_minerals.upgrade @property def lost_vespene_none(self): return self._proto.lost_vespene.none @property def lost_vespene_army(self): return self._proto.lost_vespene.army @property def lost_vespene_economy(self): return self._proto.lost_vespene.economy @property def lost_vespene_technology(self): return self._proto.lost_vespene.technology @property def lost_vespene_upgrade(self): return self._proto.lost_vespene.upgrade @property def friendly_fire_minerals_none(self): return self._proto.friendly_fire_minerals.none @property def friendly_fire_minerals_army(self): return self._proto.friendly_fire_minerals.army @property def friendly_fire_minerals_economy(self): return self._proto.friendly_fire_minerals.economy @property def friendly_fire_minerals_technology(self): return self._proto.friendly_fire_minerals.technology @property def friendly_fire_minerals_upgrade(self): return self._proto.friendly_fire_minerals.upgrade @property def friendly_fire_vespene_none(self): return self._proto.friendly_fire_vespene.none @property def friendly_fire_vespene_army(self): return self._proto.friendly_fire_vespene.army @property def friendly_fire_vespene_economy(self): return self._proto.friendly_fire_vespene.economy @property def friendly_fire_vespene_technology(self): return self._proto.friendly_fire_vespene.technology @property def friendly_fire_vespene_upgrade(self): return self._proto.friendly_fire_vespene.upgrade @property def used_minerals_none(self): return self._proto.used_minerals.none @property def used_minerals_army(self): return self._proto.used_minerals.army @property def used_minerals_economy(self): return self._proto.used_minerals.economy @property def used_minerals_technology(self): return self._proto.used_minerals.technology @property def used_minerals_upgrade(self): return self._proto.used_minerals.upgrade @property def used_vespene_none(self): return self._proto.used_vespene.none @property def used_vespene_army(self): return self._proto.used_vespene.army @property def used_vespene_economy(self): return self._proto.used_vespene.economy @property def used_vespene_technology(self): return self._proto.used_vespene.technology @property def used_vespene_upgrade(self): return self._proto.used_vespene.upgrade @property def total_used_minerals_none(self): return self._proto.total_used_minerals.none @property def total_used_minerals_army(self): return self._proto.total_used_minerals.army @property def total_used_minerals_economy(self): return self._proto.total_used_minerals.economy @property def total_used_minerals_technology(self): return self._proto.total_used_minerals.technology @property def total_used_minerals_upgrade(self): return self._proto.total_used_minerals.upgrade @property def total_used_vespene_none(self): return self._proto.total_used_vespene.none @property def total_used_vespene_army(self): return self._proto.total_used_vespene.army @property def total_used_vespene_economy(self): return self._proto.total_used_vespene.economy @property def total_used_vespene_technology(self): return self._proto.total_used_vespene.technology @property def total_used_vespene_upgrade(self): return self._proto.total_used_vespene.upgrade @property def total_damage_dealt_life(self): return self._proto.total_damage_dealt.life @property def total_damage_dealt_shields(self): return self._proto.total_damage_dealt.shields @property def total_damage_dealt_energy(self): return self._proto.total_damage_dealt.energy @property def total_damage_taken_life(self): return self._proto.total_damage_taken.life @property def total_damage_taken_shields(self): return self._proto.total_damage_taken.shields @property def total_damage_taken_energy(self): return self._proto.total_damage_taken.energy @property def total_healed_life(self): return self._proto.total_healed.life @property def total_healed_shields(self): return self._proto.total_healed.shields @property def total_healed_energy(self): return self._proto.total_healed.energy @property def current_apm(self): return self._proto.current_apm @property def current_effective_apm(self): return self._proto.current_effective_apm ================================================ FILE: sc2/unit.py ================================================ from __future__ import annotations import math import warnings from dataclasses import dataclass from functools import cached_property from typing import TYPE_CHECKING, Any from s2clientprotocol import raw_pb2 from sc2.cache import CacheDict from sc2.constants import ( CAN_BE_ATTACKED, DAMAGE_BONUS_PER_UPGRADE, IS_ARMORED, IS_ATTACKING, IS_BIOLOGICAL, IS_CARRYING_MINERALS, IS_CARRYING_RESOURCES, IS_CARRYING_VESPENE, IS_CLOAKED, IS_COLLECTING, IS_CONSTRUCTING_SCV, IS_DETECTOR, IS_ENEMY, IS_GATHERING, IS_LIGHT, IS_MASSIVE, IS_MECHANICAL, IS_MINE, IS_PATROLLING, IS_PLACEHOLDER, IS_PSIONIC, IS_REPAIRING, IS_RETURNING, IS_REVEALED, IS_SNAPSHOT, IS_STRUCTURE, IS_VISIBLE, OFF_CREEP_SPEED_INCREASE_DICT, OFF_CREEP_SPEED_UPGRADE_DICT, SPEED_ALTERING_BUFFS, SPEED_INCREASE_DICT, SPEED_INCREASE_ON_CREEP_DICT, SPEED_UPGRADE_DICT, TARGET_AIR, TARGET_BOTH, TARGET_GROUND, TARGET_HELPER, UNIT_BATTLECRUISER, UNIT_COLOSSUS, UNIT_ORACLE, UNIT_PHOTONCANNON, transforming, ) from sc2.data import Attribute, CloakState, Race, Target, race_gas, warpgate_abilities from sc2.ids.ability_id import AbilityId from sc2.ids.buff_id import BuffId from sc2.ids.unit_typeid import UnitTypeId from sc2.ids.upgrade_id import UpgradeId from sc2.position import HasPosition2D, Point2, Point3, _PointLike from sc2.unit_command import UnitCommand if TYPE_CHECKING: from sc2.bot_ai import BotAI from sc2.game_data import AbilityData, UnitTypeData @dataclass class RallyTarget: point: Point2 tag: int | None = None @classmethod def from_proto(cls, proto: raw_pb2.RallyTarget) -> RallyTarget: return cls( Point2.from_proto(proto.point), proto.tag if proto.HasField("tag") else None, ) @dataclass class UnitOrder: ability: AbilityData # TODO: Should this be AbilityId instead? target: int | Point2 | None = None progress: float = 0 @classmethod def from_proto(cls, proto: raw_pb2.UnitOrder, bot_object: BotAI) -> UnitOrder: target: int | Point2 | None = proto.target_unit_tag if proto.HasField("target_world_space_pos"): target = Point2.from_proto(proto.target_world_space_pos) elif proto.HasField("target_unit_tag"): target = proto.target_unit_tag return cls( ability=bot_object.game_data.abilities[proto.ability_id], target=target, progress=proto.progress, ) def __repr__(self) -> str: return f"UnitOrder({self.ability}, {self.target}, {self.progress})" class Unit(HasPosition2D): class_cache = CacheDict() def __init__( self, proto_data: raw_pb2.Unit, bot_object: BotAI, distance_calculation_index: int = -1, base_build: int = -1, ) -> None: """ :param proto_data: :param bot_object: :param distance_calculation_index: :param base_build: """ self._proto = proto_data self._bot_object = bot_object self.game_loop: int = bot_object.state.game_loop self.base_build = base_build # Index used in the 2D numpy array to access the 2D distance between two units self.distance_calculation_index: int = distance_calculation_index def __repr__(self) -> str: """Returns string of this form: Unit(name='SCV', tag=4396941328).""" return f"Unit(name={self.name!r}, tag={self.tag})" @property def type_id(self) -> UnitTypeId: """UnitTypeId found in sc2/ids/unit_typeid.""" unit_type: int = self._proto.unit_type return self.class_cache.retrieve_and_set(unit_type, lambda: UnitTypeId(unit_type)) @cached_property def _type_data(self) -> UnitTypeData: """Provides the unit type data.""" return self._bot_object.game_data.units[self._proto.unit_type] @cached_property def _creation_ability(self) -> AbilityData | None: """Provides the AbilityData of the creation ability of this unit.""" return self._type_data.creation_ability @property def name(self) -> str: """Returns the name of the unit.""" return self._type_data.name @cached_property def race(self) -> Race: """Returns the race of the unit""" return Race(self._type_data._proto.race) @cached_property def tag(self) -> int: """Returns the unique tag of the unit.""" return self._proto.tag @property def is_structure(self) -> bool: """Checks if the unit is a structure.""" return IS_STRUCTURE in self._type_data._proto.attributes @property def is_light(self) -> bool: """Checks if the unit has the 'light' attribute.""" return IS_LIGHT in self._type_data._proto.attributes @property def is_armored(self) -> bool: """Checks if the unit has the 'armored' attribute.""" return IS_ARMORED in self._type_data._proto.attributes @property def is_biological(self) -> bool: """Checks if the unit has the 'biological' attribute.""" return IS_BIOLOGICAL in self._type_data._proto.attributes @property def is_mechanical(self) -> bool: """Checks if the unit has the 'mechanical' attribute.""" return IS_MECHANICAL in self._type_data._proto.attributes @property def is_massive(self) -> bool: """Checks if the unit has the 'massive' attribute.""" return IS_MASSIVE in self._type_data._proto.attributes @property def is_psionic(self) -> bool: """Checks if the unit has the 'psionic' attribute.""" return IS_PSIONIC in self._type_data._proto.attributes @cached_property def tech_alias(self) -> list[UnitTypeId] | None: """Building tech equality, e.g. OrbitalCommand is the same as CommandCenter For Hive, this returns [UnitTypeId.Hatchery, UnitTypeId.Lair] For SCV, this returns None""" return self._type_data.tech_alias @cached_property def unit_alias(self) -> UnitTypeId | None: """Building type equality, e.g. FlyingOrbitalCommand is the same as OrbitalCommand For flying OrbitalCommand, this returns UnitTypeId.OrbitalCommand For SCV, this returns None""" return self._type_data.unit_alias @cached_property def _weapons(self): """Returns the weapons of the unit.""" return self._type_data._proto.weapons @cached_property def can_attack(self) -> bool: """Checks if the unit can attack at all.""" # TODO BATTLECRUISER doesnt have weapons in proto?! return bool(self._weapons) or self.type_id in {UNIT_BATTLECRUISER, UNIT_ORACLE} @property def can_attack_both(self) -> bool: """Checks if the unit can attack both ground and air units.""" return self.can_attack_ground and self.can_attack_air @cached_property def can_attack_ground(self) -> bool: """Checks if the unit can attack ground units.""" if self.type_id in {UNIT_BATTLECRUISER, UNIT_ORACLE}: return True if self._weapons: return any(weapon.type in TARGET_GROUND for weapon in self._weapons) return False @cached_property def ground_dps(self) -> float: """Returns the dps against ground units. Does not include upgrades.""" if self.can_attack_ground: weapon = next((weapon for weapon in self._weapons if weapon.type in TARGET_GROUND), None) if weapon: return (weapon.damage * weapon.attacks) / weapon.speed return 0 @cached_property def ground_range(self) -> float: """Returns the range against ground units. Does not include upgrades.""" if self.type_id == UNIT_ORACLE: return 4 if self.type_id == UNIT_BATTLECRUISER: return 6 if self.can_attack_ground: weapon = next((weapon for weapon in self._weapons if weapon.type in TARGET_GROUND), None) if weapon: return weapon.range return 0 @cached_property def can_attack_air(self) -> bool: """Checks if the unit can air attack at all. Does not include upgrades.""" if self.type_id == UNIT_BATTLECRUISER: return True if self._weapons: return any(weapon.type in TARGET_AIR for weapon in self._weapons) return False @cached_property def air_dps(self) -> float: """Returns the dps against air units. Does not include upgrades.""" if self.can_attack_air: weapon = next((weapon for weapon in self._weapons if weapon.type in TARGET_AIR), None) if weapon: return (weapon.damage * weapon.attacks) / weapon.speed return 0 @cached_property def air_range(self) -> float: """Returns the range against air units. Does not include upgrades.""" if self.type_id == UNIT_BATTLECRUISER: return 6 if self.can_attack_air: weapon = next((weapon for weapon in self._weapons if weapon.type in TARGET_AIR), None) if weapon: return weapon.range return 0 @cached_property def bonus_damage(self) -> tuple[float, str] | None: """Returns a tuple of form '(bonus damage, armor type)' if unit does 'bonus damage' against 'armor type'. Possible armor typs are: 'Light', 'Armored', 'Biological', 'Mechanical', 'Psionic', 'Massive', 'Structure'.""" # TODO: Consider units with ability attacks (Oracle, Baneling) or multiple attacks (Thor). if self._weapons: for weapon in self._weapons: if weapon.damage_bonus: b = weapon.damage_bonus[0] return b.bonus, Attribute(b.attribute).name return None @property def armor(self) -> float: """Returns the armor of the unit. Does not include upgrades""" return self._type_data._proto.armor @property def sight_range(self) -> float: """Returns the sight range of the unit.""" return self._type_data._proto.sight_range @property def movement_speed(self) -> float: """Returns the movement speed of the unit. This is the unit movement speed on game speed 'normal'. To convert it to 'faster' movement speed, multiply it by a factor of '1.4'. E.g. reaper movement speed is listed here as 3.75, but should actually be 5.25. Does not include upgrades or buffs.""" return self._type_data._proto.movement_speed @cached_property def real_speed(self) -> float: """See 'calculate_speed'.""" return self.calculate_speed() def calculate_speed(self, upgrades: set[UpgradeId] | None = None) -> float: """Calculates the movement speed of the unit including buffs and upgrades. Note: Upgrades only work with own units. Use "upgrades" param to set expected enemy upgrades. :param upgrades: """ speed: float = self.movement_speed unit_type: UnitTypeId = self.type_id # ---- Upgrades ---- if upgrades is None and self.is_mine: upgrades = self._bot_object.state.upgrades if upgrades and unit_type in SPEED_UPGRADE_DICT: upgrade_id: UpgradeId | None = SPEED_UPGRADE_DICT.get(unit_type, None) if upgrade_id and upgrade_id in upgrades: speed *= SPEED_INCREASE_DICT.get(unit_type, 1) # ---- Creep ---- if unit_type in SPEED_INCREASE_ON_CREEP_DICT or unit_type in OFF_CREEP_SPEED_UPGRADE_DICT: # On creep x, y = self.position_tuple if self._bot_object.state.creep[(int(x), int(y))]: speed *= SPEED_INCREASE_ON_CREEP_DICT.get(unit_type, 1) # Off creep upgrades elif upgrades: upgrade_id2: UpgradeId | None = OFF_CREEP_SPEED_UPGRADE_DICT.get(unit_type, None) if upgrade_id2: speed *= OFF_CREEP_SPEED_INCREASE_DICT[unit_type] # Ultralisk has passive ability "Frenzied" which makes it immune to speed altering buffs if unit_type == UnitTypeId.ULTRALISK: return speed # ---- Buffs ---- # Hard reset movement speed: medivac boost, void ray charge if self.buffs and unit_type in {UnitTypeId.MEDIVAC, UnitTypeId.VOIDRAY}: if BuffId.MEDIVACSPEEDBOOST in self.buffs: speed = self.movement_speed * 1.7 elif BuffId.VOIDRAYSWARMDAMAGEBOOST in self.buffs: speed = self.movement_speed * 0.75 # Speed altering buffs, e.g. stimpack, zealot charge, concussive shell, time warp, fungal growth, inhibitor zone for buff in self.buffs: speed *= SPEED_ALTERING_BUFFS.get(buff, 1) return speed @property def distance_per_step(self) -> float: """The distance a unit can move in one step. This does not take acceleration into account. Useful for micro-retreat/pathfinding""" return (self.real_speed / 22.4) * self._bot_object.client.game_step @property def distance_to_weapon_ready(self) -> float: """Distance a unit can travel before it's weapon is ready to be fired again.""" return (self.real_speed / 22.4) * self.weapon_cooldown @property def is_mineral_field(self) -> bool: """Checks if the unit is a mineral field.""" return self._type_data.has_minerals @property def is_vespene_geyser(self) -> bool: """Checks if the unit is a non-empty vespene geyser or gas extraction building.""" return self._type_data.has_vespene @property def health(self) -> float: """Returns the health of the unit. Does not include shields.""" return self._proto.health @property def health_max(self) -> float: """Returns the maximum health of the unit. Does not include shields.""" return self._proto.health_max @cached_property def health_percentage(self) -> float: """Returns the percentage of health the unit has. Does not include shields.""" if not self._proto.health_max: return 0 return self._proto.health / self._proto.health_max @property def shield(self) -> float: """Returns the shield points the unit has. Returns 0 for non-protoss units.""" return self._proto.shield @property def shield_max(self) -> float: """Returns the maximum shield points the unit can have. Returns 0 for non-protoss units.""" return self._proto.shield_max @cached_property def shield_percentage(self) -> float: """Returns the percentage of shield points the unit has. Returns 0 for non-protoss units.""" if not self._proto.shield_max: return 0 return self._proto.shield / self._proto.shield_max @cached_property def shield_health_percentage(self) -> float: """Returns the percentage of combined shield + hp points the unit has. Also takes build progress into account.""" max_ = (self._proto.shield_max + self._proto.health_max) * self.build_progress if max_ == 0: return 0 return (self._proto.shield + self._proto.health) / max_ @property def energy(self) -> float: """Returns the amount of energy the unit has. Returns 0 for units without energy.""" return self._proto.energy @property def energy_max(self) -> float: """Returns the maximum amount of energy the unit can have. Returns 0 for units without energy.""" return self._proto.energy_max @cached_property def energy_percentage(self) -> float: """Returns the percentage of amount of energy the unit has. Returns 0 for units without energy.""" if not self._proto.energy_max: return 0 return self._proto.energy / self._proto.energy_max @property def age_in_frames(self) -> int: """Returns how old the unit object data is (in game frames). This age does not reflect the unit was created / trained / morphed!""" return self._bot_object.state.game_loop - self.game_loop @property def age(self) -> float: """Returns how old the unit object data is (in game seconds). This age does not reflect when the unit was created / trained / morphed!""" return (self._bot_object.state.game_loop - self.game_loop) / 22.4 @property def is_memory(self) -> bool: """Returns True if this Unit object is referenced from the future and is outdated.""" return self.game_loop != self._bot_object.state.game_loop @cached_property def is_snapshot(self) -> bool: """Checks if the unit is only available as a snapshot for the bot. Enemy buildings that have been scouted and are in the fog of war or attacking enemy units on higher, not visible ground appear this way.""" if self.base_build >= 82457: return self._proto.display_type == IS_SNAPSHOT # TODO: Fixed in version 5.0.4, remove if a new linux binary is released: https://github.com/Blizzard/s2client-proto/issues/167 # pyrefly: ignore position: tuple[int, int] = self.position.rounded return self._bot_object.state.visibility.data_numpy[position[1], position[0]] != 2 @cached_property def is_visible(self) -> bool: """Checks if the unit is visible for the bot. NOTE: This means the bot has vision of the position of the unit! It does not give any information about the cloak status of the unit.""" if self.base_build >= 82457: return self._proto.display_type == IS_VISIBLE # TODO: Remove when a new linux binary (5.0.4 or newer) is released return self._proto.display_type == IS_VISIBLE and not self.is_snapshot @property def is_placeholder(self) -> bool: """Checks if the unit is a placerholder for the bot. Raw information about placeholders: display_type: Placeholder alliance: Self unit_type: 86 owner: 1 pos { x: 29.5 y: 53.5 z: 7.98828125 } radius: 2.75 is_on_screen: false """ return self._proto.display_type == IS_PLACEHOLDER @property def alliance(self) -> int: """Returns the team the unit belongs to.""" return self._proto.alliance @property def is_mine(self) -> bool: """Checks if the unit is controlled by the bot.""" return self._proto.alliance == IS_MINE @property def is_enemy(self) -> bool: """Checks if the unit is hostile.""" return self._proto.alliance == IS_ENEMY @property def owner_id(self) -> int: """Returns the owner of the unit. This is a value of 1 or 2 in a two player game.""" return self._proto.owner @property def position_tuple(self) -> tuple[float, float]: """Returns the 2d position of the unit as tuple without conversion to Point2.""" return self._proto.pos.x, self._proto.pos.y @cached_property def position(self) -> Point2: """Returns the 2d position of the unit.""" return Point2.from_proto(self._proto.pos) @cached_property def position3d(self) -> Point3: """Returns the 3d position of the unit.""" return Point3.from_proto(self._proto.pos) def distance_to(self, p: Unit | _PointLike) -> float: """Using the 2d distance between self and p. To calculate the 3d distance, use unit.position3d.distance_to(p) :param p: """ if isinstance(p, Unit): return self._bot_object._distance_squared_unit_to_unit(self, p) ** 0.5 return self._bot_object.distance_math_hypot(self.position_tuple, p) def distance_to_squared(self, p: Unit | _PointLike) -> float: """Using the 2d distance squared between self and p. Slightly faster than distance_to, so when filtering a lot of units, this function is recommended to be used. To calculate the 3d distance, use unit.position3d.distance_to(p) :param p: """ if isinstance(p, Unit): return self._bot_object._distance_squared_unit_to_unit(self, p) return self._bot_object.distance_math_hypot_squared(self.position_tuple, p) def target_in_range(self, target: Unit, bonus_distance: float = 0) -> bool: """Checks if the target is in range. Includes the target's radius when calculating distance to target. :param target: :param bonus_distance: """ # TODO: Fix this because immovable units (sieged tank, planetary fortress etc.) have a little lower range than this formula if self.can_attack_ground and not target.is_flying: unit_attack_range = self.ground_range elif self.can_attack_air and (target.is_flying or target.type_id == UNIT_COLOSSUS): unit_attack_range = self.air_range else: return False return ( self._bot_object._distance_squared_unit_to_unit(self, target) <= (self.radius + target.radius + unit_attack_range + bonus_distance) ** 2 ) def in_ability_cast_range(self, ability_id: AbilityId, target: Unit | Point2, bonus_distance: float = 0) -> bool: """Test if a unit is able to cast an ability on the target without checking ability cooldown (like stalker blink) or if ability is made available through research (like HT storm). :param ability_id: :param target: :param bonus_distance: """ cast_range = self._bot_object.game_data.abilities[ability_id.value]._proto.cast_range assert cast_range > 0, f"Checking for an ability ({ability_id}) that has no cast range" ability_target_type = self._bot_object.game_data.abilities[ability_id.value]._proto.target # For casting abilities that target other units, like transfuse, feedback, snipe, yamato if ability_target_type in {Target.Unit.value, Target.PointOrUnit.value} and isinstance(target, Unit): return ( self._bot_object._distance_squared_unit_to_unit(self, target) <= (cast_range + self.radius + target.radius + bonus_distance) ** 2 ) # For casting abilities on the ground, like queen creep tumor, ravager bile, HT storm if ability_target_type in {Target.Point.value, Target.PointOrUnit.value} and isinstance(target, Point2): return ( self._bot_object._distance_pos_to_pos(self.position_tuple, target) <= cast_range + self.radius + bonus_distance ) return False def calculate_damage_vs_target( self, target: Unit, ignore_armor: bool = False, include_overkill_damage: bool = True, ) -> tuple[float, float, float]: """Returns a tuple of: [potential damage against target, attack speed, attack range] Returns the properly calculated damage per full-attack against the target unit. Returns (0, 0, 0) if this unit can't attack the target unit. If 'include_overkill_damage=True' and the unit deals 10 damage, the target unit has 5 hp and 0 armor, the target unit would result in -5hp, so the returning damage would be 10. For 'include_overkill_damage=False' this function would return 5. If 'ignore_armor=False' and the unit deals 10 damage, the target unit has 20 hp and 5 armor, the target unit would result in 15hp, so the returning damage would be 5. For 'ignore_armor=True' this function would return 10. :param target: :param ignore_armor: :param include_overkill_damage: """ if self.type_id not in {UnitTypeId.BATTLECRUISER, UnitTypeId.BUNKER}: if not self.can_attack: return 0, 0, 0 if target.type_id != UnitTypeId.COLOSSUS: if not self.can_attack_ground and not target.is_flying: return 0, 0, 0 if not self.can_attack_air and target.is_flying: return 0, 0, 0 # Structures that are not completed can't attack if not self.is_ready: return 0, 0, 0 target_has_guardian_shield: bool = False if ignore_armor: enemy_armor: float = 0 enemy_shield_armor: float = 0 else: # TODO: enemy is under influence of anti armor missile -> reduce armor and shield armor enemy_armor = target.armor + target.armor_upgrade_level enemy_shield_armor = target.shield_upgrade_level # Ultralisk armor upgrade, only works if target belongs to the bot calling this function if ( target.type_id in {UnitTypeId.ULTRALISK, UnitTypeId.ULTRALISKBURROWED} and target.is_mine and UpgradeId.CHITINOUSPLATING in target._bot_object.state.upgrades ): enemy_armor += 2 # Guardian shield adds 2 armor if BuffId.GUARDIANSHIELD in target.buffs: target_has_guardian_shield = True # Anti armor missile of raven if BuffId.RAVENSHREDDERMISSILETINT in target.buffs: enemy_armor -= 2 enemy_shield_armor -= 2 # Hard coded return for battlecruiser because they have no weapon in the API if self.type_id == UnitTypeId.BATTLECRUISER: if target_has_guardian_shield: enemy_armor += 2 enemy_shield_armor += 2 weapon_damage: float = (5 if target.is_flying else 8) + self.attack_upgrade_level weapon_damage = weapon_damage - enemy_shield_armor if target.shield else weapon_damage - enemy_armor return weapon_damage, 0.224, 6 # Fast return for bunkers, since they don't have a weapon similar to BCs if self.type_id == UnitTypeId.BUNKER and self.is_enemy: if self.is_active: # Expect fully loaded bunker with marines return (24, 0.854, 6) return (0, 0, 0) # TODO if bunker belongs to us, use passengers and upgrade level to calculate damage required_target_type: set[int] = ( TARGET_BOTH if target.type_id == UnitTypeId.COLOSSUS else TARGET_GROUND if not target.is_flying else TARGET_AIR ) # Contains total damage, attack speed and attack range damages: list[tuple[float, float, float]] = [] for weapon in self._weapons: if weapon.type not in required_target_type: continue enemy_health: float = target.health enemy_shield: float = target.shield total_attacks: int = weapon.attacks weapon_speed: float = weapon.speed weapon_range: float = weapon.range bonus_damage_per_upgrade = ( 0 if not self.attack_upgrade_level else DAMAGE_BONUS_PER_UPGRADE.get(self.type_id, {}).get(weapon.type, {}).get(None, 1) ) damage_per_attack: float = weapon.damage + self.attack_upgrade_level * bonus_damage_per_upgrade # Remaining damage after all damage is dealt to shield remaining_damage: float = 0 # Calculate bonus damage against target boni: list[float] = [] # TODO: hardcode hellbats when they have blueflame or attack upgrades for bonus in weapon.damage_bonus: # More about damage bonus https://github.com/Blizzard/s2client-proto/blob/b73eb59ac7f2c52b2ca585db4399f2d3202e102a/s2clientprotocol/data.proto#L55 if bonus.attribute in target._type_data._proto.attributes: bonus_damage_per_upgrade = ( 0 if not self.attack_upgrade_level else DAMAGE_BONUS_PER_UPGRADE.get(self.type_id, {}).get(weapon.type, {}).get(bonus.attribute, 0) ) # Hardcode blueflame damage bonus from hellions if ( bonus.attribute == IS_LIGHT and self.type_id == UnitTypeId.HELLION and UpgradeId.HIGHCAPACITYBARRELS in self._bot_object.state.upgrades ): bonus_damage_per_upgrade += 5 # TODO buffs e.g. void ray charge beam vs armored boni.append(bonus.bonus + self.attack_upgrade_level * bonus_damage_per_upgrade) if boni: damage_per_attack += max(boni) # Subtract enemy unit's shield if target.shield > 0: # Fix for ranged units + guardian shield enemy_shield_armor_temp = ( enemy_shield_armor + 2 if target_has_guardian_shield and weapon_range >= 2 else enemy_shield_armor ) # Shield-armor has to be applied while total_attacks > 0 and enemy_shield > 0: # Guardian shield correction enemy_shield -= max(0.5, damage_per_attack - enemy_shield_armor_temp) total_attacks -= 1 if enemy_shield < 0: remaining_damage = -enemy_shield enemy_shield = 0 # TODO roach and hydra in melee range are not affected by guardian shield # Fix for ranged units if enemy has guardian shield buff enemy_armor_temp = enemy_armor + 2 if target_has_guardian_shield and weapon_range >= 2 else enemy_armor # Subtract enemy unit's HP if remaining_damage > 0: enemy_health -= max(0.5, remaining_damage - enemy_armor_temp) while total_attacks > 0 and (include_overkill_damage or enemy_health > 0): # Guardian shield correction enemy_health -= max(0.5, damage_per_attack - enemy_armor_temp) total_attacks -= 1 # Calculate the final damage if not include_overkill_damage: enemy_health = max(0, enemy_health) enemy_shield = max(0, enemy_shield) total_damage_dealt = target.health + target.shield - enemy_health - enemy_shield # Unit modifiers: buffs and upgrades that affect weapon speed and weapon range if self.type_id in { UnitTypeId.ZERGLING, UnitTypeId.MARINE, UnitTypeId.MARAUDER, UnitTypeId.ADEPT, UnitTypeId.HYDRALISK, UnitTypeId.PHOENIX, UnitTypeId.PLANETARYFORTRESS, UnitTypeId.MISSILETURRET, UnitTypeId.AUTOTURRET, }: upgrades: set[UpgradeId] = self._bot_object.state.upgrades if ( self.type_id == UnitTypeId.ZERGLING # Attack speed calculation only works for our unit and self.is_mine and UpgradeId.ZERGLINGATTACKSPEED in upgrades ): # 0.696044921875 for zerglings divided through 1.4 equals (+40% attack speed bonus from the upgrade): weapon_speed /= 1.4 elif ( # Adept ereceive 45% attack speed bonus from glaives self.type_id == UnitTypeId.ADEPT and self.is_mine and UpgradeId.ADEPTPIERCINGATTACK in upgrades ): # TODO next patch: if self.type_id is adept: check if attack speed buff is active, instead of upgrade weapon_speed /= 1.45 elif self.type_id == UnitTypeId.MARINE and BuffId.STIMPACK in self.buffs: # Marine and marauder receive 50% attack speed bonus from stim weapon_speed /= 1.5 elif self.type_id == UnitTypeId.MARAUDER and BuffId.STIMPACKMARAUDER in self.buffs: weapon_speed /= 1.5 elif ( # TODO always assume that the enemy has the range upgrade researched self.type_id == UnitTypeId.HYDRALISK and self.is_mine and UpgradeId.EVOLVEGROOVEDSPINES in upgrades ): weapon_range += 1 elif self.type_id == UnitTypeId.PHOENIX and self.is_mine and UpgradeId.PHOENIXRANGEUPGRADE in upgrades: weapon_range += 2 elif ( self.type_id in {UnitTypeId.PLANETARYFORTRESS, UnitTypeId.MISSILETURRET, UnitTypeId.AUTOTURRET} and self.is_mine and UpgradeId.HISECAUTOTRACKING in upgrades ): weapon_range += 1 # Append it to the list of damages, e.g. both thor and queen attacks work on colossus damages.append((total_damage_dealt, weapon_speed, weapon_range)) # If no attack was found, return (0, 0, 0) if not damages: return 0, 0, 0 # Returns: total potential damage, attack speed, attack range return max(damages, key=lambda damage_tuple: damage_tuple[0]) def calculate_dps_vs_target( self, target: Unit, ignore_armor: bool = False, include_overkill_damage: bool = True, ) -> float: """Returns the DPS against the given target. :param target: :param ignore_armor: :param include_overkill_damage: """ calc_tuple: tuple[float, float, float] = self.calculate_damage_vs_target( target, ignore_armor, include_overkill_damage ) # TODO fix for real time? The result may have to be multiplied by 1.4 because of game_speed=normal if calc_tuple[1] == 0: return 0 return calc_tuple[0] / calc_tuple[1] @property def facing(self) -> float: """Returns direction the unit is facing as a float in range [0,2π). 0 is in direction of x axis.""" return self._proto.facing def is_facing(self, other_unit: Unit, angle_error: float = 0.05) -> bool: """Check if this unit is facing the target unit. If you make angle_error too small, there might be rounding errors. If you make angle_error too big, this function might return false positives. :param other_unit: :param angle_error: """ # TODO perhaps return default True for units that cannot 'face' another unit? e.g. structures (planetary fortress, bunker, missile turret, photon cannon, spine, spore) or sieged tanks angle = math.atan2( other_unit.position_tuple[1] - self.position_tuple[1], other_unit.position_tuple[0] - self.position_tuple[0] ) if angle < 0: angle += math.pi * 2 angle_difference = math.fabs(angle - self.facing) return angle_difference < angle_error @property def footprint_radius(self) -> float | None: """For structures only. For townhalls this returns 2.5 For barracks, spawning pool, gateway, this returns 1.5 For supply depot, this returns 1 For sensor tower, creep tumor, this return 0.5 NOTE: This can be None if a building doesn't have a creation ability. For rich vespene buildings, flying terran buildings, this returns None""" return self._type_data.footprint_radius @property def radius(self) -> float: """Half of unit size. See https://liquipedia.net/starcraft2/Unit_Statistics_(Legacy_of_the_Void)""" return self._proto.radius @property def build_progress(self) -> float: """Returns completion in range [0,1].""" return self._proto.build_progress @property def is_ready(self) -> bool: """Checks if the unit is completed.""" return self.build_progress == 1 @property def cloak(self) -> CloakState: """Returns cloak state. See https://github.com/Blizzard/s2client-api/blob/d9ba0a33d6ce9d233c2a4ee988360c188fbe9dbf/include/sc2api/sc2_unit.h#L95 """ return CloakState(self._proto.cloak) @property def is_cloaked(self) -> bool: """Checks if the unit is cloaked.""" return self._proto.cloak in IS_CLOAKED @property def is_revealed(self) -> bool: """Checks if the unit is revealed.""" return self._proto.cloak == IS_REVEALED @property def can_be_attacked(self) -> bool: """Checks if the unit is revealed or not cloaked and therefore can be attacked.""" return self._proto.cloak in CAN_BE_ATTACKED @cached_property def buffs(self) -> frozenset[BuffId]: """Returns the set of current buffs the unit has.""" return frozenset(BuffId(buff_id) for buff_id in self._proto.buff_ids) @cached_property def is_carrying_minerals(self) -> bool: """Checks if a worker or MULE is carrying (gold-)minerals.""" return not IS_CARRYING_MINERALS.isdisjoint(self.buffs) @cached_property def is_carrying_vespene(self) -> bool: """Checks if a worker is carrying vespene gas.""" return not IS_CARRYING_VESPENE.isdisjoint(self.buffs) @cached_property def is_carrying_resource(self) -> bool: """Checks if a worker is carrying a resource.""" return not IS_CARRYING_RESOURCES.isdisjoint(self.buffs) @property def detect_range(self) -> float: """Returns the detection distance of the unit.""" return self._proto.detect_range @cached_property def is_detector(self) -> bool: """Checks if the unit is a detector. Has to be completed in order to detect and Photoncannons also need to be powered.""" return self.is_ready and (self.type_id in IS_DETECTOR or self.type_id == UNIT_PHOTONCANNON and self.is_powered) @property def radar_range(self) -> float: return self._proto.radar_range @property def is_selected(self) -> bool: """Checks if the unit is currently selected.""" return self._proto.is_selected @property def is_on_screen(self) -> bool: """Checks if the unit is on the screen.""" return self._proto.is_on_screen @property def is_blip(self) -> bool: """Checks if the unit is detected by a sensor tower.""" return self._proto.is_blip @property def is_powered(self) -> bool: """Checks if the unit is powered by a pylon or warppism.""" return self._proto.is_powered @property def is_active(self) -> bool: """Checks if the unit has an order (e.g. unit is currently moving or attacking, structure is currently training or researching).""" return self._proto.is_active # PROPERTIES BELOW THIS COMMENT ARE NOT POPULATED FOR SNAPSHOTS @property def mineral_contents(self) -> int: """Returns the amount of minerals remaining in a mineral field.""" return self._proto.mineral_contents @property def vespene_contents(self) -> int: """Returns the amount of gas remaining in a geyser.""" return self._proto.vespene_contents @property def has_vespene(self) -> bool: """Checks if a geyser has any gas remaining. You can't build extractors on empty geysers.""" return bool(self._proto.vespene_contents) @property def is_flying(self) -> bool: """Checks if the unit is flying.""" return self._proto.is_flying or self.has_buff(BuffId.GRAVITONBEAM) @property def is_burrowed(self) -> bool: """Checks if the unit is burrowed.""" return self._proto.is_burrowed @property def is_hallucination(self) -> bool: """Returns True if the unit is your own hallucination or detected.""" return self._proto.is_hallucination @property def attack_upgrade_level(self) -> int: """Returns the upgrade level of the units attack. # NOTE: Returns 0 for units without a weapon.""" return self._proto.attack_upgrade_level @property def armor_upgrade_level(self) -> int: """Returns the upgrade level of the units armor.""" return self._proto.armor_upgrade_level @property def shield_upgrade_level(self) -> int: """Returns the upgrade level of the units shield. # NOTE: Returns 0 for units without a shield.""" return self._proto.shield_upgrade_level @property def buff_duration_remain(self) -> int: """Returns the amount of remaining frames of the visible timer bar. # NOTE: Returns 0 for units without a timer bar.""" return self._proto.buff_duration_remain @property def buff_duration_max(self) -> int: """Returns the maximum amount of frames of the visible timer bar. # NOTE: Returns 0 for units without a timer bar.""" return self._proto.buff_duration_max # PROPERTIES BELOW THIS COMMENT ARE NOT POPULATED FOR ENEMIES @cached_property def orders(self) -> list[UnitOrder]: """Returns the a list of the current orders.""" # TODO: add examples on how to use unit orders # pyrefly: ignore return [UnitOrder.from_proto(order, self._bot_object) for order in self._proto.orders] @cached_property def order_target(self) -> int | Point2 | None: """Returns the target tag (if it is a Unit) or Point2 (if it is a Position) from the first order, returns None if the unit is idle""" if self.orders: target = self.orders[0].target if target is None or isinstance(target, int): return target return Point2.from_proto(target) return None @property def is_idle(self) -> bool: """Checks if unit is idle.""" return not self._proto.orders def is_using_ability(self, abilities: AbilityId | set[AbilityId]) -> bool: """Check if the unit is using one of the given abilities. Only works for own units.""" if not self.orders: return False if isinstance(abilities, AbilityId): abilities = {abilities} return self.orders[0].ability.id in abilities @cached_property def is_moving(self) -> bool: """Checks if the unit is moving. Only works for own units.""" return self.is_using_ability(AbilityId.MOVE) @cached_property def is_attacking(self) -> bool: """Checks if the unit is attacking. Only works for own units.""" return self.is_using_ability(IS_ATTACKING) @cached_property def is_patrolling(self) -> bool: """Checks if a unit is patrolling. Only works for own units.""" return self.is_using_ability(IS_PATROLLING) @cached_property def is_gathering(self) -> bool: """Checks if a unit is on its way to a mineral field or vespene geyser to mine. Only works for own units.""" return self.is_using_ability(IS_GATHERING) @cached_property def is_returning(self) -> bool: """Checks if a unit is returning from mineral field or vespene geyser to deliver resources to townhall. Only works for own units.""" return self.is_using_ability(IS_RETURNING) @cached_property def is_collecting(self) -> bool: """Checks if a unit is gathering or returning. Only works for own units.""" return self.is_using_ability(IS_COLLECTING) @cached_property def is_constructing_scv(self) -> bool: """Checks if the unit is an SCV that is currently building. Only works for own units.""" return self.is_using_ability(IS_CONSTRUCTING_SCV) @cached_property def is_transforming(self) -> bool: """Checks if the unit transforming. Only works for own units.""" return self.type_id in transforming and self.is_using_ability(transforming[self.type_id]) @cached_property def is_repairing(self) -> bool: """Checks if the unit is an SCV or MULE that is currently repairing. Only works for own units.""" return self.is_using_ability(IS_REPAIRING) @property def add_on_tag(self) -> int: """Returns the tag of the addon of unit. If the unit has no addon, returns 0.""" return self._proto.add_on_tag @property def has_add_on(self) -> bool: """Checks if unit has an addon attached.""" return bool(self._proto.add_on_tag) @cached_property def has_techlab(self) -> bool: """Check if a structure is connected to a techlab addon. This should only ever return True for BARRACKS, FACTORY, STARPORT.""" return self.add_on_tag in self._bot_object.techlab_tags @cached_property def has_reactor(self) -> bool: """Check if a structure is connected to a reactor addon. This should only ever return True for BARRACKS, FACTORY, STARPORT.""" return self.add_on_tag in self._bot_object.reactor_tags @cached_property def add_on_land_position(self) -> Point2: """If this unit is an addon (techlab, reactor), returns the position where a terran building (BARRACKS, FACTORY, STARPORT) has to land to connect to this addon. Why offset (-2.5, 0.5)? See description in 'add_on_position' """ return self.position.offset(Point2((-2.5, 0.5))) @cached_property def add_on_position(self) -> Point2: """If this unit is a terran production building (BARRACKS, FACTORY, STARPORT), this property returns the position of where the addon should be, if it should build one or has one attached. Why offset (2.5, -0.5)? A barracks is of size 3x3. The distance from the center to the edge is 1.5. An addon is 2x2 and the distance from the edge to center is 1. The total distance from center to center on the x-axis is 2.5. The distance from center to center on the y-axis is -0.5. """ return self.position.offset(Point2((2.5, -0.5))) @cached_property def passengers(self) -> set[Unit]: """Returns the units inside a Bunker, CommandCenter, PlanetaryFortress, Medivac, Nydus, Overlord or WarpPrism.""" # pyrefly: ignore return {Unit(unit, self._bot_object) for unit in self._proto.passengers} @cached_property def passengers_tags(self) -> set[int]: """Returns the tags of the units inside a Bunker, CommandCenter, PlanetaryFortress, Medivac, Nydus, Overlord or WarpPrism.""" return {unit.tag for unit in self._proto.passengers} @property def cargo_used(self) -> int: """Returns how much cargo space is currently used in the unit. Note that some units take up more than one space.""" return self._proto.cargo_space_taken @property def has_cargo(self) -> bool: """Checks if this unit has any units loaded.""" return bool(self._proto.cargo_space_taken) @property def cargo_size(self) -> int: """Returns the amount of cargo space the unit needs.""" return self._type_data.cargo_size @property def cargo_max(self) -> int: """How much cargo space is available at maximum.""" return self._proto.cargo_space_max @property def cargo_left(self) -> int: """Returns how much cargo space is currently left in the unit.""" return self._proto.cargo_space_max - self._proto.cargo_space_taken @property def assigned_harvesters(self) -> int: """Returns the number of workers currently gathering resources at a geyser or mining base.""" return self._proto.assigned_harvesters @property def ideal_harvesters(self) -> int: """Returns the ideal harverster count for unit. 3 for gas buildings, 2*n for n mineral patches on that base.""" return self._proto.ideal_harvesters @property def surplus_harvesters(self) -> int: """Returns a positive int if unit has too many harvesters mining, a negative int if it has too few mining. Will only works on townhalls, and gas buildings. """ return self._proto.assigned_harvesters - self._proto.ideal_harvesters @property def weapon_cooldown(self) -> float: """Returns the time until the unit can fire again, returns -1 for units that can't attack. Usage: if unit.weapon_cooldown == 0: unit.attack(target) elif unit.weapon_cooldown < 0: unit.move(closest_allied_unit_because_cant_attack) else: unit.move(retreatPosition)""" if self.can_attack: return self._proto.weapon_cooldown return -1 @property def weapon_ready(self) -> bool: """Checks if the weapon is ready to be fired.""" return self.weapon_cooldown == 0 @property def engaged_target_tag(self) -> int: # TODO What does this do? return self._proto.engaged_target_tag @cached_property def rally_targets(self) -> list[RallyTarget]: """Returns the queue of rallytargets of the structure.""" return [RallyTarget.from_proto(rally_target) for rally_target in self._proto.rally_targets] # Unit functions def has_buff(self, buff: BuffId) -> bool: """Checks if unit has buff 'buff'. :param buff: """ assert isinstance(buff, BuffId), f"{buff} is no BuffId" return buff in self.buffs def train( self, unit: UnitTypeId, queue: bool = False, can_afford_check: bool = False, ) -> UnitCommand | bool: """Orders unit to train another 'unit'. Usage: COMMANDCENTER.train(SCV) :param unit: :param queue: :param can_afford_check: """ creation_ability = self._bot_object.game_data.units[unit.value].creation_ability if creation_ability is None: return False return self( creation_ability.id, queue=queue, subtract_cost=True, can_afford_check=can_afford_check, ) def build( self, unit: UnitTypeId, position: Point2 | Unit | None = None, queue: bool = False, can_afford_check: bool = False, ) -> UnitCommand | bool: """Orders unit to build another 'unit' at 'position'. Usage:: SCV.build(COMMANDCENTER, position) hatchery.build(UnitTypeId.LAIR) # Target for refinery, assimilator and extractor needs to be the vespene geysir unit, not its position SCV.build(REFINERY, target_vespene_geyser) :param unit: :param position: :param queue: :param can_afford_check: """ if unit in {UnitTypeId.EXTRACTOR, UnitTypeId.ASSIMILATOR, UnitTypeId.REFINERY}: assert isinstance(position, Unit), ( "When building the gas structure, the target needs to be a unit (the vespene geysir) not the position of the vespene geysir." ) creation_ability = self._bot_object.game_data.units[unit.value].creation_ability if creation_ability is None: return False return self( creation_ability.id, target=position, queue=queue, subtract_cost=True, can_afford_check=can_afford_check, ) def build_gas( self, target_geysir: Unit, queue: bool = False, can_afford_check: bool = False, ) -> UnitCommand | bool: """Orders unit to build another 'unit' at 'position'. Usage:: # Target for refinery, assimilator and extractor needs to be the vespene geysir unit, not its position SCV.build_gas(target_vespene_geyser) :param target_geysir: :param queue: :param can_afford_check: """ gas_structure_type_id: UnitTypeId = race_gas[self._bot_object.race] assert isinstance(target_geysir, Unit), ( "When building the gas structure, the target needs to be a unit (the vespene geysir) not the position of the vespene geysir." ) creation_ability = self._bot_object.game_data.units[gas_structure_type_id.value].creation_ability if creation_ability is None: return False return self( creation_ability.id, target=target_geysir, queue=queue, subtract_cost=True, can_afford_check=can_afford_check, ) def research( self, upgrade: UpgradeId, queue: bool = False, can_afford_check: bool = False, ) -> UnitCommand | bool: """Orders unit to research 'upgrade'. Requires UpgradeId to be passed instead of AbilityId. :param upgrade: :param queue: :param can_afford_check: """ research_ability = self._bot_object.game_data.upgrades[upgrade.value].research_ability if research_ability is None: return False return self( research_ability.exact_id, queue=queue, subtract_cost=True, can_afford_check=can_afford_check, ) def warp_in( self, unit: UnitTypeId, position: Point2, can_afford_check: bool = False, ) -> UnitCommand | bool: """Orders Warpgate to warp in 'unit' at 'position'. :param unit: :param queue: :param can_afford_check: """ creation_ability = self._bot_object.game_data.units[unit.value].creation_ability if creation_ability is None: return False return self( warpgate_abilities[creation_ability.id], target=position, subtract_cost=True, subtract_supply=True, can_afford_check=can_afford_check, ) def attack(self, target: Unit | Point2, queue: bool = False) -> UnitCommand | bool: """Orders unit to attack. Target can be a Unit or Point2. Attacking a position will make the unit move there and attack everything on its way. :param target: :param queue: """ return self(AbilityId.ATTACK, target=target, queue=queue) def smart(self, target: Unit | Point2, queue: bool = False) -> UnitCommand | bool: """Orders the smart command. Equivalent to a right-click order. :param target: :param queue: """ return self(AbilityId.SMART, target=target, queue=queue) def gather(self, target: Unit, queue: bool = False) -> UnitCommand | bool: """Orders a unit to gather minerals or gas. 'Target' must be a mineral patch or a gas extraction building. :param target: :param queue: """ return self(AbilityId.HARVEST_GATHER, target=target, queue=queue) def return_resource(self, queue: bool = False) -> UnitCommand | bool: """Orders the unit to return resource to the nearest townhall. :param queue: """ return self(AbilityId.HARVEST_RETURN, target=None, queue=queue) def move(self, position: Unit | Point2, queue: bool = False) -> UnitCommand | bool: """Orders the unit to move to 'position'. Target can be a Unit (to follow that unit) or Point2. :param position: :param queue: """ return self(AbilityId.MOVE_MOVE, target=position, queue=queue) def hold_position(self, queue: bool = False) -> UnitCommand | bool: """Orders a unit to stop moving. It will not move until it gets new orders. :param queue: """ return self(AbilityId.HOLDPOSITION, queue=queue) def stop(self, queue: bool = False) -> UnitCommand | bool: """Orders a unit to stop, but can start to move on its own if it is attacked, enemy unit is in range or other friendly units need the space. :param queue: """ return self(AbilityId.STOP, queue=queue) def patrol(self, position: Point2, queue: bool = False) -> UnitCommand | bool: """Orders a unit to patrol between position it has when the command starts and the target position. Can be queued up to seven patrol points. If the last point is the same as the starting point, the unit will patrol in a circle. :param position: :param queue: """ return self(AbilityId.PATROL, target=position, queue=queue) def repair(self, repair_target: Unit, queue: bool = False) -> UnitCommand | bool: """Order an SCV or MULE to repair. :param repair_target: :param queue: """ return self(AbilityId.EFFECT_REPAIR, target=repair_target, queue=queue) def __hash__(self) -> int: return self.tag def __eq__(self, other: Unit | Any) -> bool: """ :param other: """ return self.tag == getattr(other, "tag", -1) def __call__( self, ability: AbilityId, target: Point2 | Unit | None = None, queue: bool = False, subtract_cost: bool = False, subtract_supply: bool = False, can_afford_check: bool = False, ) -> UnitCommand | bool: """Deprecated: Stop using self.do() - This may be removed in the future. :param ability: :param target: :param queue: :param subtract_cost: :param subtract_supply: :param can_afford_check: """ if self._bot_object.unit_command_uses_self_do: return UnitCommand(ability, self, target=target, queue=queue) expected_target: int = self._bot_object.game_data.abilities[ability.value]._proto.target # 1: None, 2: Point, 3: Unit, 4: PointOrUnit, 5: PointOrNone if target is None and expected_target not in {1, 5}: warnings.warn( f"{self} got {ability} with no target but expected {TARGET_HELPER[expected_target]}", RuntimeWarning, stacklevel=2, ) elif isinstance(target, Point2) and expected_target not in {2, 4, 5}: warnings.warn( f"{self} got {ability} with Point2 as target but expected {TARGET_HELPER[expected_target]}", RuntimeWarning, stacklevel=2, ) elif isinstance(target, Unit) and expected_target not in {3, 4}: warnings.warn( f"{self} got {ability} with Unit as target but expected {TARGET_HELPER[expected_target]}", RuntimeWarning, stacklevel=2, ) return self._bot_object.do( UnitCommand(ability, self, target=target, queue=queue), subtract_cost=subtract_cost, subtract_supply=subtract_supply, can_afford_check=can_afford_check, ) ================================================ FILE: sc2/unit_command.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from sc2.constants import COMBINEABLE_ABILITIES from sc2.ids.ability_id import AbilityId from sc2.position import Point2 if TYPE_CHECKING: from sc2.unit import Unit class UnitCommand: def __init__( self, ability: AbilityId, unit: Unit, target: Unit | Point2 | None = None, queue: bool = False ) -> None: """ :param ability: :param unit: :param target: :param queue: """ assert ability in AbilityId, f"ability {ability} is not in AbilityId" assert unit.__class__.__name__ == "Unit", f"unit {unit} is of type {type(unit)}" assert any( [ target is None, isinstance(target, Point2), unit.__class__.__name__ == "Unit", ] ), f"target {target} is of type {type(target)}" assert isinstance(queue, bool), f"queue flag {queue} is of type {type(queue)}" self.ability = ability self.unit = unit self.target = target self.queue = queue @property def combining_tuple(self) -> tuple[AbilityId, Unit | Point2 | None, bool, bool]: return self.ability, self.target, self.queue, self.ability in COMBINEABLE_ABILITIES def __repr__(self) -> str: return f"UnitCommand({self.ability}, {self.unit}, {self.target}, {self.queue})" ================================================ FILE: sc2/units.py ================================================ from __future__ import annotations import random from collections.abc import Callable, Generator, Iterable from itertools import chain from typing import TYPE_CHECKING, Any from s2clientprotocol import raw_pb2 from sc2.ids.unit_typeid import UnitTypeId from sc2.position import Point2 from sc2.unit import Unit if TYPE_CHECKING: from sc2.bot_ai import BotAI class Units(list[Unit]): """A collection of Unit objects. Makes it easy to select units by selectors.""" @classmethod def from_proto(cls, units: list[raw_pb2.Unit], bot_object: BotAI) -> Units: return cls((Unit(raw_unit, bot_object=bot_object) for raw_unit in units), bot_object) def __init__(self, units: Iterable[Unit], bot_object: BotAI) -> None: """ :param units: :param bot_object: """ super().__init__(units) self._bot_object = bot_object def __call__(self, unit_types: UnitTypeId | Iterable[UnitTypeId]) -> Units: """Creates a new mutable Units object from Units or list object. :param unit_types: """ return self.of_type(unit_types) def __iter__(self) -> Generator[Unit, None, None]: return (item for item in super().__iter__()) def copy(self) -> Units: """Creates a new mutable Units object from Units or list object. :param units: """ return Units(self, self._bot_object) def __or__(self, other: Units) -> Units: """ :param other: """ return Units( chain( iter(self), (other_unit for other_unit in other if other_unit.tag not in (self_unit.tag for self_unit in self)), ), self._bot_object, ) def __add__(self, other: Units) -> Units: """ :param other: """ return Units( chain( iter(self), (other_unit for other_unit in other if other_unit.tag not in (self_unit.tag for self_unit in self)), ), self._bot_object, ) def __and__(self, other: Units) -> Units: """ :param other: """ return Units( (other_unit for other_unit in other if other_unit.tag in (self_unit.tag for self_unit in self)), self._bot_object, ) def __sub__(self, other: Units) -> Units: """ :param other: """ return Units( (self_unit for self_unit in self if self_unit.tag not in (other_unit.tag for other_unit in other)), self._bot_object, ) def __hash__(self) -> int: return hash(unit.tag for unit in self) @property def amount(self) -> int: return len(self) @property def empty(self) -> bool: return not bool(self) @property def exists(self) -> bool: return bool(self) def find_by_tag(self, tag: int) -> Unit | None: """ :param tag: """ for unit in self: if unit.tag == tag: return unit return None def by_tag(self, tag: int) -> Unit: """ :param tag: """ unit = self.find_by_tag(tag) if unit is None: raise KeyError("Unit not found") return unit @property def first(self) -> Unit: assert self, "Units object is empty" return self[0] def take(self, n: int) -> Units: """ :param n: """ if n >= self.amount: return self return self.subgroup(self[:n]) @property def random(self) -> Unit: assert self, "Units object is empty" return random.choice(self) def random_or(self, other: Any) -> Unit: return random.choice(self) if self else other def random_group_of(self, n: int) -> Units: """Returns self if n >= self.amount.""" if n < 1: return Units([], self._bot_object) if n >= self.amount: return self return self.subgroup(random.sample(self, n)) def in_attack_range_of(self, unit: Unit, bonus_distance: float = 0) -> Units: """Filters units that are in attack range of the given unit. This uses the unit and target unit.radius when calculating the distance, so it should be accurate. Caution: This may not work well for static structures (bunker, sieged tank, planetary fortress, photon cannon, spine and spore crawler) because it seems attack ranges differ for static / immovable units. Example:: enemy_zerglings = self.enemy_units(UnitTypeId.ZERGLING) my_marine = next((unit for unit in self.units if unit.type_id == UnitTypeId.MARINE), None) if my_marine: all_zerglings_my_marine_can_attack = enemy_zerglings.in_attack_range_of(my_marine) Example:: enemy_mutalisks = self.enemy_units(UnitTypeId.MUTALISK) my_marauder = next((unit for unit in self.units if unit.type_id == UnitTypeId.MARAUDER), None) if my_marauder: all_mutalisks_my_marauder_can_attack = enemy_mutaliskss.in_attack_range_of(my_marauder) # Is empty because mutalisk are flying and marauder cannot attack air :param unit: :param bonus_distance: """ return self.filter(lambda x: unit.target_in_range(x, bonus_distance=bonus_distance)) def closest_distance_to(self, position: Unit | Point2) -> float: """Returns the distance between the closest unit from this group to the target unit. Example:: enemy_zerglings = self.enemy_units(UnitTypeId.ZERGLING) my_marine = next((unit for unit in self.units if unit.type_id == UnitTypeId.MARINE), None) if my_marine: closest_zergling_distance = enemy_zerglings.closest_distance_to(my_marine) # Contains the distance between the marine and the closest zergling :param position: """ assert self, "Units object is empty" if isinstance(position, Unit): return min(self._bot_object._distance_squared_unit_to_unit(unit, position) for unit in self) ** 0.5 return min(self._bot_object._distance_units_to_pos(self, position)) def furthest_distance_to(self, position: Unit | Point2) -> float: """Returns the distance between the furthest unit from this group to the target unit Example:: enemy_zerglings = self.enemy_units(UnitTypeId.ZERGLING) my_marine = next((unit for unit in self.units if unit.type_id == UnitTypeId.MARINE), None) if my_marine: furthest_zergling_distance = enemy_zerglings.furthest_distance_to(my_marine) # Contains the distance between the marine and the furthest away zergling :param position: """ assert self, "Units object is empty" if isinstance(position, Unit): return max(self._bot_object._distance_squared_unit_to_unit(unit, position) for unit in self) ** 0.5 return max(self._bot_object._distance_units_to_pos(self, position)) def closest_to(self, position: Unit | Point2) -> Unit: """Returns the closest unit (from this Units object) to the target unit or position. Example:: enemy_zerglings = self.enemy_units(UnitTypeId.ZERGLING) my_marine = next((unit for unit in self.units if unit.type_id == UnitTypeId.MARINE), None) if my_marine: closest_zergling = enemy_zerglings.closest_to(my_marine) # Contains the zergling that is closest to the target marine :param position: """ assert self, "Units object is empty" if isinstance(position, Unit): return min( (unit1 for unit1 in self), key=lambda unit2: self._bot_object._distance_squared_unit_to_unit(unit2, position), ) distances = self._bot_object._distance_units_to_pos(self, position) return min(((unit, dist) for unit, dist in zip(self, distances)), key=lambda my_tuple: my_tuple[1])[0] def furthest_to(self, position: Unit | Point2) -> Unit: """Returns the furhest unit (from this Units object) to the target unit or position. Example:: enemy_zerglings = self.enemy_units(UnitTypeId.ZERGLING) my_marine = next((unit for unit in self.units if unit.type_id == UnitTypeId.MARINE), None) if my_marine: furthest_zergling = enemy_zerglings.furthest_to(my_marine) # Contains the zergling that is furthest away to the target marine :param position: """ assert self, "Units object is empty" if isinstance(position, Unit): return max( (unit1 for unit1 in self), key=lambda unit2: self._bot_object._distance_squared_unit_to_unit(unit2, position), ) distances = self._bot_object._distance_units_to_pos(self, position) return max(((unit, dist) for unit, dist in zip(self, distances)), key=lambda my_tuple: my_tuple[1])[0] def closer_than(self, distance: float, position: Unit | Point2) -> Units: """Returns all units (from this Units object) that are closer than 'distance' away from target unit or position. Example:: enemy_zerglings = self.enemy_units(UnitTypeId.ZERGLING) my_marine = next((unit for unit in self.units if unit.type_id == UnitTypeId.MARINE), None) if my_marine: close_zerglings = enemy_zerglings.closer_than(3, my_marine) # Contains all zerglings that are distance 3 or less away from the marine (does not include unit radius in calculation) :param distance: :param position: """ if not self: return self if isinstance(position, Unit): distance_squared = distance**2 return self.subgroup( unit for unit in self if self._bot_object._distance_squared_unit_to_unit(unit, position) < distance_squared ) distances = self._bot_object._distance_units_to_pos(self, position) return self.subgroup(unit for unit, dist in zip(self, distances) if dist < distance) def further_than(self, distance: float, position: Unit | Point2) -> Units: """Returns all units (from this Units object) that are further than 'distance' away from target unit or position. Example:: enemy_zerglings = self.enemy_units(UnitTypeId.ZERGLING) my_marine = next((unit for unit in self.units if unit.type_id == UnitTypeId.MARINE), None) if my_marine: far_zerglings = enemy_zerglings.further_than(3, my_marine) # Contains all zerglings that are distance 3 or more away from the marine (does not include unit radius in calculation) :param distance: :param position: """ if not self: return self if isinstance(position, Unit): distance_squared = distance**2 return self.subgroup( unit for unit in self if distance_squared < self._bot_object._distance_squared_unit_to_unit(unit, position) ) distances = self._bot_object._distance_units_to_pos(self, position) return self.subgroup(unit for unit, dist in zip(self, distances) if distance < dist) def in_distance_between( self, position: Unit | Point2 | tuple[float, float], distance1: float, distance2: float ) -> Units: """Returns units that are further than distance1 and closer than distance2 to unit or position. Example:: enemy_zerglings = self.enemy_units(UnitTypeId.ZERGLING) my_marine = next((unit for unit in self.units if unit.type_id == UnitTypeId.MARINE), None) if my_marine: zerglings_filtered = enemy_zerglings.in_distance_between(my_marine, 3, 5) # Contains all zerglings that are between distance 3 and 5 away from the marine (does not include unit radius in calculation) :param position: :param distance1: :param distance2: """ if not self: return self if isinstance(position, Unit): distance1_squared = distance1**2 distance2_squared = distance2**2 return self.subgroup( unit for unit in self if distance1_squared < self._bot_object._distance_squared_unit_to_unit(unit, position) < distance2_squared ) distances = self._bot_object._distance_units_to_pos(self, position) return self.subgroup(unit for unit, dist in zip(self, distances) if distance1 < dist < distance2) def closest_n_units(self, position: Unit | Point2, n: int) -> Units: """Returns the n closest units in distance to position. Example:: enemy_zerglings = self.enemy_units(UnitTypeId.ZERGLING) my_marine = next((unit for unit in self.units if unit.type_id == UnitTypeId.MARINE), None) if my_marine: zerglings_filtered = enemy_zerglings.closest_n_units(my_marine, 5) # Contains 5 zerglings that are the closest to the marine :param position: :param n: """ if not self: return self return self.subgroup(self._list_sorted_by_distance_to(position)[:n]) def furthest_n_units(self, position: Unit | Point2, n: int) -> Units: """Returns the n furhest units in distance to position. Example:: enemy_zerglings = self.enemy_units(UnitTypeId.ZERGLING) my_marine = next((unit for unit in self.units if unit.type_id == UnitTypeId.MARINE), None) if my_marine: zerglings_filtered = enemy_zerglings.furthest_n_units(my_marine, 5) # Contains 5 zerglings that are the furthest to the marine :param position: :param n: """ if not self: return self return self.subgroup(self._list_sorted_by_distance_to(position)[-n:]) def in_distance_of_group(self, other_units: Units, distance: float) -> Units: """Returns units that are closer than distance from any unit in the other units object. :param other_units: :param distance: """ assert other_units, "Other units object is empty" # Return self because there are no enemies if not self: return self distance_squared = distance**2 if len(self) == 1: if any( self._bot_object._distance_squared_unit_to_unit(self[0], target) < distance_squared for target in other_units ): return self return self.subgroup([]) return self.subgroup( self_unit for self_unit in self if any( self._bot_object._distance_squared_unit_to_unit(self_unit, other_unit) < distance_squared for other_unit in other_units ) ) def in_closest_distance_to_group(self, other_units: Units) -> Unit: """Returns unit in shortest distance from any unit in self to any unit in group. Loops over all units in self, then loops over all units in other_units and calculates the shortest distance. Returns the units that is closest to any unit of 'other_units'. :param other_units: """ assert self, "Units object is empty" assert other_units, "Given units object is empty" return min( self, key=lambda self_unit: min( self._bot_object._distance_squared_unit_to_unit(self_unit, other_unit) for other_unit in other_units ), ) def _list_sorted_closest_to_distance(self, position: Unit | Point2, distance: float) -> list[Unit]: """This function should be a bit faster than using units.sorted(key=lambda u: u.distance_to(position)) :param position: :param distance: """ if isinstance(position, Unit): return sorted( self, key=lambda unit: abs(self._bot_object._distance_squared_unit_to_unit(unit, position) - distance), reverse=True, ) distances = self._bot_object._distance_units_to_pos(self, position) unit_dist_dict = {unit.tag: dist for unit, dist in zip(self, distances)} return sorted(self, key=lambda unit2: abs(unit_dist_dict[unit2.tag] - distance), reverse=True) def n_closest_to_distance(self, position: Point2, distance: float, n: int) -> Units: """Returns n units that are the closest to distance away. For example if the distance is set to 5 and you want 3 units, from units with distance [3, 4, 5, 6, 7] to position, the units with distance [4, 5, 6] will be returned :param position: :param distance: """ return self.subgroup(self._list_sorted_closest_to_distance(position=position, distance=distance)[:n]) def n_furthest_to_distance(self, position: Point2, distance: float, n: int) -> Units: """Inverse of the function 'n_closest_to_distance', returns the furthest units instead :param position: :param distance: """ return self.subgroup(self._list_sorted_closest_to_distance(position=position, distance=distance)[-n:]) def subgroup(self, units: Iterable[Unit]) -> Units: """Creates a new mutable Units object from Units or list object. :param units: """ return Units(units, self._bot_object) def filter(self, pred: Callable[[Unit], Any]) -> Units: """Filters the current Units object and returns a new Units object. Example:: from sc2.ids.unit_typeid import UnitTypeId my_marines = self.units.filter(lambda unit: unit.type_id == UnitTypeId.MARINE) completed_structures = self.structures.filter(lambda structure: structure.is_ready) queens_with_energy_to_inject = self.units.filter(lambda unit: unit.type_id == UnitTypeId.QUEEN and unit.energy >= 25) orbitals_with_energy_to_mule = self.structures.filter(lambda structure: structure.type_id == UnitTypeId.ORBITALCOMMAND and structure.energy >= 50) my_units_that_can_shoot_up = self.units.filter(lambda unit: unit.can_attack_air) See more unit properties in unit.py :param pred: """ assert callable(pred), "Function is not callable" return self.subgroup(filter(pred, self)) def sorted(self, key: Callable[[Unit], Any], reverse: bool = False) -> Units: return self.subgroup(sorted(self, key=key, reverse=reverse)) def _list_sorted_by_distance_to(self, position: Unit | Point2, reverse: bool = False) -> list[Unit]: """This function should be a bit faster than using units.sorted(key=lambda u: u.distance_to(position)) :param position: :param reverse: """ if isinstance(position, Unit): return sorted( self, key=lambda unit: self._bot_object._distance_squared_unit_to_unit(unit, position), reverse=reverse ) distances = self._bot_object._distance_units_to_pos(self, position) unit_dist_dict = {unit.tag: dist for unit, dist in zip(self, distances)} return sorted(self, key=lambda unit2: unit_dist_dict[unit2.tag], reverse=reverse) def sorted_by_distance_to(self, position: Unit | Point2, reverse: bool = False) -> Units: """This function should be a bit faster than using units.sorted(key=lambda u: u.distance_to(position)) :param position: :param reverse: """ return self.subgroup(self._list_sorted_by_distance_to(position, reverse=reverse)) def tags_in(self, other: Iterable[int]) -> Units: """Filters all units that have their tags in the 'other' set/list/dict Example:: my_inject_queens = self.units.tags_in(self.queen_tags_assigned_to_do_injects) # Do not use the following as it is slower because it first loops over all units to filter out if they are queens and loops over those again to check if their tags are in the list/set my_inject_queens_slow = self.units(QUEEN).tags_in(self.queen_tags_assigned_to_do_injects) :param other: """ return self.filter(lambda unit: unit.tag in other) def tags_not_in(self, other: Iterable[int]) -> Units: """Filters all units that have their tags not in the 'other' set/list/dict Example:: my_non_inject_queens = self.units.tags_not_in(self.queen_tags_assigned_to_do_injects) # Do not use the following as it is slower because it first loops over all units to filter out if they are queens and loops over those again to check if their tags are in the list/set my_non_inject_queens_slow = self.units(QUEEN).tags_not_in(self.queen_tags_assigned_to_do_injects) :param other: """ return self.filter(lambda unit: unit.tag not in other) def of_type(self, other: UnitTypeId | Iterable[UnitTypeId]) -> Units: """Filters all units that are of a specific type Example:: # Use a set instead of lists in the argument some_attack_units = self.units.of_type({ZERGLING, ROACH, HYDRALISK, BROODLORD}) :param other: """ if isinstance(other, UnitTypeId): other = {other} elif isinstance(other, list): other = set(other) return self.filter(lambda unit: unit.type_id in other) def exclude_type(self, other: UnitTypeId | Iterable[UnitTypeId]) -> Units: """Filters all units that are not of a specific type Example:: # Use a set instead of lists in the argument ignore_units = self.enemy_units.exclude_type({LARVA, EGG, OVERLORD}) :param other: """ if isinstance(other, UnitTypeId): other = {other} elif isinstance(other, list): other = set(other) return self.filter(lambda unit: unit.type_id not in other) def same_tech(self, other: set[UnitTypeId]) -> Units: """Returns all structures that have the same base structure. Untested: This should return the equivalents for WarpPrism, Observer, Overseer, SupplyDepot and others Example:: # All command centers, flying command centers, orbital commands, flying orbital commands, planetary fortress terran_townhalls = self.townhalls.same_tech(UnitTypeId.COMMANDCENTER) # All hatcheries, lairs and hives zerg_townhalls = self.townhalls.same_tech({UnitTypeId.HATCHERY}) # All spires and greater spires spires = self.townhalls.same_tech({UnitTypeId.SPIRE}) # The following returns the same spires = self.townhalls.same_tech({UnitTypeId.GREATERSPIRE}) # This also works with multiple unit types zerg_townhalls_and_spires = self.structures.same_tech({UnitTypeId.HATCHERY, UnitTypeId.SPIRE}) :param other: """ assert isinstance(other, set), ( "Please use a set as this filter function is already fairly slow. For example" + " 'self.units.same_tech({UnitTypeId.LAIR})'" ) tech_alias_types: set[int] = {u.value for u in other} unit_data = self._bot_object.game_data.units for unit_type in other: for same in unit_data[unit_type.value]._proto.tech_alias: tech_alias_types.add(same) return self.filter( lambda unit: unit._proto.unit_type in tech_alias_types or any(same in tech_alias_types for same in unit._type_data._proto.tech_alias) ) def same_unit(self, other: UnitTypeId | Iterable[UnitTypeId]) -> Units: """Returns all units that have the same base unit while being in different modes. Untested: This should return the equivalents for WarpPrism, Observer, Overseer, SupplyDepot and other units that have different modes but still act as the same unit Example:: # All command centers on the ground and flying ccs = self.townhalls.same_unit(UnitTypeId.COMMANDCENTER) # All orbital commands on the ground and flying ocs = self.townhalls.same_unit(UnitTypeId.ORBITALCOMMAND) # All roaches and burrowed roaches roaches = self.units.same_unit(UnitTypeId.ROACH) # This is useful because roach has a different type id when burrowed burrowed_roaches = self.units(UnitTypeId.ROACHBURROWED) :param other: """ if isinstance(other, UnitTypeId): other = {other} unit_alias_types: set[int] = {u.value for u in other} unit_data = self._bot_object.game_data.units for unit_type in other: unit_alias_types.add(unit_data[unit_type.value]._proto.unit_alias) unit_alias_types.discard(0) return self.filter( lambda unit: unit._proto.unit_type in unit_alias_types or unit._type_data._proto.unit_alias in unit_alias_types ) @property def center(self) -> Point2: """Returns the central position of all units.""" assert self, "Units object is empty" return Point2( ( sum(unit._proto.pos.x for unit in self) / self.amount, sum(unit._proto.pos.y for unit in self) / self.amount, ) ) @property def selected(self) -> Units: """Returns all units that are selected by the human player.""" return self.filter(lambda unit: unit.is_selected) @property def tags(self) -> set[int]: """Returns all unit tags as a set.""" return {unit.tag for unit in self} @property def ready(self) -> Units: """Returns all structures that are ready (construction complete).""" return self.filter(lambda unit: unit.is_ready) @property def not_ready(self) -> Units: """Returns all structures that are not ready (construction not complete).""" return self.filter(lambda unit: not unit.is_ready) @property def idle(self) -> Units: """Returns all units or structures that are doing nothing (unit is standing still, structure is doing nothing).""" return self.filter(lambda unit: unit.is_idle) @property def owned(self) -> Units: """Deprecated: All your units.""" return self.filter(lambda unit: unit.is_mine) @property def enemy(self) -> Units: """Deprecated: All enemy units.""" return self.filter(lambda unit: unit.is_enemy) @property def flying(self) -> Units: """Returns all units that are flying.""" return self.filter(lambda unit: unit.is_flying) @property def not_flying(self) -> Units: """Returns all units that not are flying.""" return self.filter(lambda unit: not unit.is_flying) @property def structure(self) -> Units: """Deprecated: All structures.""" return self.filter(lambda unit: unit.is_structure) @property def not_structure(self) -> Units: """Deprecated: All units that are not structures.""" return self.filter(lambda unit: not unit.is_structure) @property def gathering(self) -> Units: """Returns all workers that are mining minerals or vespene (gather command).""" return self.filter(lambda unit: unit.is_gathering) @property def returning(self) -> Units: """Returns all workers that are carrying minerals or vespene and are returning to a townhall.""" return self.filter(lambda unit: unit.is_returning) @property def collecting(self) -> Units: """Returns all workers that are mining or returning resources.""" return self.filter(lambda unit: unit.is_collecting) @property def visible(self) -> Units: """Returns all units or structures that are visible. TODO: add proper description on which units are exactly visible (not snapshots?)""" return self.filter(lambda unit: unit.is_visible) @property def mineral_field(self) -> Units: """Returns all units that are mineral fields.""" return self.filter(lambda unit: unit.is_mineral_field) @property def vespene_geyser(self) -> Units: """Returns all units that are vespene geysers.""" return self.filter(lambda unit: unit.is_vespene_geyser) @property def prefer_idle(self) -> Units: """Sorts units based on if they are idle. Idle units come first.""" return self.sorted(lambda unit: unit.is_idle, reverse=True) ================================================ FILE: sc2/versions.py ================================================ from __future__ import annotations VERSIONS: list[dict[str, int | str]] = [ { "base-version": 52910, "data-hash": "8D9FEF2E1CF7C6C9CBE4FBCA830DDE1C", "fixed-hash": "009BC85EF547B51EBF461C83A9CBAB30", "label": "3.13", "replay-hash": "47BFE9D10F26B0A8B74C637D6327BF3C", "version": 52910, }, { "base-version": 53644, "data-hash": "CA275C4D6E213ED30F80BACCDFEDB1F5", "fixed-hash": "29198786619C9011735BCFD378E49CB6", "label": "3.14", "replay-hash": "5AF236FC012ADB7289DB493E63F73FD5", "version": 53644, }, { "base-version": 54518, "data-hash": "BBF619CCDCC80905350F34C2AF0AB4F6", "fixed-hash": "D5963F25A17D9E1EA406FF6BBAA9B736", "label": "3.15", "replay-hash": "43530321CF29FD11482AB9CBA3EB553D", "version": 54518, }, { "base-version": 54518, "data-hash": "6EB25E687F8637457538F4B005950A5E", "fixed-hash": "D5963F25A17D9E1EA406FF6BBAA9B736", "label": "3.15.1", "replay-hash": "43530321CF29FD11482AB9CBA3EB553D", "version": 54724, }, { "base-version": 55505, "data-hash": "60718A7CA50D0DF42987A30CF87BCB80", "fixed-hash": "0189B2804E2F6BA4C4591222089E63B2", "label": "3.16", "replay-hash": "B11811B13F0C85C29C5D4597BD4BA5A4", "version": 55505, }, { "base-version": 55958, "data-hash": "5BD7C31B44525DAB46E64C4602A81DC2", "fixed-hash": "717B05ACD26C108D18A219B03710D06D", "label": "3.16.1", "replay-hash": "21C8FA403BB1194E2B6EB7520016B958", "version": 55958, }, { "base-version": 56787, "data-hash": "DFD1F6607F2CF19CB4E1C996B2563D9B", "fixed-hash": "4E1C17AB6A79185A0D87F68D1C673CD9", "label": "3.17", "replay-hash": "D0296961C9EA1356F727A2468967A1E2", "version": 56787, }, { "base-version": 56787, "data-hash": "3F2FCED08798D83B873B5543BEFA6C4B", "fixed-hash": "4474B6B7B0D1423DAA76B9623EF2E9A9", "label": "3.17.1", "replay-hash": "D0296961C9EA1356F727A2468967A1E2", "version": 57218, }, { "base-version": 56787, "data-hash": "C690FC543082D35EA0AAA876B8362BEA", "fixed-hash": "4474B6B7B0D1423DAA76B9623EF2E9A9", "label": "3.17.2", "replay-hash": "D0296961C9EA1356F727A2468967A1E2", "version": 57490, }, { "base-version": 57507, "data-hash": "1659EF34997DA3470FF84A14431E3A86", "fixed-hash": "95666060F129FD267C5A8135A8920AA2", "label": "3.18", "replay-hash": "06D650F850FDB2A09E4B01D2DF8C433A", "version": 57507, }, { "base-version": 58400, "data-hash": "2B06AEE58017A7DF2A3D452D733F1019", "fixed-hash": "2CFE1B8757DA80086DD6FD6ECFF21AC6", "label": "3.19", "replay-hash": "227B6048D55535E0FF5607746EBCC45E", "version": 58400, }, { "base-version": 58400, "data-hash": "D9B568472880CC4719D1B698C0D86984", "fixed-hash": "CE1005E9B145BDFC8E5E40CDEB5E33BB", "label": "3.19.1", "replay-hash": "227B6048D55535E0FF5607746EBCC45E", "version": 58600, }, { "base-version": 59587, "data-hash": "9B4FD995C61664831192B7DA46F8C1A1", "fixed-hash": "D5D5798A9CCD099932C8F855C8129A7C", "label": "4.0", "replay-hash": "BB4DA41B57D490BD13C13A594E314BA4", "version": 59587, }, { "base-version": 60196, "data-hash": "1B8ACAB0C663D5510941A9871B3E9FBE", "fixed-hash": "9327F9AF76CF11FC43D20E3E038B1B7A", "label": "4.1", "replay-hash": "AEA0C2A9D56E02C6B7D21E889D6B9B2F", "version": 60196, }, { "base-version": 60321, "data-hash": "5C021D8A549F4A776EE9E9C1748FFBBC", "fixed-hash": "C53FA3A7336EDF320DCEB0BC078AEB0A", "label": "4.1.1", "replay-hash": "8EE054A8D98C7B0207E709190A6F3953", "version": 60321, }, { "base-version": 60321, "data-hash": "33D9FE28909573253B7FC352CE7AEA40", "fixed-hash": "FEE6F86A211380DF509F3BBA58A76B87", "label": "4.1.2", "replay-hash": "8EE054A8D98C7B0207E709190A6F3953", "version": 60604, }, { "base-version": 60321, "data-hash": "F486693E00B2CD305B39E0AB254623EB", "fixed-hash": "AF7F5499862F497C7154CB59167FEFB3", "label": "4.1.3", "replay-hash": "8EE054A8D98C7B0207E709190A6F3953", "version": 61021, }, { "base-version": 60321, "data-hash": "2E2A3F6E0BAFE5AC659C4D39F13A938C", "fixed-hash": "F9A68CF1FBBF867216FFECD9EAB72F4A", "label": "4.1.4", "replay-hash": "8EE054A8D98C7B0207E709190A6F3953", "version": 61545, }, { "base-version": 62347, "data-hash": "C0C0E9D37FCDBC437CE386C6BE2D1F93", "fixed-hash": "A5C4BE991F37F1565097AAD2A707FC4C", "label": "4.2", "replay-hash": "2167A7733637F3AFC49B210D165219A7", "version": 62347, }, { "base-version": 62848, "data-hash": "29BBAC5AFF364B6101B661DB468E3A37", "fixed-hash": "ABAF9318FE79E84485BEC5D79C31262C", "label": "4.2.1", "replay-hash": "A7ACEC5759ADB459A5CEC30A575830EC", "version": 62848, }, { "base-version": 63454, "data-hash": "3CB54C86777E78557C984AB1CF3494A0", "fixed-hash": "A9DCDAA97F7DA07F6EF29C0BF4DFC50D", "label": "4.2.2", "replay-hash": "A7ACEC5759ADB459A5CEC30A575830EC", "version": 63454, }, { "base-version": 64469, "data-hash": "C92B3E9683D5A59E08FC011F4BE167FF", "fixed-hash": "DDF3E0A6C00DC667F59BF90F793C71B8", "label": "4.3", "replay-hash": "6E80072968515101AF08D3953FE3EEBA", "version": 64469, }, { "base-version": 65094, "data-hash": "E5A21037AA7A25C03AC441515F4E0644", "fixed-hash": "09EF8E9B96F14C5126F1DB5378D15F3A", "label": "4.3.1", "replay-hash": "DD9B57C516023B58F5B588377880D93A", "version": 65094, }, { "base-version": 65384, "data-hash": "B6D73C85DFB70F5D01DEABB2517BF11C", "fixed-hash": "615C1705E4C7A5FD8690B3FD376C1AFE", "label": "4.3.2", "replay-hash": "DD9B57C516023B58F5B588377880D93A", "version": 65384, }, { "base-version": 65895, "data-hash": "BF41339C22AE2EDEBEEADC8C75028F7D", "fixed-hash": "C622989A4C0AF7ED5715D472C953830B", "label": "4.4", "replay-hash": "441BBF1A222D5C0117E85B118706037F", "version": 65895, }, { "base-version": 66668, "data-hash": "C094081D274A39219061182DBFD7840F", "fixed-hash": "1C236A42171AAC6DD1D5E50D779C522D", "label": "4.4.1", "replay-hash": "21D5B4B4D5175C562CF4C4A803C995C6", "version": 66668, }, { "base-version": 67188, "data-hash": "2ACF84A7ECBB536F51FC3F734EC3019F", "fixed-hash": "2F0094C990E0D4E505570195F96C2A0C", "label": "4.5", "replay-hash": "E9873B3A3846F5878CEE0D1E2ADD204A", "version": 67188, }, { "base-version": 67188, "data-hash": "6D239173B8712461E6A7C644A5539369", "fixed-hash": "A1BC35751ACC34CF887321A357B40158", "label": "4.5.1", "replay-hash": "E9873B3A3846F5878CEE0D1E2ADD204A", "version": 67344, }, { "base-version": 67926, "data-hash": "7DE59231CBF06F1ECE9A25A27964D4AE", "fixed-hash": "570BEB69151F40D010E89DE1825AE680", "label": "4.6", "replay-hash": "DA662F9091DF6590A5E323C21127BA5A", "version": 67926, }, { "base-version": 67926, "data-hash": "BEA99B4A8E7B41E62ADC06D194801BAB", "fixed-hash": "309E45F53690F8D1108F073ABB4D4734", "label": "4.6.1", "replay-hash": "DA662F9091DF6590A5E323C21127BA5A", "version": 68195, }, { "base-version": 69232, "data-hash": "B3E14058F1083913B80C20993AC965DB", "fixed-hash": "21935E776237EF12B6CC73E387E76D6E", "label": "4.6.2", "replay-hash": "A230717B315D83ACC3697B6EC28C3FF6", "version": 69232, }, { "base-version": 70154, "data-hash": "8E216E34BC61ABDE16A59A672ACB0F3B", "fixed-hash": "09CD819C667C67399F5131185334243E", "label": "4.7", "replay-hash": "9692B04D6E695EF08A2FB920979E776C", "version": 70154, }, { "base-version": 70154, "data-hash": "94596A85191583AD2EBFAE28C5D532DB", "fixed-hash": "0AE50F82AC1A7C0DCB6A290D7FBA45DB", "label": "4.7.1", "replay-hash": "D74FBB3CB0897A3EE8F44E78119C4658", "version": 70326, }, { "base-version": 71061, "data-hash": "760581629FC458A1937A05ED8388725B", "fixed-hash": "815C099DF1A17577FDC186FDB1381B16", "label": "4.8", "replay-hash": "BD692311442926E1F0B7C17E9ABDA34B", "version": 71061, }, { "base-version": 71523, "data-hash": "FCAF3F050B7C0CC7ADCF551B61B9B91E", "fixed-hash": "4593CC331691620509983E92180A309A", "label": "4.8.1", "replay-hash": "BD692311442926E1F0B7C17E9ABDA34B", "version": 71523, }, { "base-version": 71663, "data-hash": "FE90C92716FC6F8F04B74268EC369FA5", "fixed-hash": "1DBF3819F3A7367592648632CC0D5BFD", "label": "4.8.2", "replay-hash": "E43A9885B3EFAE3D623091485ECCCB6C", "version": 71663, }, { "base-version": 72282, "data-hash": "0F14399BBD0BA528355FF4A8211F845B", "fixed-hash": "E9958B2CB666DCFE101D23AF87DB8140", "label": "4.8.3", "replay-hash": "3AF3657F55AB961477CE268F5CA33361", "version": 72282, }, { "base-version": 73286, "data-hash": "CD040C0675FD986ED37A4CA3C88C8EB5", "fixed-hash": "62A146F7A0D19A8DD05BF011631B31B8", "label": "4.8.4", "replay-hash": "EE3A89F443BE868EBDA33A17C002B609", "version": 73286, }, { "base-version": 73559, "data-hash": "B2465E73AED597C74D0844112D582595", "fixed-hash": "EF0A43C33413613BC7343B86C0A7CC92", "label": "4.8.5", "replay-hash": "147388D35E76861BD4F590F8CC5B7B0B", "version": 73559, }, { "base-version": 73620, "data-hash": "AA18FEAD6573C79EF707DF44ABF1BE61", "fixed-hash": "4D76491CCAE756F0498D1C5B2973FF9C", "label": "4.8.6", "replay-hash": "147388D35E76861BD4F590F8CC5B7B0B", "version": 73620, }, { "base-version": 74071, "data-hash": "70C74A2DCA8A0D8E7AE8647CAC68ACCA", "fixed-hash": "C4A3F01B4753245296DC94BC1B5E9B36", "label": "4.9", "replay-hash": "19D15E5391FACB379BFCA262CA8FD208", "version": 74071, }, { "base-version": 74456, "data-hash": "218CB2271D4E2FA083470D30B1A05F02", "fixed-hash": "E82051387C591CAB1212B64073759826", "label": "4.9.1", "replay-hash": "1586ADF060C26219FF3404673D70245B", "version": 74456, }, { "base-version": 74741, "data-hash": "614480EF79264B5BD084E57F912172FF", "fixed-hash": "500CC375B7031C8272546B78E9BE439F", "label": "4.9.2", "replay-hash": "A7FAC56F940382E05157EAB19C932E3A", "version": 74741, }, { "base-version": 75025, "data-hash": "C305368C63621480462F8F516FB64374", "fixed-hash": "DEE7842C8BCB6874EC254AA3D45365F7", "label": "4.9.3", "replay-hash": "A7FAC56F940382E05157EAB19C932E3A", "version": 75025, }, { "base-version": 75689, "data-hash": "B89B5D6FA7CBF6452E721311BFBC6CB2", "fixed-hash": "2B2097DC4AD60A2D1E1F38691A1FF111", "label": "4.10", "replay-hash": "6A60E59031A7DB1B272EE87E51E4C7CD", "version": 75689, }, { "base-version": 75800, "data-hash": "DDFFF9EC4A171459A4F371C6CC189554", "fixed-hash": "1FB8FAF4A87940621B34F0B8F6FDDEA6", "label": "4.10.1", "replay-hash": "6A60E59031A7DB1B272EE87E51E4C7CD", "version": 75800, }, { "base-version": 76052, "data-hash": "D0F1A68AA88BA90369A84CD1439AA1C3", "fixed-hash": "", "label": "4.10.2", "replay-hash": "", "version": 76052, }, { "base-version": 76114, "data-hash": "CDB276D311F707C29BA664B7754A7293", "fixed-hash": "", "label": "4.10.3", "replay-hash": "", "version": 76114, }, { "base-version": 76811, "data-hash": "FF9FA4EACEC5F06DEB27BD297D73ED67", "fixed-hash": "", "label": "4.10.4", "replay-hash": "", "version": 76811, }, { "base-version": 77379, "data-hash": "70E774E722A58287EF37D487605CD384", "fixed-hash": "", "label": "4.11.0", "replay-hash": "", "version": 77379, }, { "base-version": 77379, "data-hash": "F92D1127A291722120AC816F09B2E583", "fixed-hash": "", "label": "4.11.1", "replay-hash": "", "version": 77474, }, { "base-version": 77535, "data-hash": "FC43E0897FCC93E4632AC57CBC5A2137", "fixed-hash": "", "label": "4.11.2", "replay-hash": "", "version": 77535, }, { "base-version": 77661, "data-hash": "A15B8E4247434B020086354F39856C51", "fixed-hash": "", "label": "4.11.3", "replay-hash": "", "version": 77661, }, { "base-version": 78285, "data-hash": "69493AFAB5C7B45DDB2F3442FD60F0CF", "fixed-hash": "21D2EBD5C79DECB3642214BAD4A7EF56", "label": "4.11.4", "replay-hash": "CAB5C056EDBDA415C552074BF363CC85", "version": 78285, }, { "base-version": 79998, "data-hash": "B47567DEE5DC23373BFF57194538DFD3", "fixed-hash": "0A698A1B072BC4B087F44DDEF0BE361E", "label": "4.12.0", "replay-hash": "9E15AA09E15FE3AF3655126CEEC7FF42", "version": 79998, }, { "base-version": 80188, "data-hash": "44DED5AED024D23177C742FC227C615A", "fixed-hash": "0A698A1B072BC4B087F44DDEF0BE361E", "label": "4.12.1", "replay-hash": "9E15AA09E15FE3AF3655126CEEC7FF42", "version": 80188, }, { "base-version": 80949, "data-hash": "9AE39C332883B8BF6AA190286183ED72", "fixed-hash": "DACEAFAB8B983C08ACD31ABC085A0052", "label": "5.0.0", "replay-hash": "28C41277C5837AABF9838B64ACC6BDCF", "version": 80949, }, { "base-version": 81009, "data-hash": "0D28678BC32E7F67A238F19CD3E0A2CE", "fixed-hash": "DACEAFAB8B983C08ACD31ABC085A0052", "label": "5.0.1", "replay-hash": "28C41277C5837AABF9838B64ACC6BDCF", "version": 81009, }, { "base-version": 81102, "data-hash": "DC0A1182FB4ABBE8E29E3EC13CF46F68", "fixed-hash": "0C193BD5F63BBAB79D798278F8B2548E", "label": "5.0.2", "replay-hash": "08BB9D4CAE25B57160A6E4AD7B8E1A5A", "version": 81102, }, { "base-version": 81433, "data-hash": "5FD8D4B6B52723B44862DF29F232CF31", "fixed-hash": "4FC35CEA63509AB06AA80AACC1B3B700", "label": "5.0.3", "replay-hash": "0920F1BD722655B41DA096B98CC0912D", "version": 81433, }, { "base-version": 82457, "data-hash": "D2707E265785612D12B381AF6ED9DBF4", "fixed-hash": "ED05F0DB335D003FBC3C7DEF69911114", "label": "5.0.4", "replay-hash": "7D9EE968AAD81761334BD9076BFD9EFF", "version": 82457, }, { "base-version": 82893, "data-hash": "D795328C01B8A711947CC62AA9750445", "fixed-hash": "ED05F0DB335D003FBC3C7DEF69911114", "label": "5.0.5", "replay-hash": "7D9EE968AAD81761334BD9076BFD9EFF", "version": 82893, }, { "base-version": 83830, "data-hash": "B4745D6A4F982A3143C183D8ACB6C3E3", "fixed-hash": "ed05f0db335d003fbc3c7def69911114", "label": "5.0.6", "replay-hash": "7D9EE968AAD81761334BD9076BFD9EFF", "version": 83830, }, { "base-version": 84643, "data-hash": "A389D1F7DF9DD792FBE980533B7119FF", "fixed-hash": "368DE29820A74F5BE747543AC02DB3F8", "label": "5.0.7", "replay-hash": "7D9EE968AAD81761334BD9076BFD9EFF", "version": 84643, }, { "base-version": 86383, "data-hash": "22EAC562CD0C6A31FB2C2C21E3AA3680", "fixed-hash": "B19F4D8B87A2835F9447CA17EDD40C1E", "label": "5.0.8", "replay-hash": "7D9EE968AAD81761334BD9076BFD9EFF", "version": 86383, }, { "base-version": 87702, "data-hash": "F799E093428D419FD634CCE9B925218C", "fixed-hash": "B19F4D8B87A2835F9447CA17EDD40C1E", "label": "5.0.9", "replay-hash": "7D9EE968AAD81761334BD9076BFD9EFF", "version": 87702, }, { "base-version": 88500, "data-hash": "F38043A301B034A78AD13F558257DCF8", "fixed-hash": "F3853B6E3B6013415CAC30EF3B27564B", "label": "5.0.10", "replay-hash": "A79CD3B6C6DADB0ECAEFA06E6D18E47B", "version": 88500, }, { "base-version": 89720, "data-hash": "D371D4D7D1E6C131B24A09FC0E758547", "fixed-hash": "F3853B6E3B6013415CAC30EF3B27564B", "label": "5.0.11", "replay-hash": "A79CD3B6C6DADB0ECAEFA06E6D18E47B", "version": 89720, }, { "base-version": 91115, "data-hash": "7857A76754FEB47C823D18993C476BF0", "fixed-hash": "99E19D19DA59112C1744A83CB49614A5", "label": "5.0.12", "replay-hash": "BE64E420B329BD2A7D10EEBC0039D6E5", "version": 89720, }, { "base-version": 92028, "data-hash": "2B7746A6706F919775EF1BADFC95EA1C", "fixed-hash": "163B1CDF46F09B621F6312CD6901228E", "label": "5.0.13", "replay-hash": "BE64E420B329BD2A7D10EEBC0039D6E5", "version": 92028, }, { "base-version": 93333, "data-hash": "446907060311fb1cc29eb31e547bb9fd", "fixed-hash": "BE86048D1DCE8650E1655D2FE2B665A8", "label": "5.0.14.93333", "replay-hash": "BE64E420B329BD2A7D10EEBC0039D6E5", "version": 93333, }, { "base-version": 94137, "data-hash": "519EE8D06E384469C652DD58FC6016AC", "fixed-hash": "B100C340B3D0797CBE914AE091A68653", "label": "5.0.14.94137", "replay-hash": "BE64E420B329BD2A7D10EEBC0039D6E5", "version": 94137, }, ] ================================================ FILE: sc2/wsl.py ================================================ from __future__ import annotations import os import re import subprocess from pathlib import Path, PureWindowsPath from loguru import logger ## This file is used for compatibility with WSL and shouldn't need to be ## accessed directly by any bot clients def win_path_to_wsl_path(path) -> Path: """Convert a path like C:\\foo to /mnt/c/foo""" return Path("/mnt") / PureWindowsPath(re.sub("^([A-Z]):", lambda m: m.group(1).lower(), path)) def wsl_path_to_win_path(path) -> PureWindowsPath: """Convert a path like /mnt/c/foo to C:\\foo""" return PureWindowsPath(re.sub("^/mnt/([a-z])", lambda m: m.group(1).upper() + ":", path)) def get_wsl_home(): """Get home directory of from Windows, even if run in WSL""" proc = subprocess.run(["powershell.exe", "-Command", "Write-Host -NoNewLine $HOME"], capture_output=True) if proc.returncode != 0: return None return win_path_to_wsl_path(proc.stdout.decode("utf-8")) RUN_SCRIPT = """$proc = Start-Process -NoNewWindow -PassThru "%s" "%s" if ($proc) { Write-Host $proc.id exit $proc.ExitCode } else { exit 1 }""" def run(popen_args, sc2_cwd) -> subprocess.Popen[str]: """Run SC2 in Windows and get the pid so that it can be killed later.""" path = wsl_path_to_win_path(popen_args[0]) args = " ".join(popen_args[1:]) return subprocess.Popen( ["powershell.exe", "-Command", RUN_SCRIPT % (path, args)], cwd=sc2_cwd, stdout=subprocess.PIPE, universal_newlines=True, bufsize=1, ) def kill(wsl_process) -> bool: """Needed to kill a process started with WSL. Returns true if killed successfully.""" # HACK: subprocess and WSL1 appear to have a nasty interaction where # any streams are never closed and the process is never considered killed, # despite having an exit code (this works on WSL2 as well, but isn't # necessary). As a result, # 1: We need to read using readline (to make sure we block long enough to # get the exit code in the rare case where the user immediately hits ^C) out = wsl_process.stdout.readline().rstrip() # 2: We need to use __exit__, since kill() calls send_signal(), which thinks # the process has already exited! wsl_process.__exit__(None, None, None) proc = subprocess.run(["taskkill.exe", "-f", "-pid", out], capture_output=True) return proc.returncode == 0 # Returns 128 on failure def detect() -> str | None: """Detect the current running version of WSL, and bail out if it doesn't exist""" # Allow disabling WSL detection with an environment variable if os.getenv("SC2_WSL_DETECT", "1") == "0": return None wsl_name = os.environ.get("WSL_DISTRO_NAME") if not wsl_name: return None try: wsl_proc = subprocess.run(["wsl.exe", "--list", "--running", "--verbose"], capture_output=True) except (OSError, ValueError): return None if wsl_proc.returncode != 0: return None # WSL.exe returns a bunch of null characters for some reason, as well as # windows-style linebreaks. It's inconsistent about how many \rs it uses # and this could change in the future, so strip out all junk and split by # Unix-style newlines for safety's sake. lines = re.sub(r"\000|\r", "", wsl_proc.stdout.decode("utf-8")).split("\n") def line_has_proc(ln: str): if wsl_name is not None: return re.search("^\\s*[*]?\\s+" + wsl_name, ln) def line_version(ln: str): return re.sub("^.*\\s+(\\d+)\\s*$", "\\1", ln) versions = [line_version(ln) for ln in lines if line_has_proc(ln)] try: version = versions[0] if int(version) not in [1, 2]: return None except (ValueError, IndexError): return None logger.info(f"WSL version {version} detected") if version == "2" and not (os.environ.get("SC2CLIENTHOST") and os.environ.get("SC2SERVERHOST")): logger.warning("You appear to be running WSL2 without your hosts configured correctly.") logger.warning("This may result in SC2 staying on a black screen and not connecting to your bot.") logger.warning("Please see the python-sc2 README for WSL2 configuration instructions.") return "WSL" + version ================================================ FILE: test/Dockerfile ================================================ # Buildable via command from root folder # docker build -f test/Dockerfile -t test_image --build-arg PYTHON_VERSION=3.10 --build-arg SC2_VERSION=4.10 . # For more info see https://github.com/BurnySc2/python-sc2-docker ARG PYTHON_VERSION=3.10 ARG SC2_VERSION=4.10 ARG VERSION_NUMBER=1.0.0 FROM burnysc2/python-sc2-docker:py_$PYTHON_VERSION-sc2_$SC2_VERSION-v$VERSION_NUMBER # Debugging purposes RUN echo $PYTHON_VERSION RUN echo $SC2_VERSION RUN echo $VERSION_NUMBER # Copy files from the current commit (the python-sc2 folder) to root ADD . /root/python-sc2 # Install the python-sc2 library and its requirements (s2clientprotocol etc.) to python WORKDIR /root/python-sc2 RUN pip install --no-cache-dir uv \ # This will not include dev dependencies && uv export --format requirements-txt --output-file requirements.txt --no-hashes \ && pip install --no-cache-dir -r requirements.txt # This will be executed during the container run instead: # docker run test_image -c "uv run python examples/protoss/cannon_rush.py" ENTRYPOINT [ "/bin/bash" ] ================================================ FILE: test/__init__.py ================================================ ================================================ FILE: test/autotest_bot.py ================================================ from __future__ import annotations from loguru import logger from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Race from sc2.ids.ability_id import AbilityId from sc2.ids.effect_id import EffectId from sc2.ids.unit_typeid import UnitTypeId from sc2.main import run_game from sc2.player import Bot from sc2.position import Point2 from sc2.unit import Unit from sc2.units import Units class TestBot(BotAI): def __init__(self): BotAI.__init__(self) # The time the bot has to complete all tests, here: the number of game seconds self.game_time_timeout_limit = 20 * 60 # 20 minutes ingame time # Check how many test action functions we have # At least 4 tests because we test properties and variables self.action_tests = [ getattr(self, f"test_botai_actions{index}") for index in range(4000) if hasattr(getattr(self, f"test_botai_actions{index}", 0), "__call__") ] self.tests_done_by_name = set() # Keep track of the action index and when the last action was started self.current_action_index = 1 self.iteration_last_action_started = 8 # There will be 20 iterations of the bot doing nothing between tests self.iteration_wait_time_between_actions = 20 # Variables for test_botai_actions11 async def on_start(self): self.client.game_step = 8 # await self.client.quick_save() await self.distribute_workers() async def on_step(self, iteration): if iteration == 0: await self.chat_send("(glhf)") # Test if chat message was sent correctly if iteration == 1: assert len(self.state.chat) >= 1, self.state.chat # Tests at start if iteration == 3: # No need to use try except as the travis test script checks for "Traceback" in STDOUT await self.test_botai_properties() await self.test_botai_functions() await self.test_game_state_static_variables() await self.test_game_info_static_variables() # Test actions if iteration == 7: for action_test in self.action_tests: await action_test() # Exit bot if iteration > 100: logger.info(f"Tests completed after {round(self.time, 1)} seconds") exit(0) async def clean_up_center(self): map_center = self.game_info.map_center # Remove everything close to map center my_units = self.all_own_units if my_units: my_units = my_units.closer_than(20, map_center) if my_units: await self.client.debug_kill_unit(my_units) enemy_units = self.enemy_units | self.enemy_structures if enemy_units: enemy_units = enemy_units.closer_than(20, map_center) if enemy_units: await self.client.debug_kill_unit(enemy_units) await self._advance_steps(2) # Test BotAI properties, starting conditions async def test_botai_properties(self): assert 1 <= self.player_id <= 2, self.player_id assert self.race == Race.Terran, self.race assert 0 <= self.time <= 180, self.time assert self.start_location == self.townhalls.random.position, ( self.start_location, self.townhalls.random.position, ) for loc in self.enemy_start_locations: assert isinstance(loc, Point2), loc assert loc.distance_to(self.start_location) > 20, (loc, self.start_location) assert self.main_base_ramp.top_center.distance_to(self.start_location) < 30, self.main_base_ramp.top_center assert self.can_afford(UnitTypeId.SCV) assert self.owned_expansions == {self.townhalls.first.position: self.townhalls.first} # Test if bot start location is in expansion locations assert self.townhalls.random.position in self.expansion_locations_list # Test if enemy start locations are in expansion locations for location in self.enemy_start_locations: assert location in self.expansion_locations_list self.tests_done_by_name.add("test_botai_properties") # Test BotAI functions async def test_botai_functions(self): for location in self.expansion_locations_list: # Can't build on spawn locations, skip these if location in self.enemy_start_locations or location == self.start_location: continue assert (await self.can_place(UnitTypeId.COMMANDCENTER, [location]))[0] assert (await self.can_place(AbilityId.TERRANBUILD_COMMANDCENTER, [location]))[0] # TODO Remove the following two lines if can_place function gets fully converted to only accept list of positions assert await self.can_place(UnitTypeId.COMMANDCENTER, [location]) assert await self.can_place(AbilityId.TERRANBUILD_COMMANDCENTER, [location]) assert await self.can_place_single(UnitTypeId.COMMANDCENTER, location) assert await self.can_place_single(AbilityId.TERRANBUILD_COMMANDCENTER, location) await self.find_placement(UnitTypeId.COMMANDCENTER, location) assert len(await self.get_available_abilities(self.workers)) == self.workers.amount self.tests_done_by_name.add("test_botai_functions") # Test self.state variables async def test_game_state_static_variables(self): assert len(self.state.actions) == 0, self.state.actions assert len(self.state.action_errors) == 0, self.state.action_errors assert len(self.state.chat) == 0, self.state.chat assert self.state.game_loop > 0, self.state.game_loop assert self.state.score.collection_rate_minerals >= 0, self.state.score.collection_rate_minerals assert len(self.state.upgrades) == 0, self.state.upgrades self.tests_done_by_name.add("test_game_state_static_variables") # Test self._game_info variables async def test_game_info_static_variables(self): assert len(self.game_info.players) == 2, self.game_info.players assert len(self.game_info.map_ramps) >= 2, self.game_info.map_ramps assert len(self.game_info.player_races) == 2, self.game_info.player_races self.tests_done_by_name.add("test_game_info_static_variables") async def test_botai_actions1(self): # Test BotAI action: train SCV while self.already_pending(UnitTypeId.SCV) < 1: if self.can_afford(UnitTypeId.SCV): self.townhalls.random.train(UnitTypeId.SCV) await self._advance_steps(2) await self._advance_steps(2) logger.warning("Action test 01 successful.") # Test BotAI action: move all SCVs to center of map async def test_botai_actions2(self): center = self.game_info.map_center def temp_filter(unit: Unit): return ( unit.is_moving or unit.is_patrolling or unit.orders and unit.orders[0] == AbilityId.HOLDPOSITION_HOLD or unit.is_attacking ) scv_action_list = ["move", "patrol", "attack", "hold", "scan_move"] while self.units.filter(lambda unit: temp_filter(unit)).amount < len(scv_action_list): scv: Unit for index, scv in enumerate(self.workers): if index > len(scv_action_list): scv.stop() action = scv_action_list[index % len(scv_action_list)] if action == "move": scv.move(center) elif action == "patrol": scv.patrol(center) elif action == "attack": scv.attack(center) elif action == "hold": scv.hold_position() await self._advance_steps(2) await self._advance_steps(2) logger.warning("Action test 02 successful.") async def test_botai_actions3(self): # Test BotAI action: move some scvs to the center, some to minerals center = self.game_info.map_center while self.units.filter(lambda x: x.is_moving).amount < 6 and self.units.gathering.amount >= 6: scvs = self.workers scvs1 = scvs[:6] scvs2 = scvs[6:] for scv in scvs1: scv.move(center) mf = self.mineral_field.closest_to(self.townhalls.random) for scv in scvs2: scv.gather(mf) await self._advance_steps(2) await self._advance_steps(2) logger.warning("Action test 03 successful.") async def test_botai_actions4(self): # Test BotAI action: move all SCVs to mine minerals near townhall while self.units.gathering.amount < 12: mf = self.mineral_field.closest_to(self.townhalls.random) for scv in self.workers: scv.gather(mf) await self._advance_steps(2) await self._advance_steps(2) logger.warning("Action test 04 successful.") async def test_botai_actions5(self): # Test BotAI action: self.expand_now() which tests for get_next_expansion, select_build_worker, can_place, find_placement, build and can_afford # Wait till worker has started construction of CC while True: if self.can_afford(UnitTypeId.COMMANDCENTER): await self.get_next_expansion() await self.expand_now() await self._advance_steps(10) assert self.structures_without_construction_SCVs(UnitTypeId.COMMANDCENTER).amount == 0 if self.townhalls(UnitTypeId.COMMANDCENTER).amount >= 2: assert self.townhalls(UnitTypeId.COMMANDCENTER).not_ready.amount == 1 assert self.already_pending(UnitTypeId.COMMANDCENTER) == 1 # The CC construction has started, 'worker_en_route_to_build' should show 0 assert self.worker_en_route_to_build(UnitTypeId.COMMANDCENTER) == 0 break elif self.already_pending(UnitTypeId.COMMANDCENTER) == 1: assert self.worker_en_route_to_build(UnitTypeId.COMMANDCENTER) == 1 await self._advance_steps(2) logger.warning("Action test 05 successful.") async def test_botai_actions6(self): # Test if reaper grenade shows up in effects center = self.game_info.map_center while True: if self.units(UnitTypeId.REAPER).amount < 10: await self.client.debug_create_unit([(UnitTypeId.REAPER, 10, center, 1)]) for reaper in self.units(UnitTypeId.REAPER): reaper(AbilityId.KD8CHARGE_KD8CHARGE, center) # logger.info(f"Effects: {self.state.effects}") for effect in self.state.effects: # logger.info(f"Effect: {effect}") pass # Cleanup await self._advance_steps(2) # Check if condition is met if len(self.state.effects) != 0: break await self.client.debug_kill_unit(self.units(UnitTypeId.REAPER)) # Wait for effectts to time out await self._advance_steps(100) logger.warning("Action test 06 successful.") async def test_botai_actions7(self): # Test ravager effects center = self.game_info.map_center while True: if self.units(UnitTypeId.RAVAGER).amount < 10: await self.client.debug_create_unit([(UnitTypeId.RAVAGER, 10, center, 1)]) for ravager in self.units(UnitTypeId.RAVAGER): ravager(AbilityId.EFFECT_CORROSIVEBILE, center) # logger.info(f"Effects: {self.state.effects}") for effect in self.state.effects: # logger.info(f"Effect: {effect}") if effect.id == EffectId.RAVAGERCORROSIVEBILECP: success = True await self._advance_steps(2) # Check if condition is met if len(self.state.effects) != 0: break # Cleanup await self.client.debug_kill_unit(self.units(UnitTypeId.RAVAGER)) # Wait for effectts to time out await self._advance_steps(100) logger.warning("Action test 07 successful.") async def test_botai_actions8(self): # Test if train function works on hatchery, lair, hive center = self.game_info.map_center if not self.structures(UnitTypeId.HIVE): await self.client.debug_create_unit([(UnitTypeId.HIVE, 1, center, 1)]) if not self.structures(UnitTypeId.LAIR): await self.client.debug_create_unit([(UnitTypeId.LAIR, 1, center, 1)]) if not self.structures(UnitTypeId.HATCHERY): await self.client.debug_create_unit([(UnitTypeId.HATCHERY, 1, center, 1)]) if not self.structures(UnitTypeId.SPAWNINGPOOL): await self.client.debug_create_unit([(UnitTypeId.SPAWNINGPOOL, 1, center, 1)]) while True: townhalls = self.structures.of_type({UnitTypeId.HIVE, UnitTypeId.LAIR, UnitTypeId.HATCHERY}) if townhalls.amount == 3 and self.minerals >= 450 and not self.already_pending(UnitTypeId.QUEEN): self.train(UnitTypeId.QUEEN, amount=3) # Equivalent to: # for townhall in townhalls: # townhall.train(UnitTypeId.QUEEN) await self._advance_steps(20) # Check if condition is met if self.already_pending(UnitTypeId.QUEEN) == 3: break # Cleanup townhalls = self.structures.of_type({UnitTypeId.HIVE, UnitTypeId.LAIR, UnitTypeId.HATCHERY}) queens = self.units(UnitTypeId.QUEEN) pool = self.structures(UnitTypeId.SPAWNINGPOOL) await self.client.debug_kill_unit(townhalls | queens | pool) await self._advance_steps(2) logger.warning("Action test 08 successful.") async def test_botai_actions9(self): # Morph an archon from 2 high templars center = self.game_info.map_center await self.client.debug_create_unit( [ (UnitTypeId.HIGHTEMPLAR, 1, center, 1), (UnitTypeId.DARKTEMPLAR, 1, center + Point2((5, 0)), 1), ] ) await self._advance_steps(4) assert self.already_pending(UnitTypeId.ARCHON) == 0 while True: for templar in self.units.of_type({UnitTypeId.HIGHTEMPLAR, UnitTypeId.DARKTEMPLAR}): templar(AbilityId.MORPH_ARCHON) await self._advance_steps(4) templars = self.units.of_type({UnitTypeId.HIGHTEMPLAR, UnitTypeId.DARKTEMPLAR}) archons = self.units(UnitTypeId.ARCHON) if templars.amount > 0: # High templars are on their way to morph ot morph has started assert self.already_pending(UnitTypeId.ARCHON) == 1 else: # Morph started assert self.already_pending(UnitTypeId.ARCHON) == archons.not_ready.amount # Check if condition is met if archons.ready.amount == 1: assert templars.amount == 0 assert self.already_pending(UnitTypeId.ARCHON) == 0 break # Cleanup if archons: await self.client.debug_kill_unit(archons) if templars: await self.client.debug_kill_unit(templars) await self._advance_steps(2) logger.warning("Action test 09 successful.") async def test_botai_actions10(self): # Morph 400 banelings from 400 lings in the same frame center = self.game_info.map_center target_amount = 400 while True: bane_nests = self.structures(UnitTypeId.BANELINGNEST) lings = self.units(UnitTypeId.ZERGLING) banes = self.units(UnitTypeId.BANELING) bane_cocoons = self.units(UnitTypeId.BANELINGCOCOON) # Cheat money, need 10k/10k to morph 400 lings to 400 banes if not banes and not bane_cocoons and (self.minerals < 10_000 or self.vespene < 10_000): await self.client.debug_all_resources() # Spawn units if not bane_nests: await self.client.debug_create_unit([(UnitTypeId.BANELINGNEST, 1, center, 1)]) current_amount = banes.amount + bane_cocoons.amount + lings.amount if current_amount < target_amount: await self.client.debug_create_unit([(UnitTypeId.ZERGLING, target_amount - current_amount, center, 1)]) if lings.amount >= target_amount and self.minerals >= 10_000 and self.vespene >= 10_000: for ling in lings: ling.train(UnitTypeId.BANELING) await self._advance_steps(20) # Check if condition is met bane_nests = self.structures(UnitTypeId.BANELINGNEST) lings = self.units(UnitTypeId.ZERGLING) banes = self.units(UnitTypeId.BANELING) bane_cocoons = self.units(UnitTypeId.BANELINGCOCOON) if banes.amount >= target_amount: break # Cleanup await self.client.debug_kill_unit(lings | banes | bane_nests | bane_cocoons) await self._advance_steps(2) logger.warning("Action test 10 successful.") async def test_botai_actions11(self): # Trigger anti armor missile of raven against enemy unit and check if buff was received await self.clean_up_center() await self.clean_up_center() map_center = self.game_info.map_center while not self.units(UnitTypeId.RAVEN): await self.client.debug_create_unit([(UnitTypeId.RAVEN, 1, map_center, 1)]) await self._advance_steps(2) while not self.enemy_units(UnitTypeId.INFESTOR): await self.client.debug_create_unit([(UnitTypeId.INFESTOR, 1, map_center, 2)]) await self._advance_steps(2) raven = self.units(UnitTypeId.RAVEN)[0] # Set raven energy to max await self.client.debug_set_unit_value(raven, 1, 200) await self._advance_steps(4) enemy = self.enemy_units(UnitTypeId.INFESTOR)[0] while True: raven = self.units(UnitTypeId.RAVEN)[0] raven(AbilityId.EFFECT_ANTIARMORMISSILE, enemy) await self._advance_steps(2) enemy = self.enemy_units(UnitTypeId.INFESTOR)[0] if enemy.buffs: # logger.info(enemy.buffs, enemy.buff_duration_remain, enemy.buff_duration_max) break logger.warning("Action test 11 successful.") await self.clean_up_center() async def test_botai_actions12(self): # Test if structures_without_construction_SCVs works after killing the scv # Wait till can afford depot while not self.can_afford(UnitTypeId.SUPPLYDEPOT): await self.client.debug_all_resources() await self._advance_steps(2) while True: # Once depot is under construction: debug kill scv -> advance simulation: should now match the test case if self.structures(UnitTypeId.SUPPLYDEPOT).not_ready.amount == 1: construction_scvs: Units = self.workers.filter(lambda worker: worker.is_constructing_scv) if construction_scvs: await self.client.debug_kill_unit(construction_scvs) await self._advance_steps(8) await self._advance_steps(8) # Test case assert not self.workers.filter(lambda worker: worker.is_constructing_scv) assert self.structures_without_construction_SCVs.amount >= 1 break if not self.already_pending(UnitTypeId.SUPPLYDEPOT): # Pick scv scv: Unit = self.workers.random # Pick location to build depot on placement_position: Point2 | None = await self.find_placement( UnitTypeId.SUPPLYDEPOT, near=self.townhalls.random.position ) if placement_position: scv.build(UnitTypeId.SUPPLYDEPOT, placement_position) await self._advance_steps(2) logger.warning("Action test 12 successful.") await self.clean_up_center() # TODO: # self.can_cast function # Test client.py debug functions # Test if events work (upgrade complete, unit complete, building complete, building started) # Test if functions with various combinations works (e.g. already_pending) # Test self.train function on: larva, hatchery + lair (queens), 2 barracks (2 marines), 2 nexus (probes) (best: every building) # Test unit range and (base attack damage) and other unit stats (e.g. acceleration, deceleration, movement speed (on, off creep), turn speed # Test if dicts are correct for unit_trained_from.py -> train all units once class EmptyBot(BotAI): async def on_start(self): if self.units: await self.client.debug_kill_unit(self.units) async def on_step(self, iteration: int): map_center = self.game_info.map_center enemies = self.enemy_units | self.enemy_structures if enemies: enemies = enemies.closer_than(20, map_center) if enemies: # If attacker is visible: move command to attacker but try to not attack for unit in self.units: unit.move(enemies.closest_to(unit).position) else: # If attacker is invisible: dont move for unit in self.units: unit.hold_position() def main(): run_game(maps.get("Acropolis"), [Bot(Race.Terran, TestBot()), Bot(Race.Zerg, EmptyBot())], realtime=False) if __name__ == "__main__": main() ================================================ FILE: test/battery_overcharge_bot.py ================================================ """ This bot tests if battery overcharge crashes the bot. """ from loguru import logger from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId from sc2.main import run_game from sc2.player import Bot, Computer class BatteryOverchargeBot(BotAI): async def on_start(self): """Spawn requires structures.""" await self.client.debug_create_unit( [ (UnitTypeId.PYLON, 1, self.start_location.towards(self.game_info.map_center, 5), 1), (UnitTypeId.SHIELDBATTERY, 1, self.start_location.towards(self.game_info.map_center, 5), 1), (UnitTypeId.CYBERNETICSCORE, 1, self.start_location.towards(self.game_info.map_center, 5), 1), ] ) async def on_step(self, iteration): if iteration > 10: # Cast battery overcharge nexi = self.structures(UnitTypeId.NEXUS) batteries = self.structures(UnitTypeId.SHIELDBATTERY) for nexus in nexi: for battery in batteries: if nexus.energy >= 50: nexus(AbilityId.BATTERYOVERCHARGE_BATTERYOVERCHARGE, battery) if iteration > 20: logger.warning("Success, bot did not crash. Exiting bot.") await self.client.leave() def main(): run_game( maps.get("AcropolisLE"), [Bot(Race.Protoss, BatteryOverchargeBot()), Computer(Race.Terran, Difficulty.Medium)], realtime=False, disable_fog=True, ) if __name__ == "__main__": main() ================================================ FILE: test/benchmark_array_creation.py ================================================ """ Testing what the fastest way is to create a 1D Array with 2 values """ import random import numpy as np x, y = random.uniform(0, 300), random.uniform(0, 300) def numpy_array(x, y): # Calculate distances between each of the points return np.array((x, y), dtype=float) def numpy_array_tuple(my_tuple): # Calculate distances between each of the points return np.array(my_tuple, dtype=float) def numpy_asarray(x, y): # Calculate distances between each of the points return np.asarray((x, y), dtype=float) def numpy_asarray_tuple(my_tuple): # Calculate distances between each of the points return np.asarray(my_tuple, dtype=float) def numpy_asanyarray(x, y): # Calculate distances between each of the points return np.asanyarray((x, y), dtype=float) def numpy_asanyarray_tuple(my_tuple): # Calculate distances between each of the points return np.asanyarray(my_tuple, dtype=float) def numpy_fromiter(x, y): # Calculate distances between each of the points return np.fromiter((x, y), dtype=float, count=2) def numpy_fromiter_tuple(my_tuple): # Calculate distances between each of the points return np.fromiter(my_tuple, dtype=float, count=2) def numpy_fromiter_np_float(x, y): # Calculate distances between each of the points return np.fromiter((x, y), dtype=float, count=2) def numpy_fromiter_np_float_tuple(my_tuple): # Calculate distances between each of the points return np.fromiter(my_tuple, dtype=float, count=2) def numpy_zeros(x, y): # Calculate distances between each of the points a = np.zeros(2, dtype=float) a[0] = x a[1] = y return a def numpy_ones(x, y): # Calculate distances between each of the points a = np.ones(2, dtype=float) a[0] = x a[1] = y return a numpy_array(x, y) correct_array = np.array([x, y]) def test_numpy_array(benchmark): result = benchmark(numpy_array, x, y) assert np.array_equal(result, correct_array) def test_numpy_array_tuple(benchmark): result = benchmark(numpy_array_tuple, (x, y)) assert np.array_equal(result, correct_array) def test_numpy_asarray(benchmark): result = benchmark(numpy_asarray, x, y) assert np.array_equal(result, correct_array) def test_numpy_asarray_tuple(benchmark): result = benchmark(numpy_asarray_tuple, (x, y)) assert np.array_equal(result, correct_array) def test_numpy_asanyarray(benchmark): result = benchmark(numpy_asanyarray, x, y) assert np.array_equal(result, correct_array) def test_numpy_asanyarray_tuple(benchmark): result = benchmark(numpy_asanyarray_tuple, (x, y)) assert np.array_equal(result, correct_array) def test_numpy_fromiter(benchmark): result = benchmark(numpy_fromiter, x, y) assert np.array_equal(result, correct_array) def test_numpy_fromiter_tuple(benchmark): result = benchmark(numpy_fromiter_tuple, (x, y)) assert np.array_equal(result, correct_array) def test_numpy_fromiter_np_float(benchmark): result = benchmark(numpy_fromiter_np_float, x, y) assert np.array_equal(result, correct_array) def test_numpy_fromiter_np_float_tuple(benchmark): result = benchmark(numpy_fromiter_np_float_tuple, (x, y)) assert np.array_equal(result, correct_array) def test_numpy_zeros(benchmark): result = benchmark(numpy_zeros, x, y) assert np.array_equal(result, correct_array) def test_numpy_ones(benchmark): result = benchmark(numpy_ones, x, y) assert np.array_equal(result, correct_array) # Run this file using # uv run pytest test/test_benchmark_array_creation.py --benchmark-compare ================================================ FILE: test/benchmark_bot_ai_init.py ================================================ from __future__ import annotations from typing import Any from test.test_pickled_data import MAPS, build_bot_object_from_pickle_data, load_map_pickle_data def _test_run_bot_ai_init_on_all_maps(pickle_data: list[tuple[Any, Any, Any]]): for data in pickle_data: build_bot_object_from_pickle_data(*data) def test_bench_bot_ai_init(benchmark): # Load pickle files outside of benchmark map_pickle_data: list[tuple[Any, Any, Any]] = [load_map_pickle_data(path) for path in MAPS] _result = benchmark(_test_run_bot_ai_init_on_all_maps, map_pickle_data) # Run this file using # uv run pytest test/benchmark_bot_ai_init.py --benchmark-compare --benchmark-min-rounds=5 ================================================ FILE: test/benchmark_distance_two_points.py ================================================ from __future__ import annotations import math import platform import random import numpy as np from scipy.spatial import distance as scipydistance # from numba import jit, njit, vectorize, float64, int64 from sc2.position import Point2 PYTHON_VERSION = platform.python_version_tuple() USING_PYTHON_3_8: bool = PYTHON_VERSION >= ("3", "8") def distance_to_python_raw(s, p): return ((s[0] - p[0]) ** 2 + (s[1] - p[1]) ** 2) ** 0.5 def distance_to_squared_python_raw(s, p): return (s[0] - p[0]) ** 2 + (s[1] - p[1]) ** 2 if USING_PYTHON_3_8: def distance_to_math_dist(s, p): return math.dist(s, p) def distance_to_math_hypot(s, p): return math.hypot((s[0] - p[0]), (s[1] - p[1])) def distance_scipy_euclidean(p1, p2) -> int | float: """Distance calculation using scipy""" dist = scipydistance.euclidean(p1, p2) # dist = distance.cdist(p1.T, p2.T, "euclidean") return dist def distance_numpy_linalg_norm(p1, p2): """Distance calculation using numpy""" return np.linalg.norm(p1 - p2) def distance_sum_squared_sqrt(p1, p2) -> int | float: """Distance calculation using numpy""" return np.sqrt(np.sum((p1 - p2) ** 2)) # pyrefly: ignore def distance_sum_squared(p1, p2) -> int | float: """Distance calculation using numpy""" return np.sum((p1 - p2) ** 2, axis=0) # @njit # def distance_python_raw_njit(p1: Point2, p2: Point2) -> int | float: # """ The built in Point2 distance function rewritten differently with njit, same structure as distance02 """ # return ((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) ** 0.5 # @njit # def distance_python_raw_square_njit(p1: Point2, p2: Point2) -> int | float: # """ The built in Point2 distance function rewritten differently with njit, same structure as distance02 """ # return (p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2 # @njit("float64(float64[:], float64[:])") # def distance_numpy_linalg_norm_njit(p1, p2): # """ Distance calculation using numpy + numba, same structure as distance12 """ # return np.linalg.norm(p1 - p2) # @njit("float64(float64[:], float64[:])") # def distance_numpy_square_sum_sqrt_njit(p1, p2) -> int | float: # """ Distance calculation using numpy + numba, same structure as distance13 """ # return np.sqrt(np.sum((p1 - p2) ** 2)) # @njit("float64(float64[:], float64[:])") # def distance_numpy_square_sum_njit(p1, p2) -> int | float: # """ Distance calculation using numpy + numba, same structure as distance13 """ # return np.sum((p1 - p2) ** 2, axis=0) # Points as Point2 object p1 = Point2((random.uniform(0, 300), random.uniform(0, 300))) p2 = Point2((random.uniform(0, 300), random.uniform(0, 300))) # Points as numpy array to get most accuracy if all points do not need to be converted before calculation p1_np = np.asarray(p1) p2_np = np.asarray(p2) # Correct result to ensure that in the functions the correct result is calculated correct_result = distance_to_math_hypot(p1, p2) def check_result(result1, result2, accuracy=1e-5): return abs(result1 - result2) <= accuracy if USING_PYTHON_3_8: def test_distance_to_math_dist(benchmark): result = benchmark(distance_to_math_dist, p1, p2) assert check_result(result, correct_result) def test_distance_to_math_hypot(benchmark): result = benchmark(distance_to_math_hypot, p1, p2) assert check_result(result, correct_result) def test_distance_to_python_raw(benchmark): result = benchmark(distance_to_python_raw, p1, p2) assert check_result(result, correct_result) def test_distance_to_squared_python_raw(benchmark): result = benchmark(distance_to_squared_python_raw, p1, p2) assert check_result(result, correct_result**2) def test_distance_scipy_euclidean(benchmark): result = benchmark(distance_scipy_euclidean, p1_np, p2_np) assert check_result(result, correct_result) def test_distance_numpy_linalg_norm(benchmark): result = benchmark(distance_numpy_linalg_norm, p1_np, p2_np) assert check_result(result, correct_result) def test_distance_sum_squared_sqrt(benchmark): result = benchmark(distance_sum_squared_sqrt, p1_np, p2_np) assert check_result(result, correct_result) def test_distance_sum_squared(benchmark): result = benchmark(distance_sum_squared, p1_np, p2_np) assert check_result(result, correct_result**2) # def test_distance_python_raw_njit(benchmark): # result = benchmark(distance_python_raw_njit, p1_np, p2_np) # assert check_result(result, correct_result) # def test_distance_python_raw_square_njit(benchmark): # result = benchmark(distance_python_raw_square_njit, p1_np, p2_np) # assert check_result(re`sult, correct_result ** 2) # # # def test_distance_numpy_linalg_norm_njit(benchmark): # result = benchmark(distance_numpy_linalg_norm_njit, p1_np, p2_np) # assert check_result(result, correct_result) # # # def test_distance_numpy_square_sum_sqrt_njit(benchmark): # result = benchmark(distance_numpy_square_sum_sqrt_njit, p1_np, p2_np) # assert check_result(result, correct_result) # # # def test_distance_numpy_square_sum_njit(benchmark): # result = benchmark(distance_numpy_square_sum_njit, p1_np, p2_np) # assert check_result(result, correct_result ** 2) # Run this file using # uv run pytest test/test_benchmark_distance_two_points.py --benchmark-compare ================================================ FILE: test/benchmark_distances_cdist.py ================================================ import random import numpy as np from scipy.spatial.distance import cdist, pdist def distance_matrix_scipy_cdist_braycurtis(ps): # Calculate distances between each of the points return cdist(ps, ps, "braycurtis") def distance_matrix_scipy_cdist_canberra(ps): # Calculate distances between each of the points return cdist(ps, ps, "canberra") def distance_matrix_scipy_cdist_chebyshev(ps): # Calculate distances between each of the points return cdist(ps, ps, "chebyshev") def distance_matrix_scipy_cdist_cityblock(ps): # Calculate distances between each of the points return cdist(ps, ps, "cityblock") def distance_matrix_scipy_cdist_correlation(ps): # Calculate distances between each of the points return cdist(ps, ps, "correlation") def distance_matrix_scipy_cdist_cosine(ps): # Calculate distances between each of the points return cdist(ps, ps, "cosine") def distance_matrix_scipy_cdist_hamming(ps): # Calculate distances between each of the points return cdist(ps, ps, "hamming") def distance_matrix_scipy_cdist_jaccard(ps): # Calculate distances between each of the points return cdist(ps, ps, "jaccard") def distance_matrix_scipy_cdist_jensenshannon(ps): # Calculate distances between each of the points return cdist(ps, ps, "jensenshannon") def distance_matrix_scipy_cdist_mahalanobis(ps): # Calculate distances between each of the points return cdist(ps, ps, "mahalanobis") def distance_matrix_scipy_cdist_matching(ps): # Calculate distances between each of the points return cdist(ps, ps, "matching") # pyrefly: ignore # def distance_matrix_scipy_cdist_minkowski(ps): # # Calculate distances between each of the points # return cdist(ps, ps, "minkowski") def distance_matrix_scipy_cdist_rogerstanimoto(ps): # Calculate distances between each of the points return cdist(ps, ps, "rogerstanimoto") def distance_matrix_scipy_cdist_russellrao(ps): # Calculate distances between each of the points return cdist(ps, ps, "russellrao") def distance_matrix_scipy_cdist_seuclidean(ps): # Calculate distances between each of the points return cdist(ps, ps, "seuclidean") def distance_matrix_scipy_cdist_sokalmichener(ps): # Calculate distances between each of the points return cdist(ps, ps, "sokalmichener") def distance_matrix_scipy_cdist_sokalsneath(ps): # Calculate distances between each of the points return cdist(ps, ps, "sokalsneath") # def distance_matrix_scipy_cdist_wminkowski(ps): # # Calculate distances between each of the points # return cdist(ps, ps, "wminkowski") def distance_matrix_scipy_cdist_yule(ps): # Calculate distances between each of the points return cdist(ps, ps, "yule") def distance_matrix_scipy_cdist(ps): # Calculate distances between each of the points return cdist(ps, ps, "euclidean") def distance_matrix_scipy_pdist(ps): # Calculate distances between each of the points return pdist(ps, "euclidean") def distance_matrix_scipy_cdist_squared(ps): # Calculate squared distances between each of the points return cdist(ps, ps, "sqeuclidean") def distance_matrix_scipy_pdist_squared(ps): # Calculate squared distances between each of the points return pdist(ps, "sqeuclidean") # Points as numpy arrays amount = 200 min_value = 0 max_value = 300 points = np.array( [np.array([random.uniform(min_value, max_value), random.uniform(min_value, max_value)]) for _ in range(amount)] ) def test_distance_matrix_scipy_cdist_braycurtis(benchmark): result = benchmark(distance_matrix_scipy_cdist_braycurtis, points) # assert check_result(result, correct_result) def test_distance_matrix_scipy_cdist_canberra(benchmark): result = benchmark(distance_matrix_scipy_cdist_canberra, points) # assert check_result(result, correct_result) def test_distance_matrix_scipy_cdist_chebyshev(benchmark): result = benchmark(distance_matrix_scipy_cdist_chebyshev, points) # assert check_result(result, correct_result) def test_distance_matrix_scipy_cdist_cityblock(benchmark): result = benchmark(distance_matrix_scipy_cdist_cityblock, points) # assert check_result(result, correct_result) def test_distance_matrix_scipy_cdist_correlation(benchmark): result = benchmark(distance_matrix_scipy_cdist_correlation, points) # assert check_result(result, correct_result) def test_distance_matrix_scipy_cdist_cosine(benchmark): result = benchmark(distance_matrix_scipy_cdist_cosine, points) # assert check_result(result, correct_result) def test_distance_matrix_scipy_cdist_hamming(benchmark): result = benchmark(distance_matrix_scipy_cdist_hamming, points) # assert check_result(result, correct_result) def test_distance_matrix_scipy_cdist_jaccard(benchmark): result = benchmark(distance_matrix_scipy_cdist_jaccard, points) # assert check_result(result, correct_result) def test_distance_matrix_scipy_cdist_jensenshannon(benchmark): result = benchmark(distance_matrix_scipy_cdist_jensenshannon, points) # assert check_result(result, correct_result) def test_distance_matrix_scipy_cdist_mahalanobis(benchmark): result = benchmark(distance_matrix_scipy_cdist_mahalanobis, points) # assert check_result(result, correct_result) def test_distance_matrix_scipy_cdist_matching(benchmark): result = benchmark(distance_matrix_scipy_cdist_matching, points) # assert check_result(result, correct_result) # def test_distance_matrix_scipy_cdist_minkowski(benchmark): # result = benchmark(distance_matrix_scipy_cdist_minkowski, points) # # assert check_result(result, correct_result) def test_distance_matrix_scipy_cdist_rogerstanimoto(benchmark): result = benchmark(distance_matrix_scipy_cdist_rogerstanimoto, points) # assert check_result(result, correct_result) def test_distance_matrix_scipy_cdist_russellrao(benchmark): result = benchmark(distance_matrix_scipy_cdist_russellrao, points) # assert check_result(result, correct_result) def test_distance_matrix_scipy_cdist_seuclidean(benchmark): result = benchmark(distance_matrix_scipy_cdist_seuclidean, points) # assert check_result(result, correct_result) def test_distance_matrix_scipy_cdist_sokalmichener(benchmark): result = benchmark(distance_matrix_scipy_cdist_sokalmichener, points) # assert check_result(result, correct_result) def test_distance_matrix_scipy_cdist_sokalsneath(benchmark): result = benchmark(distance_matrix_scipy_cdist_sokalsneath, points) # assert check_result(result, correct_result) # def test_distance_matrix_scipy_cdist_wminkowski(benchmark): # result = benchmark(distance_matrix_scipy_cdist_wminkowski, points) # # assert check_result(result, correct_result) def test_distance_matrix_scipy_cdist_yule(benchmark): result = benchmark(distance_matrix_scipy_cdist_yule, points) # assert check_result(result, correct_result) def test_distance_matrix_scipy_cdist(benchmark): result = benchmark(distance_matrix_scipy_cdist, points) # assert check_result(result, correct_result) def test_distance_matrix_scipy_pdist(benchmark): result = benchmark(distance_matrix_scipy_pdist, points) # assert check_result(result, correct_result) def test_distance_matrix_scipy_cdist_squared(benchmark): result = benchmark(distance_matrix_scipy_cdist_squared, points) # assert check_result(result, correct_result) def test_distance_matrix_scipy_pdist_squared(benchmark): result = benchmark(distance_matrix_scipy_pdist_squared, points) # assert check_result(result, correct_result) # Run this file using # uv run pytest test/test_benchmark_distances_cdist.py --benchmark-compare ================================================ FILE: test/benchmark_distances_points_to_point.py ================================================ from __future__ import annotations import math import random import numpy as np from scipy.spatial.distance import cdist def distance_matrix_scipy_cdist_squared(ps, p1): # Calculate squared distances between multiple points and target point flat_units = (item for sublist in ps for item in sublist) units_np = np.fromiter(flat_units, dtype=float, count=2 * len(ps)).reshape((-1, 2)) point_np = np.fromiter(p1, dtype=float, count=2).reshape((-1, 2)) return cdist(units_np, point_np, "sqeuclidean") def distance_numpy_basic_1(ps, p1): """Distance calculation using numpy""" flat_units = (item for sublist in ps for item in sublist) units_np = np.fromiter(flat_units, dtype=float, count=2 * len(ps)).reshape((-1, 2)) point_np = np.fromiter(p1, dtype=float, count=2).reshape((-1, 2)) # Subtract and then square the values nppoints = (units_np - point_np) ** 2 # Calc the sum of each vector nppoints = nppoints.sum(axis=1) return nppoints def distance_numpy_basic_2(ps, p1): """Distance calculation using numpy""" flat_units = (item for sublist in ps for item in sublist) units_np = np.fromiter(flat_units, dtype=float, count=2 * len(ps)).reshape((-1, 2)) point_np = np.fromiter(p1, dtype=float, count=2).reshape((-1, 2)) dist_2 = np.sum((units_np - point_np) ** 2, axis=1) return dist_2 def distance_numpy_einsum(ps, p1): """Distance calculation using numpy einstein sum""" flat_units = (item for sublist in ps for item in sublist) units_np = np.fromiter(flat_units, dtype=float, count=2 * len(ps)).reshape((-1, 2)) point_np = np.fromiter(p1, dtype=float, count=2).reshape((-1, 2)) deltas = units_np - point_np dist_2 = np.einsum("ij,ij->i", deltas, deltas) return dist_2 def distance_numpy_einsum_pre_converted(ps, p1): """Distance calculation using numpy einstein sum""" deltas = ps - p1 dist_2 = np.einsum("ij,ij->i", deltas, deltas) return dist_2 # @njit("float64[:](float64[:, :], float64[:, :])") # def distance_numpy_basic_1_numba(ps, p1): # """ Distance calculation using numpy with njit """ # # Subtract and then square the values # nppoints = (ps - p1) ** 2 # # Calc the sum of each vector # nppoints = nppoints.sum(axis=1) # return nppoints # @njit("float64[:](float64[:, :], float64[:, :])") # def distance_numpy_basic_2_numba(ps, p1): # """ Distance calculation using numpy with njit """ # distances = np.sum((ps - p1) ** 2, axis=1) # return distances # # @njit("float64[:](float64[:], float64[:])") # @jit(nopython=True) # def distance_numba(ps, p1, amount): # """ Distance calculation using numpy with jit(nopython=True) """ # distances = [] # x1 = p1[0] # y1 = p1[1] # for index in range(amount): # x0 = ps[2 * index] # y0 = ps[2 * index + 1] # distance_squared = (x0 - x1) ** 2 + (y0 - y1) ** 2 # distances.append(distance_squared) # return distances def distance_pure_python(ps, p1): """Distance calculation using numpy with jit(nopython=True)""" distances = [] x1 = p1[0] y1 = p1[1] for x0, y0 in ps: distance_squared = (x0 - x1) ** 2 + (y0 - y1) ** 2 distances.append(distance_squared) return distances def distance_math_hypot(ps, p1): """Distance calculation using math.hypot""" distances = [] x1 = p1[0] y1 = p1[1] # for x0, y0 in ps: # distance = math.hypot(x0 - x1, y0 - y1) # distances.append(distance) # return distances return [math.hypot(x0 - x1, y0 - y1) for x0, y0 in ps] # Points as numpy arrays amount = 50 min_value = 0 max_value = 250 point: tuple[float, float] = (random.uniform(min_value, max_value), random.uniform(min_value, max_value)) units: list[tuple[float, float]] = [ (random.uniform(min_value, max_value), random.uniform(min_value, max_value)) for _ in range(amount) ] # Pre convert points to numpy array flat_units = [item for sublist in units for item in sublist] units_np = np.fromiter(flat_units, dtype=float, count=2 * len(units)).reshape((-1, 2)) point_np = np.fromiter(point, dtype=float, count=2).reshape((-1, 2)) r1 = distance_matrix_scipy_cdist_squared(units, point).flatten() r2 = distance_numpy_basic_1(units, point) r3 = distance_numpy_basic_2(units, point) r4 = distance_numpy_einsum(units, point) assert np.array_equal(r1, r2) assert np.array_equal(r1, r3) assert np.array_equal(r1, r4) def test_distance_matrix_scipy_cdist_squared(benchmark): result = benchmark(distance_matrix_scipy_cdist_squared, units, point) def test_distance_numpy_basic_1(benchmark): result = benchmark(distance_numpy_basic_1, units, point) def test_distance_numpy_basic_2(benchmark): result = benchmark(distance_numpy_basic_2, units, point) def test_distance_numpy_einsum(benchmark): result = benchmark(distance_numpy_einsum, units, point) def test_distance_numpy_einsum_pre_converted(benchmark): result = benchmark(distance_numpy_einsum_pre_converted, units_np, point_np) # def test_distance_numpy_basic_1_numba(benchmark): # result = benchmark(distance_numpy_basic_1_numba, units_np, point_np) # def test_distance_numpy_basic_2_numba(benchmark): # result = benchmark(distance_numpy_basic_2_numba, units_np, point_np) # def test_distance_numba(benchmark): # result = benchmark(distance_numba, flat_units, point, len(flat_units) // 2) def test_distance_pure_python(benchmark): result = benchmark(distance_pure_python, units, point) def test_distance_math_hypot(benchmark): result = benchmark(distance_math_hypot, units, point) # Run this file using # uv run pytest test/test_benchmark_distances_points_to_point.py --benchmark-compare ================================================ FILE: test/benchmark_distances_units.py ================================================ import math import random import numpy as np from scipy.spatial.distance import cdist, pdist def distance_matrix_scipy_cdist(ps): # Calculate distances between each of the points return cdist(ps, ps, "euclidean") def distance_matrix_scipy_pdist(ps): # Calculate distances between each of the points return pdist(ps, "euclidean") def distance_matrix_scipy_cdist_squared(ps): # Calculate squared distances between each of the points return cdist(ps, ps, "sqeuclidean") def distance_matrix_scipy_pdist_squared(ps): # Calculate squared distances between each of the points return pdist(ps, "sqeuclidean") # Points as numpy arrays amount = 200 min_value = 0 max_value = 250 points = np.array( [np.array([random.uniform(min_value, max_value), random.uniform(min_value, max_value)]) for _ in range(amount)] ) m1 = distance_matrix_scipy_cdist(points) m2 = distance_matrix_scipy_pdist(points) ms1 = distance_matrix_scipy_cdist_squared(points) ms2 = distance_matrix_scipy_pdist_squared(points) def calc_row_idx(k, n): return int(math.ceil((1 / 2.0) * (-((-8 * k + 4 * n**2 - 4 * n - 7) ** 0.5) + 2 * n - 1) - 1)) def elem_in_i_rows(i, n): return i * (n - 1 - i) + (i * (i + 1)) // 2 def calc_col_idx(k, i, n): return int(n - elem_in_i_rows(i + 1, n) + k) def condensed_to_square(k, n): i = calc_row_idx(k, n) j = calc_col_idx(k, i, n) return i, j def square_to_condensed(i, j, amount): # Converts indices of a square matrix to condensed matrix # 'amount' is the number of points that were used to calculate the distances # https://stackoverflow.com/a/36867493/10882657 assert i != j, "No diagonal elements in condensed matrix! Diagonal elements are zero" if i < j: i, j = j, i return amount * j - j * (j + 1) // 2 + i - 1 - j # Test if distance in cdist is same as in pdist, and that the indices function is correct indices = set() for i1 in range(amount): for i2 in range(amount): if i1 == i2: # Diagonal entries are zero continue # m1: cdist square matrix v1 = m1[i1, i2] # m2: pdist condensed matrix vector index = square_to_condensed(i1, i2, amount) indices.add(index) v2 = m2[index] # Test if convert indices functions work j1, j2 = condensed_to_square(index, amount) # Swap if first is bigger than 2nd assert j1 == i1 and j2 == i2 or j2 == i1 and j2 == i1, f"{j1} == {i1} and {j2} == {i2}" # Assert if the values of cdist is the same as the value of pdist assert v1 == v2, f"m1[i1, i2] is {v1}, m2[index] is {v2}" # Test that all indices were generated using the for loop above assert max(indices) == len(m2) - 1 assert min(indices) == 0 assert len(indices) == len(m2), f"{len(indices)} == {len(m2)}" def test_distance_matrix_scipy_cdist(benchmark): _result = benchmark(distance_matrix_scipy_cdist, points) # assert check_result(result, correct_result) def test_distance_matrix_scipy_pdist(benchmark): _result = benchmark(distance_matrix_scipy_pdist, points) # assert check_result(result, correct_result) def test_distance_matrix_scipy_cdist_squared(benchmark): _result = benchmark(distance_matrix_scipy_cdist_squared, points) # assert check_result(result, correct_result) def test_distance_matrix_scipy_pdist_squared(benchmark): _result = benchmark(distance_matrix_scipy_pdist_squared, points) # assert check_result(result, correct_result) # Run this file using # uv run pytest test/test_benchmark_distances_units.py --benchmark-compare ================================================ FILE: test/benchmark_prepare_units.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from test.test_pickled_data import MAPS, get_map_specific_bot if TYPE_CHECKING: from sc2.bot_ai import BotAI def _run_prepare_units(bot_objects: list[BotAI]): for bot_object in bot_objects: bot_object._prepare_units() def test_bench_prepare_units(benchmark): bot_objects = [get_map_specific_bot(map_) for map_ in MAPS] _result = benchmark(_run_prepare_units, bot_objects) # Run this file using # uv run pytest test/benchmark_prepare_units.py --benchmark-compare ================================================ FILE: test/damagetest_bot.py ================================================ from __future__ import annotations import math from loguru import logger from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Race from sc2.ids.unit_typeid import UnitTypeId from sc2.main import run_game from sc2.player import Bot from sc2.unit import Unit class TestBot(BotAI): def __init__(self): # The time the bot has to complete all tests, here: the number of game seconds self.game_time_timeout_limit = 20 * 60 # 20 minutes ingame time # Check how many test action functions we have # At least 4 tests because we test properties and variables self.action_tests = [ getattr(self, f"test_botai_actions{index}") for index in range(4000) if hasattr(getattr(self, f"test_botai_actions{index}", 0), "__call__") ] self.tests_target = 4 self.tests_done_by_name = set() # Keep track of the action index and when the last action was started self.current_action_index = 1 self.iteration_last_action_started = 8 # There will be 20 iterations of the bot doing nothing between tests self.iteration_wait_time_between_actions = 20 self.scv_action_list = ["move", "patrol", "attack", "hold", "scan_move"] # Variables for test_botai_actions11 async def on_start(self): self.client.game_step = 8 # await self.client.quick_save() await self.distribute_workers() async def on_step(self, iteration): # Test actions if iteration == 7: for action_test in self.action_tests: await action_test() # Exit bot if iteration > 100: logger.info(f"Tests completed after {round(self.time, 1)} seconds") exit(0) async def clean_up_center(self): map_center = self.game_info.map_center # Remove everything close to map center my_units = self.units | self.structures if my_units: my_units = my_units.closer_than(20, map_center) if my_units: await self.client.debug_kill_unit(my_units) enemy_units = self.enemy_units | self.enemy_structures if enemy_units: enemy_units = enemy_units.closer_than(20, map_center) if enemy_units: await self.client.debug_kill_unit(enemy_units) await self._advance_steps(2) # Create a lot of units and check if their damage calculation is correct based on Unit.calculate_damage_vs_target() async def test_botai_actions1001(self): upgrade_levels = {0, 1} attacker_units = [ # # Protoss # UnitTypeId.PROBE, # UnitTypeId.ZEALOT, UnitTypeId.ADEPT, UnitTypeId.STALKER, UnitTypeId.HIGHTEMPLAR, UnitTypeId.DARKTEMPLAR, UnitTypeId.ARCHON, # Doesnt work vs workers when attacklevel > 1 UnitTypeId.IMMORTAL, UnitTypeId.COLOSSUS, UnitTypeId.PHOENIX, UnitTypeId.VOIDRAY, # UnitTypeId.CARRIER, # TODO UnitTypeId.MOTHERSHIP, UnitTypeId.TEMPEST, # # Terran # UnitTypeId.SCV, UnitTypeId.MARINE, UnitTypeId.MARAUDER, UnitTypeId.GHOST, UnitTypeId.HELLION, # UnitTypeId.HELLIONTANK, # Incorrect for light targets because hellbat does not seem to have another weapon vs light specifically in the API # UnitTypeId.CYCLONE, # Seems to lock on as soon as it spawns UnitTypeId.SIEGETANK, UnitTypeId.THOR, # UnitTypeId.THORAP, # TODO uncomment when new version for linux client is released UnitTypeId.BANSHEE, UnitTypeId.VIKINGFIGHTER, UnitTypeId.VIKINGASSAULT, # UnitTypeId.BATTLECRUISER, # Does not work because weapon_cooldown is not displayed in the API # # Zerg # UnitTypeId.DRONE, UnitTypeId.ZERGLING, # UnitTypeId.BANELING, # TODO UnitTypeId.QUEEN, # UnitTypeId.ROACH, # Has bugs that I don't know how to fix UnitTypeId.RAVAGER, # UnitTypeId.HYDRALISK, # TODO # UnitTypeId.LURKERMPBURROWED, # Somehow fails the test # UnitTypeId.MUTALISK, # Mutalisk is supposed to deal 9-3-1 damage, but it seems it deals 12 damage if there is no nearby 2nd target UnitTypeId.CORRUPTOR, # UnitTypeId.BROODLORD, # Was unreliable because the broodlings would also attack UnitTypeId.ULTRALISK, # Buildings UnitTypeId.MISSILETURRET, UnitTypeId.SPINECRAWLER, UnitTypeId.SPORECRAWLER, UnitTypeId.PLANETARYFORTRESS, ] defender_units = [ # Ideally one of each type: ground and air unit with each armor tage # Ground, no tag UnitTypeId.RAVAGER, # Ground, light UnitTypeId.MULE, # Ground, armored UnitTypeId.MARAUDER, # Ground, biological UnitTypeId.ROACH, # Ground, psionic UnitTypeId.HIGHTEMPLAR, # Ground, mechanical UnitTypeId.STALKER, # Ground, massive # UnitTypeId.ULTRALISK, # Fails vs our zergling # Ground, structure # UnitTypeId.PYLON, # Pylon seems to regenerate 1 shield for no reason UnitTypeId.SUPPLYDEPOT, UnitTypeId.BUNKER, UnitTypeId.MISSILETURRET, # Air, light UnitTypeId.PHOENIX, # Air, armored UnitTypeId.VOIDRAY, # Air, biological UnitTypeId.CORRUPTOR, # Air, psionic UnitTypeId.VIPER, # Air, mechanical UnitTypeId.MEDIVAC, # Air, massive UnitTypeId.BATTLECRUISER, # Air, structure UnitTypeId.BARRACKSFLYING, # Ground and air UnitTypeId.COLOSSUS, ] await self._advance_steps(20) map_center = self.game_info.map_center # Show whole map await self.client.debug_show_map() def get_attacker_and_defender(): my_units = self.units | self.structures enemy_units = self.enemy_units | self.enemy_structures if not my_units or not enemy_units: # logger.info("my units:", my_units) # logger.info("enemy units:",enemy_units) return None, None attacker: Unit = my_units.closest_to(map_center) defender: Unit = enemy_units.closest_to(map_center) return attacker, defender def do_some_unit_property_tests(attacker: Unit, defender: Unit): """Some tests that are not covered by test_pickled_data.py""" # TODO move unit unrelated tests elsewhere self.step_time self.units_created self.structure_type_build_progress(attacker.type_id) self.structure_type_build_progress(defender.type_id) self.tech_requirement_progress(attacker.type_id) self.tech_requirement_progress(defender.type_id) self.in_map_bounds(attacker.position) self.in_map_bounds(defender.position) self.get_terrain_z_height(attacker.position) self.get_terrain_z_height(defender.position) for unit in [attacker, defender]: unit.shield_percentage unit.shield_health_percentage unit.energy_percentage unit.age_in_frames unit.age unit.is_memory unit.is_snapshot unit.cloak unit.is_revealed unit.can_be_attacked unit.buff_duration_remain unit.buff_duration_max unit.order_target unit.is_transforming unit.has_techlab unit.has_reactor unit.add_on_position unit.health_percentage unit.bonus_damage unit.air_dps attacker.target_in_range(defender) defender.target_in_range(attacker) attacker.calculate_dps_vs_target(defender) defender.calculate_dps_vs_target(attacker) attacker.is_facing(defender) defender.is_facing(attacker) attacker == defender defender == attacker await self.clean_up_center() for upgrade_level in upgrade_levels: if upgrade_level != 0: await self.client.debug_upgrade() for attacker_type in attacker_units: for defender_type in defender_units: # DT, Thor, Tempest one-shots workers, so skip test if attacker_type in { UnitTypeId.DARKTEMPLAR, UnitTypeId.TEMPEST, UnitTypeId.THOR, UnitTypeId.THORAP, UnitTypeId.LIBERATORAG, UnitTypeId.PLANETARYFORTRESS, UnitTypeId.ARCHON, } and defender_type in {UnitTypeId.PROBE, UnitTypeId.DRONE, UnitTypeId.SCV, UnitTypeId.MULE}: continue # Spawn units await self.client.debug_create_unit( [(attacker_type, 1, map_center, 1), (defender_type, 1, map_center, 2)] ) await self._advance_steps(1) # Wait for units to spawn attacker, defender = get_attacker_and_defender() while ( attacker is None or defender is None or attacker.type_id != attacker_type or defender.type_id != defender_type ): await self._advance_steps(1) attacker, defender = get_attacker_and_defender() # TODO check if shield calculation is correct by setting shield of enemy unit # logger.info(f"Attacker: {attacker}, defender: {defender}") do_some_unit_property_tests(attacker, defender) # Units have spawned, calculate expected damage expected_damage: float = attacker.calculate_damage_vs_target(defender)[0] # If expected damage is zero, it means that the attacker cannot attack the defender: skip test if expected_damage == 0: await self.clean_up_center() continue # Thor antiground seems buggy sometimes and not reliable in tests, skip it if attacker_type in {UnitTypeId.THOR, UnitTypeId.THORAP} and not defender.is_flying: await self.clean_up_center() continue real_damage = 0 # Limit the while loop max_steps = 100 while ( attacker.weapon_cooldown == 0 or attacker.weapon_cooldown > 3 ) and real_damage < expected_damage: if attacker_type in {UnitTypeId.PROBE, UnitTypeId.SCV, UnitTypeId.DRONE}: attacker.attack(defender) await self._advance_steps(1) # Unsure why I have to recalculate this here again but it prevents a bug attacker, defender = get_attacker_and_defender() # pyrefly: ignore expected_damage: float = max(expected_damage, attacker.calculate_damage_vs_target(defender)[0]) real_damage = math.ceil( # pyrefly: ignore defender.health_max + defender.shield_max - defender.health - defender.shield ) # logger.info( # f"Attacker type: {attacker_type}, defender health: {defender.health} / {defender.health_max}, defender shield: {defender.shield} / {defender.shield_max}, expected damage: {expected_damage}, real damage so far: {real_damage}, attacker weapon cooldown: {attacker.weapon_cooldown}" # ) max_steps -= 1 assert max_steps > 0, ( f"Step limit reached. Test timed out for attacker {attacker_type} and defender {defender_type}" ) assert expected_damage == real_damage, ( # pyrefly: ignore f"Expected damage does not match real damage: Unit type {attacker_type} (attack upgrade: {attacker.attack_upgrade_level}) deals {real_damage} damage against {defender_type} (armor upgrade: {defender.armor_upgrade_level} and shield upgrade: {defender.shield_upgrade_level}) but calculated damage was {expected_damage}, attacker weapons: \n{attacker._weapons}" ) await self.clean_up_center() # Hide map again await self.client.debug_show_map() await self._advance_steps(2) logger.warning("Action test 1001 successful.") class EmptyBot(BotAI): async def on_start(self): if self.units: await self.client.debug_kill_unit(self.units) async def on_step(self, iteration: int): map_center = self.game_info.map_center enemies = self.enemy_units | self.enemy_structures if enemies: enemies = enemies.closer_than(20, map_center) if enemies: # If attacker is visible: move command to attacker but try to not attack for unit in self.units: unit.move(enemies.closest_to(unit).position) else: # If attacker is invisible: dont move for unit in self.units: unit.hold_position() def main(): run_game(maps.get("Empty128"), [Bot(Race.Terran, TestBot()), Bot(Race.Zerg, EmptyBot())], realtime=False) if __name__ == "__main__": main() ================================================ FILE: test/generate_pickle_files_bot.py ================================================ """ This "bot" will loop over several available ladder maps and generate the pickle file in the "/test/pickle_data/" subfolder. These will then be used to run tests from the test script "test_pickled_data.py" """ import lzma import pickle from pathlib import Path from loguru import logger from s2clientprotocol import sc2api_pb2 as sc_pb from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race from sc2.game_data import GameData from sc2.game_info import GameInfo from sc2.game_state import GameState from sc2.ids.unit_typeid import UnitTypeId from sc2.main import run_game from sc2.player import Bot, Computer from sc2.protocol import ProtocolError class ExporterBot(BotAI): def __init__(self): BotAI.__init__(self) self.map_name: str = None # pyrefly: ignore async def on_step(self, iteration): pass def get_pickle_file_path(self) -> Path: folder_path = Path(__file__).parent subfolder_name = "pickle_data" file_name = f"{self.map_name}.xz" file_path = folder_path / subfolder_name / file_name return file_path def get_combat_file_path(self) -> Path: folder_path = Path(__file__).parent subfolder_name = "combat_data" file_name = f"{self.map_name}.xz" file_path = folder_path / subfolder_name / file_name return file_path async def store_data_to_file(self, file_path: Path): # Grab all raw data from observation raw_game_data = await self.client._execute( data=sc_pb.RequestData(ability_id=True, unit_type_id=True, upgrade_id=True, buff_id=True, effect_id=True) ) raw_game_info = await self.client._execute(game_info=sc_pb.RequestGameInfo()) raw_observation = self.state.response_observation # To test if this data is convertable in the first place _game_data = GameData(raw_game_data.data) _game_info = GameInfo(raw_game_info.game_info) _game_state = GameState(raw_observation) Path(file_path).parent.mkdir(exist_ok=True, parents=True) with lzma.open(file_path, "wb") as f: pickle.dump([raw_game_data, raw_game_info, raw_observation], f) async def on_start(self): file_path = self.get_pickle_file_path() logger.info(f"Saving pickle file to {self.map_name}.xz") await self.store_data_to_file(file_path) # Make map visible await self.client.debug_show_map() await self.client.debug_control_enemy() await self.client.debug_god() # Spawn one of each unit valid_units: set[UnitTypeId] = { UnitTypeId(unit_id) for unit_id, data in self.game_data.units.items() if data._proto.race != Race.NoRace and data._proto.race != Race.Random and data._proto.available # Dont cloak units and UnitTypeId(unit_id) != UnitTypeId.MOTHERSHIP and (data._proto.mineral_cost or data._proto.movement_speed or data._proto.weapons) } # Create units for self await self.client.debug_create_unit([(valid_unit, 1, self.start_location, 1) for valid_unit in valid_units]) # Create units for enemy await self.client.debug_create_unit( [(valid_unit, 1, self.enemy_start_locations[0], 2) for valid_unit in valid_units] ) await self._advance_steps(2) file_path = self.get_combat_file_path() await self.store_data_to_file(file_path) await self.client.leave() def main(): maps_ = [ "16-BitLE", "2000AtmospheresAIE", "AbiogenesisLE", "AbyssalReefLE", "AcidPlantLE", "AcolyteLE", "AcropolisLE", "AncientCisternAIE", "Artana", "AscensiontoAiurLE", "AutomatonLE", "BackwaterLE", "Bandwidth", "BattleontheBoardwalkLE", "BelShirVestigeLE", "BerlingradAIE", "BlackburnAIE", "BlackpinkLE", "BlueshiftLE", "CactusValleyLE", "CatalystLE", "CeruleanFallLE", "CrystalCavern", "CuriousMindsAIE", "CyberForestLE", "DarknessSanctuaryLE", "DeathAura506", "DeathAuraLE", "DefendersLandingLE", "DigitalFrontier", "DiscoBloodbathLE", "DragonScalesAIE", "DreamcatcherLE", "EastwatchLE", "Ephemeron", "EphemeronLE", "EternalEmpire506", "EternalEmpireLE", "EverDream506", "EverDreamLE", "FractureLE", "FrostLE", "GlitteringAshesAIE", "GoldenauraAIE", "GoldenWall506", "GoldenWallLE", "GresvanAIE", "HardwireAIE", "HonorgroundsLE", "IceandChrome506", "IceandChromeLE", "InfestationStationAIE", "InsideAndOutAIE", "InterloperLE", "JagannathaAIE", "KairosJunctionLE", "KingsCoveLE", "LostandFoundLE", "LightshadeAIE", "MechDepotLE", "MoondanceAIE", "NeonVioletSquareLE", "NewkirkPrecinctTE", "NewRepugnancyLE", "NightshadeLE", "OdysseyLE", "OldSunshine", "OxideAIE", "PaladinoTerminalLE", "ParaSiteLE", "PersephoneAIE", "PillarsofGold506", "PillarsofGoldLE", "PortAleksanderLE", "PrimusQ9", "ProximaStationLE", "PylonAIE", "RedshiftLE", "Reminiscence", "RomanticideAIE", "RoyalBloodAIE", "Sanglune", "SequencerLE", "SimulacrumLE", "Submarine506", "SubmarineLE", "StargazersAIE", "StasisLE", "TheTimelessVoid", "ThunderbirdLE", "TorchesAIE", "Treachery", "Triton", "Urzagol", "WaterfallAIE", "WintersGateLE", "WorldofSleepersLE", "YearZeroLE", "ZenLE", "Equilibrium513AIE", "GoldenAura513AIE", "HardLead513AIE", "Oceanborn513AIE", "SiteDelta513AIE", "Gresvan513AIE", ] for map_ in maps_: try: bot = ExporterBot() bot.map_name = map_ file_path = bot.get_pickle_file_path() if Path(file_path).is_file(): logger.warning( f"Pickle file for map {map_} was already generated. Skipping. If you wish to re-generate files, please remove them first." ) continue logger.info(f"Creating pickle file for map {map_} ...") run_game(maps.get(map_), [Bot(Race.Terran, bot), Computer(Race.Zerg, Difficulty.Easy)], realtime=False) except ProtocolError: # ProtocolError appears after a leave game request pass except Exception as e: logger.exception(f"Caught unknown exception: {e}") logger.error( f"Map {map_} could not be found, so pickle files for that map could not be generated. Error: {e}" ) if __name__ == "__main__": main() ================================================ FILE: test/queries_test_bot.py ================================================ """ This testbot's purpose is to test the query behavior of the API. These query functions are: self.can_place (RequestQueryBuildingPlacement) TODO: self.client.query_pathing (RequestQueryPathing) """ from __future__ import annotations import sys from loguru import logger from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Race from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId from sc2.main import run_game from sc2.player import Bot from sc2.position import Point2 class TestBot(BotAI): def __init__(self): # The time the bot has to complete all tests, here: the number of game seconds self.game_time_timeout_limit = 20 * 60 # 20 minutes ingame time async def on_start(self): self.client.game_step = 16 async def on_step(self, iteration): if iteration <= 7: return await self.clear_map_center() await self.test_can_place_expect_true() await self.test_can_place_expect_false() await self.test_rally_points_with_rally_ability() await self.test_rally_points_with_smart_ability() # await self.client.leave() sys.exit(0) async def clear_map_center(self): """Spawn observer in map center, remove all enemy units, remove all own units.""" map_center = self.game_info.map_center # Spawn observer to be able to see enemy invisible units await self.client.debug_create_unit([(UnitTypeId.OBSERVER, 1, map_center, 1)]) await self._advance_steps(10) # Remove everything close to map center enemy_units = self.enemy_units | self.enemy_structures if enemy_units: await self.client.debug_kill_unit(enemy_units) await self._advance_steps(10) neutral_units = self.resources if neutral_units: await self.client.debug_kill_unit(neutral_units) await self._advance_steps(10) my_units = self.units | self.structures if my_units: await self.client.debug_kill_unit(my_units) await self._advance_steps(10) async def spawn_unit(self, unit_type: UnitTypeId | list[UnitTypeId]): await self._advance_steps(10) if not isinstance(unit_type, list): unit_type = [unit_type] for i in unit_type: await self.client.debug_create_unit([(i, 1, self.game_info.map_center, 1)]) async def spawn_unit_enemy(self, unit_type: UnitTypeId | list[UnitTypeId]): await self._advance_steps(10) if not isinstance(unit_type, list): unit_type = [unit_type] for i in unit_type: if i == UnitTypeId.CREEPTUMOR: await self.client.debug_create_unit([(i, 1, self.game_info.map_center + Point2((5, 5)), 2)]) else: await self.client.debug_create_unit([(i, 1, self.game_info.map_center, 2)]) async def run_can_place(self) -> bool: result = await self.can_place(AbilityId.TERRANBUILD_COMMANDCENTER, [self.game_info.map_center]) return result[0] async def run_can_place_single(self) -> bool: result = await self.can_place_single(AbilityId.TERRANBUILD_COMMANDCENTER, self.game_info.map_center) return result async def test_can_place_expect_true(self): test_cases = [ # Invisible undetected enemy units [UnitTypeId.OVERLORD, UnitTypeId.DARKTEMPLAR], [UnitTypeId.OVERLORD, UnitTypeId.ROACHBURROWED], [UnitTypeId.OVERLORD, UnitTypeId.ZERGLINGBURROWED], [UnitTypeId.BARRACKSFLYING, UnitTypeId.WIDOWMINEBURROWED], # Own units [UnitTypeId.ZEALOT, None], # Enemy units and structures, but without vision [None, UnitTypeId.ZEALOT], [None, UnitTypeId.SUPPLYDEPOT], [None, UnitTypeId.DARKTEMPLAR], [None, UnitTypeId.ROACHBURROWED], ] for i, (own_unit_type, enemy_unit_type) in enumerate(test_cases): if enemy_unit_type: await self.spawn_unit_enemy(enemy_unit_type) if own_unit_type: await self.spawn_unit(own_unit_type) # Wait for creep if enemy_unit_type == UnitTypeId.CREEPTUMOR: await self._advance_steps(1000) else: await self._advance_steps(10) result = await self.run_can_place() if result: logger.info(f"Test case successful: {i}, own unit: {own_unit_type}, enemy unit: {enemy_unit_type}") else: logger.error( f"Expected result to be True, but was False for test case: {i}, own unit: {own_unit_type}, enemy unit: {enemy_unit_type}" ) assert result, f"Expected result to be True, but was False for test case: {i}" result2 = await self.run_can_place_single() if result2: logger.info(f"Test case successful: {i}, own unit: {own_unit_type}, enemy unit: {enemy_unit_type}") else: logger.error( f"Expected result2 to be True, but was False for test case: {i}, own unit: {own_unit_type}, enemy unit: {enemy_unit_type}" ) assert result2, f"Expected result to be False, but was True for test case: {i}" await self.clear_map_center() async def test_can_place_expect_false(self): test_cases = [ # Own structures [UnitTypeId.COMMANDCENTER, None], # Enemy structures [UnitTypeId.OVERLORD, UnitTypeId.SUPPLYDEPOT], [UnitTypeId.OVERLORD, UnitTypeId.SUPPLYDEPOTLOWERED], # Visible units [UnitTypeId.OVERLORD, UnitTypeId.ZEALOT], [UnitTypeId.OVERLORD, UnitTypeId.SIEGETANKSIEGED], # Visible creep [UnitTypeId.OVERLORD, UnitTypeId.CREEPTUMOR], [UnitTypeId.OBSERVER, UnitTypeId.CREEPTUMOR], # Invisible but detected units [UnitTypeId.OBSERVER, UnitTypeId.DARKTEMPLAR], [UnitTypeId.OBSERVER, UnitTypeId.ROACHBURROWED], [UnitTypeId.OBSERVER, UnitTypeId.WIDOWMINEBURROWED], # Special cases [UnitTypeId.SIEGETANKSIEGED, None], [UnitTypeId.OVERLORD, UnitTypeId.CHANGELING], [UnitTypeId.OBSERVER, UnitTypeId.CHANGELING], # True for linux client, False for windows client: # [UnitTypeId.OVERLORD, UnitTypeId.MINERALFIELD450], # [None, UnitTypeId.MINERALFIELD450], ] for i, (own_unit_type, enemy_unit_type) in enumerate(test_cases): if own_unit_type: await self.spawn_unit(own_unit_type) if enemy_unit_type: await self.spawn_unit_enemy(enemy_unit_type) # Wait for creep if enemy_unit_type == UnitTypeId.CREEPTUMOR: await self._advance_steps(1000) else: await self._advance_steps(10) result = await self.run_can_place() if result: logger.error( f"Expected result to be False, but was True for test case: {i}, own unit: {own_unit_type}, enemy unit: {enemy_unit_type}" ) else: logger.info(f"Test case successful: {i}, own unit: {own_unit_type}, enemy unit: {enemy_unit_type}") assert not result, f"Expected result to be False, but was True for test case: {i}" await self.clear_map_center() # TODO Losing vision of a blocking enemy unit, check if can_place still returns False # for: creep, burrowed ling, burrowed roach, dark templar # TODO Check if a moving invisible unit is blocking (patroulling dark templar, patroulling burrowed roach) async def test_rally_points_with_rally_ability(self): map_center = self.game_info.map_center barracks_spawn_point = map_center.offset(Point2((10, 10))) await self.client.debug_create_unit( [(UnitTypeId.BARRACKS, 2, barracks_spawn_point, 1), (UnitTypeId.FACTORY, 2, barracks_spawn_point, 1)] ) await self._advance_steps(10) for structure in self.structures([UnitTypeId.BARRACKS, UnitTypeId.FACTORY]): structure(AbilityId.RALLY_UNITS, map_center) assert len(self.actions) == 4 filtered_actions = list(filter(self.prevent_double_actions, self.actions)) assert len(filtered_actions) == 4 await self._advance_steps(10) for structure in self.structures([UnitTypeId.BARRACKS, UnitTypeId.FACTORY]): if not structure.rally_targets: logger.error("Test case incomplete: Rally point command by using rally ability") return rally_target_point = structure.rally_targets[0].point distance = rally_target_point.distance_to_point2(map_center) assert distance < 0.1 logger.info("Test case successful: Rally point command by using rally ability") await self.clear_map_center() async def test_rally_points_with_smart_ability(self): map_center = self.game_info.map_center barracks_spawn_point = map_center.offset(Point2((10, 10))) await self.client.debug_create_unit( [(UnitTypeId.BARRACKS, 2, barracks_spawn_point, 1), (UnitTypeId.FACTORY, 2, barracks_spawn_point, 1)] ) await self._advance_steps(10) for structure in self.structures([UnitTypeId.BARRACKS, UnitTypeId.FACTORY]): structure(AbilityId.SMART, map_center) assert len(self.actions) == 4 filtered_actions = list(filter(self.prevent_double_actions, self.actions)) assert len(filtered_actions) == 4 await self._advance_steps(10) for structure in self.structures([UnitTypeId.BARRACKS, UnitTypeId.FACTORY]): if not structure.rally_targets: logger.error("Test case incomplete: Rally point command by using smart ability") sys.exit(1) rally_target_point = structure.rally_targets[0].point distance = rally_target_point.distance_to_point2(map_center) assert distance < 0.1 logger.info("Test case successful: Rally point command by using smart ability") await self.clear_map_center() # TODO: Add more examples that use constants.py "COMBINEABLE_ABILITIES" # TODO self.can_cast() class EmptyBot(BotAI): async def on_step(self, iteration: int): for unit in self.units: unit.hold_position() def main(): run_game(maps.get("Empty128"), [Bot(Race.Terran, TestBot()), Bot(Race.Zerg, EmptyBot())], realtime=False) if __name__ == "__main__": main() ================================================ FILE: test/real_time_worker_production.py ================================================ """ This bot tests if on 'realtime=True' any nexus has more than 1 probe in the queue. """ from __future__ import annotations import asyncio from loguru import logger from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race, Result from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId from sc2.main import run_game from sc2.player import Bot, Computer from sc2.unit import Unit on_end_was_called: bool = False class RealTimeTestBot(BotAI): async def on_before_start(self): mf = self.mineral_field for w in self.workers: w.gather(mf.closest_to(w)) # for nexus in self.townhalls: # nexus.train(UnitTypeId.PROBE) await self._do_actions(self.actions) self.actions.clear() await asyncio.sleep(1) async def on_start(self): """This function is run after the expansion locations and ramps are calculated.""" self.client.game_step = 1 async def on_step(self, iteration): # assert ( # self.supply_left <= 15 # ), f"Bot created 2 nexus in one step. Supply: {self.supply_used} / {self.supply_cap}" # Simulate that the bot takes too long in one iteration, sometimes if iteration % 20 != 0: await asyncio.sleep(0.1) # Queue probes for nexus in self.townhalls: nexus_orders_amount = len(nexus.orders) assert nexus_orders_amount <= 1, f"{nexus_orders_amount}" # logger.info(f"{self.time_formatted} {self.state.game_loop} {nexus} orders: {nexus_orders_amount}") if nexus.is_idle and self.can_afford(UnitTypeId.PROBE): nexus.train(UnitTypeId.PROBE) logger.info( f"{self.time_formatted} {self.state.game_loop} Training probe {self.supply_used} / {self.supply_cap}" ) # Chrono if nexus.energy >= 50: nexus(AbilityId.EFFECT_CHRONOBOOSTENERGYCOST, nexus) # Spawn nexus at expansion location that is not used made_nexus = False if self.supply_left == 0: for expansion_location in self.expansion_locations_list: if self.townhalls.closer_than(10, expansion_location): continue if self.enemy_structures.closer_than(10, expansion_location): continue await self.client.debug_create_unit([(UnitTypeId.NEXUS, 1, expansion_location, 1)]) logger.info( f"{self.time_formatted} {self.state.game_loop} Spawning a nexus {self.supply_used} / {self.supply_cap}" ) made_nexus = True continue # Spawn new pylon in map center if no more expansions are available if self.supply_left == 0 and not made_nexus: await self.client.debug_create_unit([(UnitTypeId.PYLON, 1, self.game_info.map_center, 1)]) # Don't get disturbed during this test if self.enemy_units: await self.client.debug_kill_unit(self.enemy_units) if self.supply_used >= 199 or self.time > 7 * 60: logger.info("Test successful, bot reached 199 supply without queueing two probes at once") await self.client.leave() async def on_building_construction_complete(self, unit: Unit): # Set worker rally point if unit.is_structure: unit(AbilityId.RALLY_WORKERS, self.mineral_field.closest_to(unit)) async def on_end(self, game_result: Result): global on_end_was_called on_end_was_called = True logger.info(f"on_end() was called with result: {game_result}") def main(): run_game( maps.get("AcropolisLE"), [Bot(Race.Protoss, RealTimeTestBot()), Computer(Race.Terran, Difficulty.Medium)], realtime=True, disable_fog=True, ) assert on_end_was_called, f"{on_end_was_called}" if __name__ == "__main__": main() ================================================ FILE: test/run_example_bots_vs_computer.py ================================================ """ This script makes sure to run all bots in the examples folder to check if they can launch. """ from __future__ import annotations import asyncio from importlib import import_module from loguru import logger from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race, Result from sc2.main import GameMatch, a_run_multiple_games_nokill from sc2.player import Bot, Computer # Time limit given in seconds of total in game time game_time_limit_vs_computer = 240 bot_infos = [ # Protoss { "race": Race.Protoss, "path": "examples.protoss.cannon_rush", "bot_class_name": "CannonRushBot", }, { "race": Race.Protoss, "path": "examples.protoss.find_adept_shades", "bot_class_name": "FindAdeptShadesBot", }, { "race": Race.Protoss, "path": "examples.protoss.threebase_voidray", "bot_class_name": "ThreebaseVoidrayBot", }, { "race": Race.Protoss, "path": "examples.protoss.warpgate_push", "bot_class_name": "WarpGateBot", }, # Terran { "race": Race.Terran, "path": "examples.terran.cyclone_push", "bot_class_name": "CyclonePush", }, { "race": Race.Terran, "path": "examples.terran.mass_reaper", "bot_class_name": "MassReaperBot", }, { "race": Race.Terran, "path": "examples.terran.onebase_battlecruiser", "bot_class_name": "BCRushBot", }, { "race": Race.Terran, "path": "examples.terran.proxy_rax", "bot_class_name": "ProxyRaxBot", }, { "race": Race.Terran, "path": "examples.terran.ramp_wall", "bot_class_name": "RampWallBot", }, # Zerg { "race": Race.Zerg, "path": "examples.zerg.expand_everywhere", "bot_class_name": "ExpandEverywhere", }, { "race": Race.Zerg, "path": "examples.zerg.hydralisk_push", "bot_class_name": "Hydralisk", }, { "race": Race.Zerg, "path": "examples.zerg.onebase_broodlord", "bot_class_name": "BroodlordBot", }, { "race": Race.Zerg, "path": "examples.zerg.zerg_rush", "bot_class_name": "ZergRushBot", }, # # Other { "race": Race.Protoss, "path": "examples.worker_stack_bot", "bot_class_name": "WorkerStackBot", }, { "race": Race.Zerg, "path": "examples.worker_rush", "bot_class_name": "WorkerRushBot", }, { "race": Race.Terran, "path": "examples.too_slow_bot", "bot_class_name": "SlowBot", }, { "race": Race.Terran, "path": "examples.distributed_workers", "bot_class_name": "TerranBot", }, ] matches: list[GameMatch] = [] # Run example bots for bot_info in bot_infos: bot_race: Race = bot_info["race"] # pyrefly: ignore bot_path: str = bot_info["path"] # pyrefly: ignore bot_class_name: str = bot_info["bot_class_name"] # pyrefly: ignore module = import_module(bot_path) bot_class: type[BotAI] = getattr(module, bot_class_name) limit_match_duration = game_time_limit_vs_computer if bot_class_name in {"SlowBot", "RampWallBot"}: limit_match_duration = 2 matches.append( GameMatch( map_sc2=maps.get("Acropolis"), players=[Bot(bot_race, bot_class()), Computer(Race.Protoss, Difficulty.Easy)], realtime=False, game_time_limit=limit_match_duration, ) ) async def main(): results = await a_run_multiple_games_nokill(matches) # Verify results for result, game_match in zip(results, matches): # Zergrush bot sets variable to True when on_end was called if hasattr(game_match.players[0], "on_end_called"): assert getattr(game_match.players[0], "on_end_called", False) is True assert all(v == Result.Tie for k, v in result.items()), ( f"result={result} in bot vs computer: {game_match.players[0]} in realtime={game_match.realtime}" ) logger.info("Checked all results") if __name__ == "__main__": asyncio.run(main()) ================================================ FILE: test/run_example_bots_vs_each_other.py ================================================ """ This script makes sure to run all bots in the examples folder to check if they can launch against each other. """ from __future__ import annotations import asyncio from importlib import import_module from itertools import combinations from loguru import logger from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Race, Result from sc2.main import GameMatch, a_run_multiple_games_nokill from sc2.player import Bot # Time limit given in seconds of total in game time game_time_limit_bot_vs_bot = 10 game_time_limit_bot_vs_bot_realtime = 2 bot_infos = [ # Protoss { "race": Race.Protoss, "path": "examples.protoss.cannon_rush", "bot_class_name": "CannonRushBot", }, { "race": Race.Protoss, "path": "examples.protoss.find_adept_shades", "bot_class_name": "FindAdeptShadesBot", }, { "race": Race.Protoss, "path": "examples.protoss.threebase_voidray", "bot_class_name": "ThreebaseVoidrayBot", }, { "race": Race.Protoss, "path": "examples.protoss.warpgate_push", "bot_class_name": "WarpGateBot", }, # Terran { "race": Race.Terran, "path": "examples.terran.cyclone_push", "bot_class_name": "CyclonePush", }, { "race": Race.Terran, "path": "examples.terran.mass_reaper", "bot_class_name": "MassReaperBot", }, { "race": Race.Terran, "path": "examples.terran.onebase_battlecruiser", "bot_class_name": "BCRushBot", }, { "race": Race.Terran, "path": "examples.terran.proxy_rax", "bot_class_name": "ProxyRaxBot", }, { "race": Race.Terran, "path": "examples.terran.ramp_wall", "bot_class_name": "RampWallBot", }, # Zerg { "race": Race.Zerg, "path": "examples.zerg.expand_everywhere", "bot_class_name": "ExpandEverywhere", }, { "race": Race.Zerg, "path": "examples.zerg.hydralisk_push", "bot_class_name": "Hydralisk", }, { "race": Race.Zerg, "path": "examples.zerg.onebase_broodlord", "bot_class_name": "BroodlordBot", }, { "race": Race.Zerg, "path": "examples.zerg.zerg_rush", "bot_class_name": "ZergRushBot", }, ] matches: list[GameMatch] = [] # Run bots against each other for bot_info1, bot_info2 in combinations(bot_infos, 2): bot_race1: Race = bot_info1["race"] # pyrefly: ignore bot_path: str = bot_info1["path"] # pyrefly: ignore bot_class_name: str = bot_info1["bot_class_name"] # pyrefly: ignore module = import_module(bot_path) bot_class1: type[BotAI] = getattr(module, bot_class_name) bot_race2: Race = bot_info2["race"] # pyrefly: ignore bot_path: str = bot_info2["path"] # pyrefly: ignore bot_class_name: str = bot_info2["bot_class_name"] # pyrefly: ignore module = import_module(bot_path) bot_class2: type[BotAI] = getattr(module, bot_class_name) for realtime in [True, False]: matches.append( GameMatch( map_sc2=maps.get("Acropolis"), players=[ Bot(bot_race1, bot_class1()), Bot(bot_race2, bot_class2()), ], realtime=False, game_time_limit=game_time_limit_bot_vs_bot_realtime if realtime else game_time_limit_bot_vs_bot, ) ) async def main(): results = await a_run_multiple_games_nokill(matches) # Verify results for result, game_match in zip(results, matches): # Zergrush bot sets variable to True when on_end was called if hasattr(game_match.players[0], "on_end_called"): assert getattr(game_match.players[0], "on_end_called", False) is True assert all(v == Result.Tie for k, v in result.items()), ( f"result={result} in bot vs bot: {game_match.players[0]} vs {game_match.players[1]} in realtime={game_match.realtime}" ) logger.info("Checked all results") if __name__ == "__main__": asyncio.run(main()) ================================================ FILE: test/test_directions.py ================================================ import random from math import atan2, cos, pi, sin, sqrt from sc2.position import EPSILON, Point2 P0 = Point2((0, 0)) P1 = Point2((1, 1)) P2 = Point2((3, 1)) P3 = Point2((3, 3)) def rad_diff(a, b): r1 = abs(a - b) r2 = abs(b - a) r3 = min(r1, r2) r4 = abs(2 * pi - r3) return min(r3, r4) def test_test_rad_diff(): assert rad_diff(0, 0) == 0 assert rad_diff(0, 1) == 1 assert rad_diff(0, pi) == pi assert rad_diff(pi, pi) == 0 assert rad_diff(2 * pi, 0) == 0 assert rad_diff(2 * pi + 1, 0) == 1 assert rad_diff(2 * pi + 1, 1) == 0 assert rad_diff(pi, -pi) == 0 def test_distance(): assert P0.distance_to(P1) == sqrt(2) assert P1.distance_to(P2) == 2 assert P0.distance_to(P2) == sqrt(10) def test_towards(): assert P0.towards(P1, 1) == Point2((sqrt(2) / 2, sqrt(2) / 2)) def test_random_on_distance(): random.seed(1) def get_points(source, distance, n=1000): return {source.random_on_distance(distance) for _ in range(n)} def verify_distances(source, distance, n=1000): for p in get_points(source, distance, n): assert abs(source.distance_to(p) - distance) < 0.000001 def verify_angles(source, distance, n=1000): angles_rad = {atan2(p.y - source.y, p.x - source.x) for p in get_points(source, distance, n)} quadrants = {(cos(a) < 0, sin(a) < 0) for a in angles_rad} assert len(quadrants) == 4 verify_distances(P0, 1e2) verify_distances(P1, 1e3) verify_distances(P2, 1e4) verify_angles(P0, 1e2) verify_angles(P1, 1e3) verify_angles(P2, 1e4) def test_towards_random_angle(): random.seed(1) def random_points(n=1000): def rs(): return 1 - random.random() * 2 return {Point2((rs() * 1000, rs() * 1000)) for _ in range(n)} def verify(source, target, max_difference=(pi / 4), n=1000): d = 1 + random.random() * 100 points = {source.towards_with_random_angle(target, distance=d, max_difference=max_difference) for _ in range(n)} dx, dy = target.x - source.x, target.y - source.y src_angle = atan2(dy, dx) for p in points: angle = atan2(p.y - source.y, p.x - source.x) assert rad_diff(src_angle, angle) <= max_difference assert abs(source.distance_to(p) - d) <= EPSILON verify(P0, P1) verify(P1, P2) verify(P1, P3) verify(P2, P3) verify(P1, P0) verify(P2, P1) verify(P3, P1) verify(P3, P2) ps = random_points(n=50) for p1 in ps: for p2 in ps: if p1 == p2: continue verify(p1, p2, n=10) ================================================ FILE: test/test_expiring_dict.py ================================================ from contextlib import suppress from sc2.expiring_dict import ExpiringDict def test_class(): class State: def __init__(self): self.game_loop = 0 class BotAI: def __init__(self): self.state = State() def increment(self, value=1): self.state.game_loop += value test_dict = {"hello": "its me mario", "does_this_work": "yes it works", "another_test": "yep this one also worked"} bot = BotAI() test = ExpiringDict(bot, max_age_frames=10) # pyrefly: ignore for key, value in test_dict.items(): test[key] = value bot.increment() # Test len assert len(test) == 3 # Test contains method assert "hello" in test assert "doesnt_exist" not in test # Get item result = test["hello"] assert result == "its me mario" # Get item that doesnt exist with suppress(KeyError): result = test["doesnt_exist"] assert result == test["hello"] # Set new item test["setitem"] = "test" assert len(test) == 4 # Test iteration for key, item in test.items(): assert key in test assert test[key] == item, (key, item) assert test.get(key) == item assert test.get(key, with_age=True)[0] == item # pyrefly: ignore assert test.get(key, with_age=True)[1] in {0, 1} # pyrefly: ignore c = 0 for _key in test: c += 1 assert c == 4 c = 0 for value in test.values(): c += 1 assert c == 4 # Update from another dict updater_dict = {"new_key": "my_new_value"} test.update(updater_dict) # pyrefly: ignore assert "does_this_work" in test assert "new_key" in test # Test pop method new_key = test.pop("new_key") assert new_key == "my_new_value" # Advance the frames by 10, this means all entries should now be invalid bot.increment(10) assert len(test) == 0 for _key in test: assert False for _value in test.values(): assert False for _key, _value in test.items(): assert False assert "new_key" not in test assert "setitem" not in test # len doesn't work at the moment how it should - all items in the dict are expired, so len should return 0 assert len(test) == 0, len(test) # Test repr and str function test["another_test"] = "yep this one also worked" test["setitem"] = "test" assert repr(test) == "ExpiringDict('another_test': ('yep this one also worked', 11), 'setitem': ('test', 11))" assert str(test) == "ExpiringDict('another_test': ('yep this one also worked', 11), 'setitem': ('test', 11))" if __name__ == "__main__": test_class() ================================================ FILE: test/test_pickled_data.py ================================================ """ You can execute this test running the following command from the root python-sc2 folder: uv run pytest test/test_pickled_data.py This test/script uses the pickle files located in "python-sc2/test/pickle_data" generated by "generate_pickle_files_bot.py" file, which is a bot that starts a game on each of the maps defined in the main function. It will load the pickle files, recreate the bot object from scratch and tests most of the bot properties and functions. All functions that require some kind of query or interaction with the API directly will have to be tested in the "autotest_bot.py" in a live game. """ from __future__ import annotations import lzma import math import pickle import random import unittest from contextlib import suppress from pathlib import Path from typing import Any from google.protobuf.internal import api_implementation from hypothesis import given, settings from hypothesis import strategies as st from loguru import logger from sc2.bot_ai import BotAI from sc2.client import Client from sc2.constants import ALL_GAS, CREATION_ABILITY_FIX from sc2.data import CloakState, Race from sc2.game_data import AbilityData, Cost, GameData from sc2.game_info import GameInfo from sc2.game_state import GameState from sc2.ids.ability_id import AbilityId from sc2.ids.buff_id import BuffId from sc2.ids.unit_typeid import UnitTypeId from sc2.ids.upgrade_id import UpgradeId from sc2.pixel_map import PixelMap from sc2.position import Point2, Point3, Rect, Size from sc2.unit import Unit from sc2.units import Units MAPS: list[Path] = [ map_path for map_path in (Path(__file__).parent / "pickle_data").iterdir() if map_path.suffix == ".xz" ] def load_map_pickle_data(map_path: Path) -> tuple[Any, Any, Any]: with lzma.open(str(map_path.absolute()), "rb") as f: raw_game_data, raw_game_info, raw_observation = pickle.load(f) return raw_game_data, raw_game_info, raw_observation def build_bot_object_from_pickle_data(raw_game_data, raw_game_info, raw_observation) -> BotAI: # Build fresh bot object, and load the pickled data into the bot object bot = BotAI() game_data = GameData(raw_game_data.data) game_info = GameInfo(raw_game_info.game_info) game_state = GameState(raw_observation) bot._initialize_variables() client = Client(True) # pyrefly: ignore bot._prepare_start(client=client, player_id=1, game_info=game_info, game_data=game_data) bot._prepare_step(state=game_state, proto_game_info=raw_game_info) return bot def get_map_specific_bot(map_path: Path) -> BotAI: assert map_path in MAPS data = load_map_pickle_data(map_path) return build_bot_object_from_pickle_data(*data) def test_protobuf_implementation(): """Make sure that upb is used as implementation""" assert api_implementation.Type() == "upb" def test_bot_ai(): bot: BotAI = get_map_specific_bot(random.choice(MAPS)) # Test initial bot attributes at game start # Properties from _prepare_start assert 1 <= bot.player_id <= 2 assert isinstance(bot.race, Race) assert isinstance(bot.enemy_race, Race) # Properties from _prepare_step assert bot.units.amount == bot.workers.amount assert bot.structures.amount == bot.townhalls.amount assert bot.workers.amount == 12 assert bot.townhalls.amount == 1 assert bot.gas_buildings.amount == 0 assert bot.minerals == 50 assert bot.vespene == 0 assert bot.supply_army == 0 assert bot.supply_workers == 12 assert bot.supply_cap == 15 assert bot.supply_used == 12 assert bot.supply_left == 3 assert bot.idle_worker_count == 0 assert bot.army_count == 0 # Test properties updated by "_prepare_units" function assert not bot.blips assert bot.units assert bot.structures assert not bot.enemy_units assert not bot.enemy_structures assert bot.mineral_field assert bot.vespene_geyser assert bot.resources assert len(bot.destructables) >= 0 assert isinstance(bot.destructables, (list, set, dict)) assert len(bot.watchtowers) >= 0 assert bot.all_units assert bot.workers assert bot.townhalls assert not bot.gas_buildings # Test bot_ai functions assert bot.time == 0 assert bot.time_formatted in {"0:00", "00:00"} assert bot.start_location is None # Is populated by main.py bot.game_info.player_start_location = bot.townhalls.random.position # pyrefly: ignore assert bot.townhalls.random.position not in bot.enemy_start_locations assert bot.enemy_units == Units([], bot) assert bot.enemy_structures == Units([], bot) bot.game_info.map_ramps, bot.game_info.vision_blockers = bot.game_info._find_ramps_and_vision_blockers() assert bot.main_base_ramp # Test if any ramp was found # The following functions need to be tested by autotest_bot.py because they use API query which isn't available here as this file only uses the pickle files and is not able to interact with the API as SC2 is not running while this test runs assert bot.can_feed(UnitTypeId.MARINE) assert bot.can_feed(UnitTypeId.SIEGETANK) assert not bot.can_feed(UnitTypeId.THOR) assert not bot.can_feed(UnitTypeId.BATTLECRUISER) assert not bot.can_feed(UnitTypeId.IMMORTAL) assert bot.can_afford(UnitTypeId.ZERGLING) assert bot.can_afford(UnitTypeId.MARINE) assert bot.can_afford(UnitTypeId.SCV) assert bot.can_afford(UnitTypeId.DRONE) assert bot.can_afford(UnitTypeId.PROBE) assert bot.can_afford(AbilityId.COMMANDCENTERTRAIN_SCV) assert bot.can_afford(UnitTypeId.MARINE) assert not bot.can_afford(UnitTypeId.SIEGETANK) assert not bot.can_afford(UnitTypeId.BATTLECRUISER) assert not bot.can_afford(UnitTypeId.MARAUDER) assert not bot.can_afford(UpgradeId.WARPGATERESEARCH) assert not bot.can_afford(AbilityId.RESEARCH_WARPGATE) # Store old values for minerals, vespene old_values = bot.minerals, bot.vespene, bot.supply_cap, bot.supply_left, bot.supply_used bot.vespene = 50 # pyrefly: ignore assert bot.can_afford(UpgradeId.WARPGATERESEARCH) assert bot.can_afford(AbilityId.RESEARCH_WARPGATE) bot.minerals = 150 # pyrefly: ignore bot.supply_cap = 15 # pyrefly: ignore bot.supply_left = -1 # pyrefly: ignore bot.supply_used = 16 # pyrefly: ignore # Confirm that units that don't cost supply can be built while at negative supply using can_afford function assert bot.can_afford(UnitTypeId.GATEWAY) assert bot.can_afford(UnitTypeId.PYLON) assert bot.can_afford(UnitTypeId.OVERLORD) assert bot.can_afford(UnitTypeId.BANELING) assert not bot.can_afford(UnitTypeId.ZERGLING) assert not bot.can_afford(UnitTypeId.MARINE) # pyrefly: ignore bot.minerals, bot.vespene, bot.supply_cap, bot.supply_left, bot.supply_used = old_values worker = bot.workers.random assert bot.select_build_worker(worker.position) == worker for w in bot.workers: if w == worker: continue assert bot.select_build_worker(w.position) != worker assert bot.already_pending_upgrade(UpgradeId.STIMPACK) == 0 assert bot.already_pending(UpgradeId.STIMPACK) == 0 assert bot.already_pending(UnitTypeId.SCV) == 0 assert bot.get_terrain_height(worker) > 0 assert bot.in_placement_grid(worker) assert bot.in_pathing_grid(worker) # The pickle data was created by a terran bot, so there is no creep under any worker assert not bot.has_creep(worker) # Why did this stop working, not visible on first frame? assert bot.is_visible(worker), f"Visibility value at worker is {bot.state.visibility[worker.position.rounded]}" # Check price for morphing units and upgrades cost_100 = [ AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL1, UpgradeId.TERRANSHIPWEAPONSLEVEL1, AbilityId.ARMORYRESEARCH_TERRANVEHICLEPLATINGLEVEL1, UpgradeId.TERRANVEHICLEARMORSLEVEL1, AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL1, UpgradeId.TERRANVEHICLEWEAPONSLEVEL1, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL1, UpgradeId.TERRANINFANTRYARMORSLEVEL1, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL1, UpgradeId.TERRANINFANTRYWEAPONSLEVEL1, AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL1, UpgradeId.ZERGMELEEWEAPONSLEVEL1, AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL1, UpgradeId.ZERGMISSILEWEAPONSLEVEL1, AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL1, UpgradeId.PROTOSSGROUNDARMORSLEVEL1, AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL1, UpgradeId.PROTOSSGROUNDWEAPONSLEVEL1, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL1, UpgradeId.TERRANINFANTRYWEAPONSLEVEL1, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL1, UpgradeId.TERRANINFANTRYWEAPONSLEVEL1, AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL1, UpgradeId.ZERGFLYERWEAPONSLEVEL1, AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL1, UpgradeId.PROTOSSAIRWEAPONSLEVEL1, AbilityId.RESEARCH_ZERGFLYERARMORLEVEL1, UpgradeId.ZERGFLYERARMORSLEVEL1, ] cost_175 = [ AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL2, UpgradeId.TERRANSHIPWEAPONSLEVEL2, AbilityId.ARMORYRESEARCH_TERRANVEHICLEPLATINGLEVEL2, UpgradeId.TERRANVEHICLEARMORSLEVEL2, AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL2, UpgradeId.TERRANVEHICLEWEAPONSLEVEL2, AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL2, UpgradeId.ZERGFLYERWEAPONSLEVEL2, AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL2, UpgradeId.PROTOSSAIRWEAPONSLEVEL2, AbilityId.RESEARCH_ZERGFLYERARMORLEVEL2, UpgradeId.ZERGFLYERARMORSLEVEL2, ] cost_200 = [ AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL2, UpgradeId.PROTOSSSHIELDSLEVEL2, AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL2, UpgradeId.ZERGGROUNDARMORSLEVEL2, AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL3, UpgradeId.ZERGMELEEWEAPONSLEVEL3, AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL3, UpgradeId.ZERGMISSILEWEAPONSLEVEL3, AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL3, UpgradeId.PROTOSSGROUNDARMORSLEVEL3, AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL3, UpgradeId.PROTOSSGROUNDWEAPONSLEVEL3, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL3, UpgradeId.TERRANINFANTRYARMORSLEVEL3, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL3, UpgradeId.TERRANINFANTRYWEAPONSLEVEL3, ] cost_250 = [ AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL3, UpgradeId.TERRANSHIPWEAPONSLEVEL3, AbilityId.ARMORYRESEARCH_TERRANVEHICLEPLATINGLEVEL3, UpgradeId.TERRANVEHICLEARMORSLEVEL3, AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL3, UpgradeId.TERRANVEHICLEWEAPONSLEVEL3, AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL3, UpgradeId.ZERGFLYERWEAPONSLEVEL3, AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL3, UpgradeId.PROTOSSAIRWEAPONSLEVEL3, AbilityId.RESEARCH_ZERGFLYERARMORLEVEL3, UpgradeId.ZERGFLYERARMORSLEVEL3, AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL3, UpgradeId.ZERGGROUNDARMORSLEVEL3, AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL3, UpgradeId.PROTOSSSHIELDSLEVEL3, ] cost_150 = [ AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL1, UpgradeId.ZERGGROUNDARMORSLEVEL1, AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL1, UpgradeId.PROTOSSSHIELDSLEVEL1, AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL1, UpgradeId.PROTOSSSHIELDSLEVEL1, AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL2, UpgradeId.ZERGMELEEWEAPONSLEVEL2, AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL2, UpgradeId.ZERGMISSILEWEAPONSLEVEL2, AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL2, UpgradeId.PROTOSSGROUNDARMORSLEVEL2, AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL2, UpgradeId.PROTOSSGROUNDWEAPONSLEVEL2, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL2, UpgradeId.TERRANINFANTRYARMORSLEVEL2, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL2, UpgradeId.TERRANINFANTRYWEAPONSLEVEL2, ] cost_list = [100, 175, 200, 250, 150] def calc_cost(item_id) -> Cost: if isinstance(item_id, AbilityId): return bot.game_data.calculate_ability_cost(item_id) elif isinstance(item_id, UpgradeId): return bot.game_data.upgrades[item_id.value].cost elif isinstance(item_id, UnitTypeId): creation_ability = bot.game_data.units[item_id.value].creation_ability if creation_ability is None: return Cost(0, 0) creation_ability_id = creation_ability.exact_id return bot.game_data.calculate_ability_cost(creation_ability_id) return Cost(0, 0) def assert_cost(item_id, real_cost: Cost): assert calc_cost(item_id) == real_cost, f"Cost of {item_id} should be {real_cost} but is {calc_cost(item_id)}" for items, cost in zip([cost_100, cost_175, cost_200, cost_250, cost_150], cost_list): real_cost2: Cost = Cost(cost, cost) for item in items: assert_cost(item, real_cost2) assert bot.calculate_cost(item) == real_cost2, ( f"Cost of {item} should be {real_cost2} but is {calc_cost(item)}" ) # Do not use the generic research abilities in the bot when testing if you can afford it as these are wrong assert_cost(AbilityId.RESEARCH_ZERGFLYERARMOR, Cost(0, 0)) assert_cost(AbilityId.RESEARCH_ZERGFLYERATTACK, Cost(0, 0)) assert_cost(AbilityId.RESEARCH_ZERGGROUNDARMOR, Cost(0, 0)) assert_cost(AbilityId.RESEARCH_ZERGMELEEWEAPONS, Cost(0, 0)) assert_cost(AbilityId.RESEARCH_ZERGMISSILEWEAPONS, Cost(0, 0)) assert_cost(AbilityId.RESEARCH_TERRANINFANTRYARMOR, Cost(0, 0)) assert_cost(AbilityId.RESEARCH_TERRANINFANTRYWEAPONS, Cost(0, 0)) assert_cost(AbilityId.RESEARCH_PROTOSSGROUNDARMOR, Cost(0, 0)) assert_cost(AbilityId.RESEARCH_PROTOSSGROUNDWEAPONS, Cost(0, 0)) assert_cost(AbilityId.RESEARCH_PROTOSSSHIELDS, Cost(0, 0)) assert_cost(AbilityId.RESEARCH_TERRANSHIPWEAPONS, Cost(0, 0)) assert_cost(AbilityId.RESEARCH_TERRANVEHICLEWEAPONS, Cost(0, 0)) # Somehow this is 0, returned by the API assert_cost(AbilityId.BUILD_REACTOR, Cost(0, 0)) # UnitTypeId.REACTOR has no creation ability (None) # assert_cost(UnitTypeId.REACTOR, Cost(50, 50)) assert_cost(AbilityId.BUILD_REACTOR_BARRACKS, Cost(50, 50)) assert_cost(UnitTypeId.BARRACKSREACTOR, Cost(50, 50)) assert_cost(AbilityId.UPGRADETOORBITAL_ORBITALCOMMAND, Cost(150, 0)) assert_cost(UnitTypeId.ORBITALCOMMAND, Cost(150, 0)) assert_cost(AbilityId.UPGRADETOORBITAL_ORBITALCOMMAND, Cost(150, 0)) assert bot.calculate_unit_value(UnitTypeId.ORBITALCOMMAND) == Cost(550, 0) assert bot.calculate_unit_value(UnitTypeId.RAVAGER) == Cost(100, 100) assert bot.calculate_unit_value(UnitTypeId.ARCHON) == Cost(175, 275) assert bot.calculate_unit_value(UnitTypeId.ADEPTPHASESHIFT) == Cost(0, 0) assert bot.calculate_unit_value(UnitTypeId.AUTOTURRET) == Cost(100, 0) assert bot.calculate_unit_value(UnitTypeId.INFESTORTERRAN) == Cost(0, 0) assert bot.calculate_unit_value(UnitTypeId.INFESTORTERRANBURROWED) == Cost(0, 0) assert bot.calculate_unit_value(UnitTypeId.LARVA) == Cost(0, 0) assert bot.calculate_unit_value(UnitTypeId.EGG) == Cost(0, 0) assert bot.calculate_unit_value(UnitTypeId.LOCUSTMP) == Cost(0, 0) assert bot.calculate_unit_value(UnitTypeId.LOCUSTMPFLYING) == Cost(0, 0) assert bot.calculate_unit_value(UnitTypeId.BROODLING) == Cost(0, 0) # Other and effects assert bot.calculate_unit_value(UnitTypeId.KD8CHARGE) == Cost(0, 0) assert bot.calculate_unit_value(UnitTypeId.RAVAGERCORROSIVEBILEMISSILE) == Cost(0, 0) assert bot.calculate_unit_value(UnitTypeId.VIPERACGLUESCREENDUMMY) == Cost(0, 0) assert bot.calculate_cost(UnitTypeId.BROODLORD) == Cost(150, 150) assert bot.calculate_cost(UnitTypeId.RAVAGER) == Cost(25, 75) assert bot.calculate_cost(UnitTypeId.BANELING) == Cost(25, 25) assert bot.calculate_cost(UnitTypeId.ORBITALCOMMAND) == Cost(150, 0) assert bot.calculate_cost(AbilityId.UPGRADETOORBITAL_ORBITALCOMMAND) == Cost(150, 0) assert bot.calculate_cost(UnitTypeId.REACTOR) == Cost(50, 50) assert bot.calculate_cost(UnitTypeId.TECHLAB) == Cost(50, 25) assert bot.calculate_cost(UnitTypeId.QUEEN) == Cost(150, 0) assert bot.calculate_cost(UnitTypeId.HATCHERY) == Cost(300, 0) assert bot.calculate_cost(UnitTypeId.LAIR) == Cost(150, 100) assert bot.calculate_cost(UnitTypeId.HIVE) == Cost(200, 150) assert bot.calculate_cost(UnitTypeId.DRONE) == Cost(50, 0) assert bot.calculate_cost(UnitTypeId.SCV) == Cost(50, 0) assert bot.calculate_cost(UnitTypeId.PROBE) == Cost(50, 0) assert bot.calculate_cost(UnitTypeId.SPIRE) == Cost(200, 200) assert bot.calculate_cost(UnitTypeId.ARCHON) == bot.calculate_unit_value(UnitTypeId.ARCHON) assert_cost(AbilityId.MORPHTOBROODLORD_BROODLORD, Cost(150, 150)) assert_cost(AbilityId.MORPHTORAVAGER_RAVAGER, Cost(25, 75)) assert_cost(AbilityId.MORPH_LURKER, Cost(50, 100)) assert_cost(AbilityId.MORPHZERGLINGTOBANELING_BANELING, Cost(25, 25)) assert Cost(100, 50) == 2 * Cost(50, 25) assert Cost(100, 50) == Cost(50, 25) * 2 assert Cost(50, 25) + Cost(50, 25) == Cost(50, 25) * 2 assert Cost(50, 25) + Cost(50, 25) == 2 * Cost(50, 25) assert Cost(50, 25) != Cost(50, 25) * 2 assert Cost(100, 50) - Cost(50, 25) == Cost(50, 25) assert bot.calculate_supply_cost(UnitTypeId.BARRACKS) == 0 assert bot.calculate_supply_cost(UnitTypeId.HATCHERY) == 0 assert bot.calculate_supply_cost(UnitTypeId.OVERLORD) == 0 assert bot.calculate_supply_cost(UnitTypeId.ZERGLING) == 1 assert bot.calculate_supply_cost(UnitTypeId.MARINE) == 1 assert bot.calculate_supply_cost(UnitTypeId.BANELING) == 0 assert bot.calculate_supply_cost(UnitTypeId.QUEEN) == 2 assert bot.calculate_supply_cost(UnitTypeId.ROACH) == 2 assert bot.calculate_supply_cost(UnitTypeId.RAVAGER) == 1 assert bot.calculate_supply_cost(UnitTypeId.CORRUPTOR) == 2 assert bot.calculate_supply_cost(UnitTypeId.BROODLORD) == 2 assert bot.calculate_supply_cost(UnitTypeId.HYDRALISK) == 2 assert bot.calculate_supply_cost(UnitTypeId.LURKERMP) == 1 def test_game_info(): bot: BotAI = get_map_specific_bot(random.choice(MAPS)) # Test if main base ramp works bot.game_info.map_ramps, bot.game_info.vision_blockers = bot.game_info._find_ramps_and_vision_blockers() game_info: GameInfo = bot.game_info bot.game_info.player_start_location = bot.townhalls.random.position # Test game info object assert len(game_info.players) == 2 assert game_info.map_name assert game_info.local_map_path assert game_info.map_size assert game_info.pathing_grid assert game_info.terrain_height assert game_info.placement_grid assert game_info.playable_area assert game_info.map_center assert game_info.map_ramps assert game_info.player_races assert game_info.start_locations assert game_info.player_start_location def test_game_data(): bot: BotAI = get_map_specific_bot(random.choice(MAPS)) game_data = bot.game_data assert game_data.abilities for ability_data in game_data.abilities.values(): assert ability_data.id assert ability_data.exact_id assert ability_data.friendly_name # Doesnt work for all AbilityData (may return empty string or no cost) assert isinstance(ability_data.link_name, str) assert isinstance(ability_data.button_name, str) assert isinstance(ability_data.is_free_morph, bool) assert isinstance(ability_data.cost, Cost) assert game_data.units for unit_data in game_data.units.values(): with suppress(ValueError): assert unit_data.id assert unit_data.name assert isinstance(unit_data.creation_ability, (AbilityData, type(None))) assert isinstance(unit_data.footprint_radius, (float, type(None))) # TODO Fails on newer python versions # assert isinstance(unit_data.attributes, RepeatedScalarContainer) assert isinstance(unit_data.has_minerals, bool) assert isinstance(unit_data.has_vespene, bool) assert isinstance(unit_data.cargo_size, int) assert isinstance(unit_data.tech_requirement, (UnitTypeId, type(None))) assert isinstance(unit_data.tech_alias, (list, type(None))) assert isinstance(unit_data.unit_alias, (UnitTypeId, type(None))) assert isinstance(unit_data.race, Race) assert isinstance(unit_data.cost_zerg_corrected, Cost) assert isinstance(unit_data.morph_cost, (Cost, type(None))) assert game_data.upgrades for upgrade_data in game_data.upgrades.values(): assert isinstance(upgrade_data.name, str) assert isinstance(upgrade_data.research_ability, (AbilityData, type(None))) assert isinstance(upgrade_data.cost, Cost) def test_game_state(): bot: BotAI = get_map_specific_bot(random.choice(MAPS)) state = bot.state assert not state.actions assert not state.action_errors assert not state.actions_unit_commands assert not state.actions_toggle_autocast assert not state.dead_units assert not state.alerts assert not state.player_result assert not state.chat assert state.common assert state.psionic_matrix assert state.game_loop == 0 assert state.score assert not state.upgrades assert not state.dead_units assert state.visibility assert state.creep assert not state.effects def test_pixelmap(): bot: BotAI = get_map_specific_bot(random.choice(MAPS)) pathing_grid: PixelMap = bot.game_info.pathing_grid assert pathing_grid.bits_per_pixel assert pathing_grid.bytes_per_pixel == pathing_grid.bits_per_pixel // 8 assert not pathing_grid.is_set((0, 0)) assert pathing_grid.is_empty((0, 0)) pathing_grid[Point2((0, 0))] = 123 assert pathing_grid.is_set((0, 0)) assert not pathing_grid.is_empty((0, 0)) pathing_grid.flood_fill_all(lambda i: True) pathing_grid.copy() pathing_grid.print() def test_blip(): bot: BotAI = get_map_specific_bot(random.choice(MAPS)) # TODO this needs to be done in a test bot that has a sensor tower # blips are enemy dots on the minimap that are out of vision def test_score(): bot: BotAI = get_map_specific_bot(random.choice(MAPS)) assert bot.state.score assert bot.state.score.summary def test_unit(): bot: BotAI = get_map_specific_bot(random.choice(MAPS)) scv: Unit = bot.workers.random townhall: Unit = bot.townhalls.first assert scv.name assert scv.race assert scv.tag assert not scv.is_structure assert townhall.is_structure assert scv.is_light assert not townhall.is_light assert not scv.is_armored assert townhall.is_armored assert scv.is_biological assert not townhall.is_biological assert scv.is_mechanical assert townhall.is_mechanical assert not scv.is_massive assert not townhall.is_massive assert not scv.is_psionic assert not townhall.is_psionic assert scv.tech_alias is None assert townhall.tech_alias is None assert scv.unit_alias is None assert townhall.unit_alias is None assert scv.can_attack assert not townhall.can_attack assert not scv.can_attack_both assert not townhall.can_attack_both assert scv.can_attack_ground assert not townhall.can_attack_ground assert scv.ground_dps assert not townhall.ground_dps assert scv.ground_range assert not townhall.ground_range assert not scv.can_attack_air assert not townhall.can_attack_air assert not scv.air_dps assert not townhall.air_dps assert not scv.air_range assert not townhall.air_range assert not scv.bonus_damage assert not townhall.bonus_damage assert not scv.armor assert townhall.armor assert scv.sight_range assert townhall.sight_range assert scv.movement_speed assert scv.real_speed == scv.movement_speed assert not townhall.movement_speed assert townhall.real_speed == townhall.movement_speed assert abs(scv.distance_per_step - 0.502231) < 1e-3 assert not townhall.distance_per_step assert scv.distance_to_weapon_ready == 0 assert not townhall.distance_to_weapon_ready assert not scv.is_mineral_field assert not townhall.is_mineral_field assert not scv.is_vespene_geyser assert not townhall.is_vespene_geyser assert scv.health assert townhall.health assert scv.health_max assert townhall.health_max assert scv.health_percentage assert townhall.health_percentage assert not scv.shield assert not townhall.shield assert not scv.shield_max assert not townhall.shield_max assert not scv.shield_percentage assert not townhall.shield_percentage assert scv.shield_health_percentage == 1 assert townhall.shield_health_percentage == 1 assert not scv.energy assert not townhall.energy assert not scv.energy_max assert not townhall.energy_max assert not scv.energy_percentage assert not townhall.energy_percentage assert not scv.age_in_frames assert not townhall.age_in_frames assert not scv.age assert not townhall.age assert not scv.is_memory assert not townhall.is_memory assert not scv.is_snapshot assert not townhall.is_snapshot assert scv.is_visible assert townhall.is_visible assert not scv.is_placeholder assert not townhall.is_placeholder assert scv.alliance assert townhall.alliance assert scv.is_mine assert townhall.is_mine assert not scv.is_enemy assert not townhall.is_enemy assert scv.owner_id assert townhall.owner_id assert scv.position assert townhall.position assert scv.position3d assert townhall.position3d assert scv.distance_to(townhall) assert townhall.distance_to(scv) # assert scv.facing assert townhall.facing assert scv.radius assert townhall.radius assert scv.build_progress assert townhall.build_progress assert scv.is_ready assert townhall.is_ready assert scv.cloak == CloakState.NotCloaked assert townhall.cloak == CloakState.NotCloaked assert not scv.is_cloaked assert not townhall.is_cloaked assert not scv.is_revealed assert not townhall.is_revealed assert scv.can_be_attacked assert townhall.can_be_attacked assert not scv.buffs assert not townhall.buffs assert not scv.is_carrying_minerals assert not townhall.is_carrying_minerals assert not scv.is_carrying_vespene assert not townhall.is_carrying_vespene assert not scv.is_carrying_resource assert not townhall.is_carrying_resource assert not scv.detect_range assert not townhall.detect_range assert not scv.radar_range assert not townhall.radar_range assert not scv.is_selected assert not townhall.is_selected assert scv.is_on_screen assert townhall.is_on_screen assert not scv.is_blip assert not townhall.is_blip assert not scv.is_powered assert not townhall.is_powered assert scv.is_active assert not townhall.is_active assert not scv.mineral_contents assert not townhall.mineral_contents assert not scv.vespene_contents assert not townhall.vespene_contents assert not scv.has_vespene assert not townhall.has_vespene assert not scv.is_flying assert not townhall.is_flying assert not scv.is_burrowed assert not townhall.is_burrowed assert not scv.is_hallucination assert not townhall.is_hallucination assert not scv.buff_duration_remain assert not townhall.buff_duration_remain assert not scv.buff_duration_max assert not townhall.buff_duration_max assert scv.orders assert not townhall.orders assert scv.order_target assert not townhall.order_target assert not scv.is_idle assert townhall.is_idle assert not scv.is_using_ability(AbilityId.TERRANBUILD_SUPPLYDEPOT) assert not townhall.is_using_ability(AbilityId.COMMANDCENTERTRAIN_SCV) assert not scv.is_moving assert not townhall.is_moving assert not scv.is_attacking assert not townhall.is_attacking assert not scv.is_patrolling assert not townhall.is_patrolling assert scv.is_gathering assert not townhall.is_gathering assert not scv.is_returning assert not townhall.is_returning assert scv.is_collecting assert not townhall.is_collecting assert not scv.is_constructing_scv assert not townhall.is_constructing_scv assert not scv.is_transforming assert not townhall.is_transforming assert not scv.is_repairing assert not townhall.is_repairing assert not scv.add_on_tag assert not townhall.add_on_tag assert not scv.has_add_on assert not townhall.has_add_on assert not scv.has_techlab assert not townhall.has_techlab assert not scv.has_reactor assert not townhall.has_reactor assert scv.add_on_land_position assert townhall.add_on_land_position assert scv.add_on_position assert townhall.add_on_position assert not scv.passengers assert not townhall.passengers assert not scv.passengers_tags assert not townhall.passengers_tags assert not scv.cargo_used assert not townhall.cargo_used assert not scv.has_cargo assert not townhall.has_cargo assert scv.cargo_size assert not townhall.cargo_size assert not scv.cargo_max assert not townhall.cargo_max assert not scv.cargo_left assert not townhall.cargo_left assert not scv.assigned_harvesters assert townhall.assigned_harvesters == 12 assert not scv.ideal_harvesters assert townhall.ideal_harvesters == 16 assert not scv.surplus_harvesters assert townhall.surplus_harvesters == -4 assert not scv.weapon_cooldown assert townhall.weapon_cooldown == -1 assert scv.weapon_ready assert not townhall.weapon_ready assert not scv.engaged_target_tag assert not townhall.engaged_target_tag assert not scv.is_detector assert not townhall.is_detector assert scv.distance_to_squared(townhall) assert townhall.distance_to_squared(scv) assert scv.target_in_range(townhall, bonus_distance=5) assert not townhall.target_in_range(scv, bonus_distance=5) assert not scv.has_buff(BuffId.STIMPACK) assert not townhall.has_buff(BuffId.STIMPACK) assert scv.calculate_damage_vs_target(townhall)[0] == 4 assert scv.calculate_damage_vs_target(townhall, ignore_armor=True)[0] == 5 assert townhall.calculate_damage_vs_target(scv) == (0, 0, 0) assert townhall.calculate_damage_vs_target(scv, ignore_armor=True) == (0, 0, 0) # TODO create one of each unit in the pickle tests to do damage calculations without having to create a mock class for each unit assert scv.calculate_dps_vs_target(townhall) - 2.66 < 0.01 assert scv.calculate_dps_vs_target(townhall, ignore_armor=True) - 3.33 < 0.01 assert townhall.calculate_dps_vs_target(scv) == 0 assert townhall.calculate_dps_vs_target(scv, ignore_armor=True) == 0 assert scv.is_facing(townhall, angle_error=2 * math.pi) assert not scv.is_facing(townhall) assert townhall.is_facing(scv, angle_error=2 * math.pi) assert scv.footprint_radius == 0 assert townhall.footprint_radius == 2.5 # marauder1 = Unit(marauder_proto, bot) # marauder_15_hp = Unit(marauder_proto, bot) # marauder_15_hp._proto.health = 15 # # Marauder1 should deal now 10+10vs_armored = 20 damage, but other marauder has 1 armor, so resulting damage should be 19 # assert marauder1.calculate_damage_vs_target(marauder_15_hp)[0] == 19 # assert marauder1.calculate_damage_vs_target(marauder_15_hp, ignore_armor=True)[0] == 20 # assert marauder1.calculate_damage_vs_target(marauder_15_hp, ignore_armor=True, include_overkill_damage=False)[0] == 15 # assert marauder1.calculate_damage_vs_target(marauder_15_hp, include_overkill_damage=False)[0] == 15 # # marauder1._proto.attack_upgrade_level = 2 # marauder_15_hp._proto.armor_upgrade_level = 1 # # Marauder1 should deal now 12+12vs_armored = 24 damage, but other marauder has 2 armor, so resulting damage should be 22 # assert marauder1.calculate_damage_vs_target(marauder_15_hp)[0] == 22 # assert marauder1.calculate_damage_vs_target(marauder_15_hp, ignore_armor=True)[0] == 24 # assert marauder1.calculate_damage_vs_target(marauder_15_hp, ignore_armor=True, include_overkill_damage=False)[0] == 15 # assert marauder1.calculate_damage_vs_target(marauder_15_hp, include_overkill_damage=False)[0] == 15 def test_units(): bot: BotAI = get_map_specific_bot(random.choice(MAPS)) scvs = bot.workers townhalls = bot.townhalls assert scvs.amount assert townhalls.amount assert not scvs.empty assert not townhalls.empty assert scvs.exists assert townhalls.exists assert scvs.find_by_tag(scvs.random.tag) assert not townhalls.find_by_tag(0) assert scvs.first assert townhalls.first assert scvs.take(11) assert townhalls.take(1) assert scvs.random assert townhalls.random assert scvs.random_or(1) assert townhalls.random_or(0) assert scvs.random_group_of(11) assert not scvs.random_group_of(0) assert not townhalls.random_group_of(0) # assert not scvs.in_attack_range_of(townhalls.first) # assert not townhalls.in_attack_range_of(scvs.first) assert scvs.closest_distance_to(townhalls.first) assert scvs.closest_distance_to(townhalls.first.position) assert townhalls.closest_distance_to(scvs.first) assert scvs.furthest_distance_to(townhalls.first) assert scvs.furthest_distance_to(townhalls.first.position) assert townhalls.furthest_distance_to(scvs.first) assert scvs.closest_to(townhalls.first) assert scvs.closest_to(townhalls.first.position) assert townhalls.closest_to(scvs.first) assert scvs.furthest_to(townhalls.first) assert scvs.furthest_to(townhalls.first.position) assert townhalls.furthest_to(scvs.first) assert scvs.closer_than(10, townhalls.first) assert scvs.closer_than(10, townhalls.first.position) assert townhalls.closer_than(10, scvs.first) assert scvs.further_than(0, townhalls.first) assert scvs.further_than(0, townhalls.first.position) assert townhalls.further_than(0, scvs.first) assert townhalls.in_distance_between(scvs.first, 0, 999) assert townhalls.in_distance_between(scvs.first.position, 0, 999) assert townhalls.closest_n_units(scvs.first.position, n=1) assert townhalls.furthest_n_units(scvs.first.position, n=1) assert townhalls.in_distance_of_group(scvs, 999) assert townhalls.in_closest_distance_to_group(scvs) assert townhalls.n_closest_to_distance(scvs.first.position, 0, 1) assert townhalls.n_furthest_to_distance(scvs.first.position, 0, 1) empty_units = Units([], bot_object=bot) assert not empty_units assert not empty_units.closer_than(999, townhalls.first) assert not empty_units.further_than(0, townhalls.first) assert not empty_units.in_distance_between(townhalls.first, 0, 999) assert not empty_units.closest_n_units(townhalls.first, 0) assert not empty_units.furthest_n_units(townhalls.first, 0) assert scvs.subgroup(scvs) assert townhalls.subgroup(townhalls) assert scvs.filter(pred=lambda x: x.type_id == UnitTypeId.SCV) assert not townhalls.filter(pred=lambda x: x.type_id == UnitTypeId.NEXUS) assert scvs.sorted assert townhalls.sorted assert scvs.sorted_by_distance_to(townhalls.first) assert townhalls.sorted_by_distance_to(scvs.first) assert scvs.tags_in(scvs.tags) assert not townhalls.tags_in({0, 1, 2}) assert not scvs.tags_not_in(scvs.tags) assert townhalls.tags_not_in({0, 1, 2}) assert scvs.of_type(UnitTypeId.SCV) assert scvs.of_type([UnitTypeId.SCV]) assert townhalls.of_type({UnitTypeId.COMMANDCENTER, UnitTypeId.COMMANDCENTERFLYING}) assert not scvs.exclude_type(UnitTypeId.SCV) assert townhalls.exclude_type({UnitTypeId.COMMANDCENTERFLYING}) assert not scvs.same_tech({UnitTypeId.PROBE}) assert townhalls.same_tech({UnitTypeId.ORBITALCOMMAND}) assert scvs.same_unit(UnitTypeId.SCV) assert townhalls.same_unit({UnitTypeId.COMMANDCENTERFLYING}) assert scvs.center assert townhalls.center == townhalls.first.position assert not scvs.selected assert not townhalls.selected assert scvs.tags assert townhalls.tags assert scvs.ready assert townhalls.ready assert not scvs.not_ready assert not townhalls.not_ready assert not scvs.idle assert townhalls.idle assert scvs.owned assert townhalls.owned assert not scvs.enemy assert not townhalls.enemy assert not scvs.flying assert not townhalls.flying assert scvs.not_flying assert townhalls.not_flying assert not scvs.structure assert townhalls.structure assert scvs.not_structure assert not townhalls.not_structure assert scvs.gathering assert not townhalls.gathering assert not scvs.returning assert not townhalls.returning assert scvs.collecting assert not townhalls.collecting assert scvs.visible assert townhalls.visible assert not scvs.mineral_field assert not townhalls.mineral_field assert not scvs.vespene_geyser assert not townhalls.vespene_geyser assert scvs.prefer_idle assert townhalls.prefer_idle assert len(Unit.class_cache) == 2 # Filled with CC and SCV from previous tests assert len(scvs + townhalls) == 13 assert hash(scvs + townhalls) assert scvs.copy() assert scvs.by_tag(scvs[0].tag) def test_exact_creation_ability(): try: from sc2.dicts.unit_abilities import UNIT_ABILITIES from sc2.dicts.unit_unit_alias import UNIT_UNIT_ALIAS except ImportError: logger.info("Import error: dict sc2/dicts/ are missing!") return test_case = unittest.TestCase() bot: BotAI = get_map_specific_bot(random.choice(MAPS)) ignore_types = { UnitTypeId.ADEPTPHASESHIFT, UnitTypeId.ARBITERMP, UnitTypeId.BROODLING, UnitTypeId.BYPASSARMORDRONE, UnitTypeId.CORSAIRMP, UnitTypeId.EGG, UnitTypeId.ELSECARO_COLONIST_HUT, UnitTypeId.HERC, UnitTypeId.HERCPLACEMENT, UnitTypeId.INFESTEDTERRANSEGG, UnitTypeId.LARVA, UnitTypeId.NYDUSCANALCREEPER, UnitTypeId.QUEENMP, UnitTypeId.RAVENREPAIRDRONE, UnitTypeId.REPLICANT, UnitTypeId.SCOURGEMP, UnitTypeId.SCOUTMP, UnitTypeId.WARHOUND, } unit_types = list(UNIT_UNIT_ALIAS) + list(UNIT_UNIT_ALIAS.values()) + list(UNIT_ABILITIES) + list(ALL_GAS) unit_types_unique_sorted = sorted({t.name for t in unit_types}) for unit_type_name in unit_types_unique_sorted: unit_type = UnitTypeId[unit_type_name] if unit_type in ignore_types: continue if unit_type in [ UnitTypeId.ARCHON, UnitTypeId.ASSIMILATORRICH, UnitTypeId.EXTRACTORRICH, UnitTypeId.REFINERYRICH, ]: with test_case.assertRaises(AttributeError): # pyrefly: ignore _creation_ability = bot.game_data.units[unit_type.value].creation_ability.exact_id continue try: # pyrefly: ignore _creation_ability = bot.game_data.units[unit_type.value].creation_ability.exact_id except AttributeError: if unit_type not in CREATION_ABILITY_FIX: assert False, f"Unit type '{unit_type}' missing from CREATION_ABILITY_FIX" def test_dicts(): # May be missing but that should not fail the tests try: from sc2.dicts.unit_research_abilities import RESEARCH_INFO except ImportError: logger.info("Import error: dict sc2/dicts/unit_research_abilities.py is missing!") return bot: BotAI = get_map_specific_bot(random.choice(MAPS)) for data in RESEARCH_INFO.values(): upgrade_id: UpgradeId for upgrade_id, upgrade_data in data.items(): research_ability_correct: AbilityId = upgrade_data["ability"] # pyrefly: ignore research_ability_data_from_api = bot.game_data.upgrades[upgrade_id.value].research_ability if research_ability_data_from_api is None: continue research_ability_id_from_api: AbilityId = research_ability_data_from_api.exact_id if upgrade_id.value in {116, 117, 118}: # Research abilities for armory armor plating are mapped incorrectly in the API continue if research_ability_correct.value in {807, 1284}: # Test broke on windows continue assert research_ability_correct == research_ability_id_from_api, ( f"Research abilities do not match: Correct one is {research_ability_correct} but API returned {research_ability_id_from_api}" ) @given( st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore ) @settings(max_examples=500) def test_position_pointlike(x1, y1, x2, y2, x3, y3): pos1 = Point2((x1, y1)) pos2 = Point2((x2, y2)) pos3 = Point2((x3, y3)) epsilon = 1e-3 assert pos1.position == pos1 dist = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5 assert abs(pos1.distance_to(pos2) - dist) <= epsilon assert abs(pos1.distance_to_point2(pos2) - dist) <= epsilon assert abs(pos1._distance_squared(pos2) ** 0.5 - dist) <= epsilon points = {pos2, pos3} points2 = {pos1, pos2, pos3} # All 3 points need to be different if len(points2) == 3: assert pos1.sort_by_distance(points2) == sorted(points2, key=lambda p: pos1._distance_squared(p)) assert pos1.closest(points2) == pos1 closest_point = min(points, key=lambda p: p._distance_squared(pos1)) dist_closest_point = pos1._distance_squared(closest_point) ** 0.5 furthest_point = max(points, key=lambda p: p._distance_squared(pos1)) dist_furthest_point = pos1._distance_squared(furthest_point) ** 0.5 # Distances between pos1-pos2 and pos1-pos3 might be the same, so the sorting might still be different, that's why I use a set here assert pos1.closest(points) in {p for p in points2 if abs(pos1.distance_to(p) - dist_closest_point) < epsilon} assert abs(pos1.distance_to_closest(points) - pos1._distance_squared(closest_point) ** 0.5) < epsilon assert pos1.furthest(points) in {p for p in points2 if abs(pos1.distance_to(p) - dist_furthest_point) < epsilon} assert abs(pos1.distance_to_furthest(points) - pos1._distance_squared(furthest_point) ** 0.5) < epsilon assert pos1.offset(pos2) == Point2((pos1.x + pos2.x, pos1.y + pos2.y)) if pos1 != pos2: assert pos1.unit_axes_towards(pos2) != Point2((0, 0)) if x3 > 0: temp_pos = pos1.towards(pos2, x3) if x3 <= pos1.distance_to(pos2): # Using "towards" function to go between pos1 and pos2 dist1 = pos1.distance_to(temp_pos) + pos2.distance_to(temp_pos) dist2 = pos1.distance_to(pos2) assert abs(dist1 - dist2) <= epsilon else: # Using "towards" function to go past pos2 dist1 = pos1.distance_to(pos2) + pos2.distance_to(temp_pos) dist2 = pos1.distance_to(temp_pos) assert abs(dist1 - dist2) <= epsilon elif x3 < 0: # Using "towards" function with a negative value temp_pos = pos1.towards(pos2, x3) dist1 = temp_pos.distance_to(pos1) + pos1.distance_to(pos2) dist2 = pos2.distance_to(temp_pos) assert abs(dist1 - dist2) <= epsilon assert pos1 == pos1 assert pos2 == pos2 assert pos3 == pos3 assert isinstance(hash(pos1), int) assert isinstance(hash(pos2), int) assert isinstance(hash(pos3), int) @given( st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore ) @settings(max_examples=500) def test_position_point2(x1, y1, x2, y2): pos1 = Point2((x1, y1)) pos2 = Point2((x2, y2)) assert pos1.x == x1 assert pos1.y == y1 assert pos1.to2 == pos1 assert pos1.to3 == Point3((x1, y1, 0)) length1 = (pos1.x**2 + pos1.y**2) ** 0.5 assert abs(pos1.length - length1) < 0.001 if length1: normalized1 = pos1 / length1 assert abs(pos1.normalized.is_same_as(pos1 / length1)) assert abs(normalized1.length - 1) < 0.001 length2 = (pos2.x**2 + pos2.y**2) ** 0.5 assert abs(pos2.length - length2) < 0.001 if length2: normalized2 = pos2 / length2 assert abs(pos2.normalized.is_same_as(normalized2)) assert abs(normalized2.length - 1) < 0.001 assert isinstance(pos1.distance_to(pos2), float) assert isinstance(pos1.distance_to_point2(pos2), float) if x2 > 0: assert pos1.random_on_distance(x2) != pos1 assert pos1.towards_with_random_angle(pos2, x2) != pos1 assert pos1.towards_with_random_angle(pos2) != pos1 if pos1 != pos2: dist = pos1.distance_to(pos2) intersections1 = pos1.circle_intersection(pos2, r=dist / 2) assert len(intersections1) == 1 intersections2 = pos1.circle_intersection(pos2, r=dist * 2 / 3) assert len(intersections2) == 2 neighbors4 = pos1.neighbors4 assert len(neighbors4) == 4 neighbors8 = pos1.neighbors8 assert len(neighbors8) == 8 assert pos1 + pos2 == Point2((x1 + x2, y1 + y2)) assert pos1 - pos2 == Point2((x1 - x2, y1 - y2)) assert pos1 * pos2 == Point2((x1 * x2, y1 * y2)) if 0 not in {x2, y2}: assert pos2 assert pos1 / pos2 == Point2((x1 / x2, y1 / y2)) if pos1._distance_squared(pos2) < 0.1: assert pos1.is_same_as(pos2, dist=0.1) assert pos1.unit_axes_towards(pos2) == pos1.direction_vector(pos2) @given( st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore ) @settings(max_examples=10) def test_position_point3(x1, y1, z1): pos1 = Point3((x1, y1, z1)) assert pos1.z == z1 assert pos1.to3 == pos1 @given( st.integers( min_value=-1e5, # pyrefly: ignore max_value=1e5, # pyrefly: ignore ), st.integers( min_value=-1e5, # pyrefly: ignore max_value=1e5, # pyrefly: ignore ), ) @settings(max_examples=20) def test_position_size(w, h): size = Size((w, h)) assert size.width == w assert size.height == h @given( st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore ) @settings(max_examples=20) def test_position_rect(x, y, w, h): rect = Rect((x, y, w, h)) assert rect.x == x assert rect.y == y assert rect.width == w assert rect.height == h assert rect.right == x + w assert rect.top == y + h assert rect.size == Size((w, h)) assert rect.center == Point2((rect.x + rect.width / 2, rect.y + rect.height / 2)) assert rect.offset((1, 1)) == Rect((x + 1, y + 1, w, h)) def test_missing_enum(): enum_number = 123456789 enum_converted = BuffId(enum_number) assert enum_converted == BuffId.NULL if __name__ == "__main__": test_unit() ================================================ FILE: test/test_pickled_ramp.py ================================================ """ You can execute this test running the following command from the root python-sc2 folder: uv run pytest test/test_pickled_ramp.py This test/script uses the pickle files located in "python-sc2/test/pickle_data" generated by "generate_pickle_files_bot.py" file, which is a bot that starts a game on each of the maps defined in the main function. It will load the pickle files, recreate the bot object from scratch and tests most of the bot properties and functions. All functions that require some kind of query or interaction with the API directly will have to be tested in the "autotest_bot.py" in a live game. """ import time from pathlib import Path from loguru import logger from sc2.game_info import Ramp from sc2.position import Point2 from sc2.unit import Unit from sc2.units import Units from test.test_pickled_data import MAPS, get_map_specific_bot # From https://docs.pytest.org/en/latest/example/parametrize.html#a-quick-port-of-testscenarios def pytest_generate_tests(metafunc): idlist = [] argvalues = [] argnames = [] for scenario in metafunc.cls.scenarios: idlist.append(scenario[0]) items = scenario[1].items() argnames = [x[0] for x in items] argvalues.append([x[1] for x in items]) metafunc.parametrize(argnames, argvalues, ids=idlist, scope="class") class TestClass: # Load all pickle files and convert them into bot objects from raw data (game_data, game_info, game_state) scenarios = [(map_path.name, {"map_path": map_path}) for map_path in MAPS] MAPS_WITH_ODD_EXPANSION_COUNT = {"Persephone AIE", "StargazersAIE", "Stasis LE"} def test_main_base_ramp(self, map_path: Path): bot = get_map_specific_bot(map_path) bot.game_info.map_ramps, bot.game_info.vision_blockers = bot.game_info._find_ramps_and_vision_blockers() # Test if main ramp works for all spawns for spawn in bot.game_info.start_locations + [bot.townhalls[0].position]: # Remove cached precalculated ramp if hasattr(bot, "main_base_ramp"): del bot.main_base_ramp # Set start location as one of the opponent spawns bot.game_info.player_start_location = spawn # Find main base ramp for opponent ramp: Ramp = bot.main_base_ramp assert ramp.top_center assert ramp.bottom_center assert ramp.size assert ramp.points assert ramp.upper assert ramp.lower # Test if ramp was detected far away logger.info(ramp.top_center) distance = ramp.top_center.distance_to(bot.game_info.player_start_location) assert distance < 30, ( f"Distance from spawn to main ramp was detected as {distance:.2f}, which is too far. Spawn: {spawn}, Ramp: {ramp.top_center}" ) # On the map HonorgroundsLE, the main base is large and it would take a bit of effort to fix, so it returns None or empty set if len(ramp.upper) in {2, 5}: assert ramp.upper2_for_ramp_wall # Check if terran wall was found assert ramp.barracks_correct_placement assert ramp.barracks_in_middle assert ramp.depot_in_middle assert len(ramp.corner_depots) == 2 # Check if protoss wall was found assert ramp.protoss_wall_pylon assert len(ramp.protoss_wall_buildings) == 2 assert ramp.protoss_wall_warpin else: # On maps it is unable to find valid wall positions (Honorgrounds LE) it should return None, empty sets or empty lists assert ramp.barracks_correct_placement is None assert ramp.barracks_in_middle is None assert ramp.depot_in_middle is None assert ramp.corner_depots == set() assert ramp.protoss_wall_pylon is None assert ramp.protoss_wall_buildings == frozenset() assert ramp.protoss_wall_warpin is None def test_bot_ai(self, map_path: Path): bot = get_map_specific_bot(map_path) # Recalculate and time expansion locations t0 = time.perf_counter() bot._find_expansion_locations() t1 = time.perf_counter() logger.info(f"Time to calculate expansion locations: {t1 - t0} s") # TODO: Cache all expansion positions for a map and check if it is the same # BelShirVestigeLE has only 10 bases - perhaps it should be removed since it was a WOL / HOTS map assert len(bot.expansion_locations_list) >= 10, f"Too few expansions found: {len(bot.expansion_locations_list)}" # Honorgrounds LE has 24 bases assert len(bot.expansion_locations_list) <= 24, ( f"Too many expansions found: {len(bot.expansion_locations_list)}" ) # On N player maps, it is expected that there are N*X bases because of symmetry, at least for maps designed for 1vs1 # Those maps in the list have an un-even expansion count expect_even_expansion_count = 1 if bot.game_info.map_name in self.MAPS_WITH_ODD_EXPANSION_COUNT else 0 assert ( len(bot.expansion_locations_list) % (len(bot.enemy_start_locations) + 1) == expect_even_expansion_count ), f"{bot.expansion_locations_list}" # Test if bot start location is in expansion locations assert bot.townhalls.random.position in set(bot.expansion_locations_list), ( f'This error might occur if you are running the tests locally using command "pytest test/", possibly because you are using an outdated cache.py version, but it should not occur when using docker and uv.\n{bot.townhalls.random.position}, {bot.expansion_locations_list}' ) # Test if enemy start locations are in expansion locations for location in bot.enemy_start_locations: assert location in set(bot.expansion_locations_list), f"{location}, {bot.expansion_locations_list}" # Each expansion is supposed to have at least one geysir and 6-12 minerals for expansion, resource_positions in bot.expansion_locations_dict.items(): assert isinstance(expansion, Point2) assert isinstance(resource_positions, Units) if resource_positions: assert isinstance(resource_positions[0], Unit) # 2000 Atmospheres has bases with just 4 minerals patches and a rich geysir # Neon violet has bases with just 6 resources. I think that was the back corner base with 4 minerals and 2 vespene # Odyssey has bases with 10 mineral patches and 2 geysirs # Blood boil returns 21? assert 5 <= len(resource_positions) <= 12, ( f"{len(resource_positions)} resource fields in one base on map {bot.game_info.map_name}" ) assert bot.owned_expansions == {bot.townhalls.first.position: bot.townhalls.first} ================================================ FILE: test/test_replays.py ================================================ from pathlib import Path from sc2.main import get_replay_version THIS_FOLDER = Path(__file__).parent REPLAY_PATHS = [path for path in (THIS_FOLDER / "replays").iterdir() if path.suffix == ".SC2Replay"] def test_get_replay_version(): for replay_path in REPLAY_PATHS: version = get_replay_version(replay_path) assert version == ("Base86383", "22EAC562CD0C6A31FB2C2C21E3AA3680") ================================================ FILE: test/travis_test_script.py ================================================ """ This script is made as a wrapper for sc2 bots to set a timeout to the bots (in case they can't find the last enemy structure or the game is ending in a draw) Ideally this script should be done with a bot that terminates on its own after certain things have been achieved, e.g. testing if the bot can expand at all, and then terminates after it has successfully expanded. Usage: cd into python-sc2/ directory docker build -f test/Dockerfile -t test_image . docker run test_image -c "python test/travis_test_script.py test/autotest_bot.py" Or if you want to run from windows: uv run python test/travis_test_script.py test/autotest_bot.py """ import subprocess import sys import time from loguru import logger retries = 3 # My maxout bot (reaching 200 supply in sc2) took 110 - 140 real seconds for 7 minutes in game time # How long the script should run before it will be killed: timeout_time = 8 * 60 # 8 minutes real time if len(sys.argv) > 1: # Attempt to run process with retries and timeouts t0 = time.time() process, result = None, None output_as_list = [] i = 0 for i in range(retries): t0 = time.time() process = subprocess.Popen(["python", sys.argv[1]], stdout=subprocess.PIPE) try: # Stop the current bot if the timeout was reached - the bot needs to finish a game within 3 minutes real time result = process.communicate(timeout=timeout_time) except subprocess.TimeoutExpired: continue out, err = result result = out.decode("utf-8") if process.returncode is not None and process.returncode != 0: # Bot has thrown an error, try again logger.info( f"Bot has thrown an error with error code {process.returncode}. This was try {i + 1} out of {retries}." ) continue # Break as the bot run was successful break if process is not None and process.returncode is not None and result is not None: # Reformat the output into a list linebreaks = [ ("\r\n", result.count("\r\n")), ("\r", result.count("\r")), ("\n", result.count("\n")), ] most_linebreaks_type = max(linebreaks, key=lambda x: x[1]) linebreak_type, linebreak_count = most_linebreaks_type output_as_list = result.split(linebreak_type) logger.info("Travis test script, bot output:\r\n{}\r\nEnd of bot output".format("\r\n".join(output_as_list))) time_taken = time.time() - t0 # Bot was not successfully run in time, returncode will be None if process is not None and (process.returncode is None or process.returncode != 0): logger.info( f"Exiting with exit code 5, error: Attempted to launch script {sys.argv[1]} timed out after {time_taken} seconds. Retries completed: {i}" ) sys.exit(5) # process.returncode will always return 0 if the game was run successfully or if there was a python error (in this case it returns as defeat) if process is not None and process.returncode is not None: logger.info(f"Returncode: {process.returncode}") logger.info(f"Game took {round(time.time() - t0, 1)} real time seconds") if process is not None and process.returncode == 0: for line in output_as_list: # This will throw an error even if a bot is called Traceback if "Traceback " in line: logger.info("Exiting with exit code 3") sys.exit(3) logger.info("Exiting with exit code 0") sys.exit(0) # Exit code 1: game crashed I think logger.info("Exiting with exit code 1") sys.exit(1) # Exit code 2: bot was not launched logger.info("Exiting with exit code 2") sys.exit(2) ================================================ FILE: test/upgradestest_bot.py ================================================ from __future__ import annotations from loguru import logger from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Race from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId from sc2.ids.upgrade_id import UpgradeId from sc2.main import run_game from sc2.player import Bot from sc2.position import Point2 from sc2.unit import Unit from sc2.units import Units class TestBot(BotAI): def __init__(self): BotAI.__init__(self) # The time the bot has to complete all tests, here: the number of game seconds self.game_time_timeout_limit = 20 * 60 # 20 minutes ingame time # Check how many test action functions we have # At least 4 tests because we test properties and variables self.action_tests = [ getattr(self, f"test_botai_actions{index}") for index in range(4000) if hasattr(getattr(self, f"test_botai_actions{index}", 0), "__call__") ] self.tests_done_by_name = set() # Keep track of the action index and when the last action was started self.current_action_index = 1 self.iteration_last_action_started = 8 # There will be 20 iterations of the bot doing nothing between tests self.iteration_wait_time_between_actions = 20 self.scv_action_list = ["move", "patrol", "attack", "hold", "scan_move"] # Variables for test_botai_actions11 async def on_start(self): self.client.game_step = 8 # await self.client.quick_save() await self.distribute_workers() async def on_step(self, iteration): if iteration == 0: await self.chat_send("(glhf)") # Test if chat message was sent correctly if iteration == 1: assert len(self.state.chat) >= 1, self.state.chat # Test actions if iteration == 7: for action_test in self.action_tests: await action_test() # Exit bot if iteration > 100: logger.info(f"Tests completed after {round(self.time, 1)} seconds") exit(0) async def clean_up_center(self): map_center = self.game_info.map_center # Remove everything close to map center my_units = self.units | self.structures if my_units: my_units = my_units.closer_than(20, map_center) if my_units: await self.client.debug_kill_unit(my_units) enemy_units = self.enemy_units | self.enemy_structures if enemy_units: enemy_units = enemy_units.closer_than(20, map_center) if enemy_units: await self.client.debug_kill_unit(enemy_units) await self._advance_steps(2) # Create all upgrade research structures and research each possible upgrade async def test_botai_actions1(self): map_center: Point2 = self.game_info.map_center from sc2.dicts.unit_research_abilities import RESEARCH_INFO from sc2.dicts.upgrade_researched_from import UPGRADE_RESEARCHED_FROM structure_types: list[UnitTypeId] = sorted(set(UPGRADE_RESEARCHED_FROM.values()), key=lambda data: data.name) upgrade_types: list[UpgradeId] = list(UPGRADE_RESEARCHED_FROM) # TODO if *techlab in name -> spawn rax/ fact / starport next to it addon_structures: dict[str, UnitTypeId] = { "BARRACKS": UnitTypeId.BARRACKS, "FACTORY": UnitTypeId.FACTORY, "STARPORT": UnitTypeId.STARPORT, } await self.client.debug_fast_build() structure_type: UnitTypeId for structure_type in structure_types: # TODO: techlabs if "TECHLAB" in structure_type.name: continue # pyrefly: ignore structure_upgrade_types: dict[UpgradeId, dict[str, AbilityId]] = RESEARCH_INFO[structure_type] data: dict[str, AbilityId] for upgrade_id, data in structure_upgrade_types.items(): # Collect data to spawn research_ability: AbilityId = data.get("ability", None) # pyrefly: ignore requires_power: bool = data.get("requires_power", False) # pyrefly: ignore required_building: UnitTypeId = data.get("required_building", None) # pyrefly: ignore # Prevent linux crash if ( research_ability.value not in self.game_data.abilities or upgrade_id.value not in self.game_data.upgrades or self.game_data.upgrades[upgrade_id.value].research_ability is None # pyrefly: ignore or self.game_data.upgrades[upgrade_id.value].research_ability.exact_id != research_ability ): logger.info( f"Could not find upgrade {upgrade_id} or research ability {research_ability} in self.game_data - potential version mismatch (balance upgrade - windows vs linux SC2 client" ) continue # Spawn structure and requirements spawn_structures: list[UnitTypeId] = [] if requires_power: spawn_structures.append(UnitTypeId.PYLON) spawn_structures.append(structure_type) if required_building: spawn_structures.append(required_building) await self.client.debug_create_unit([(structure, 1, map_center, 1) for structure in spawn_structures]) logger.info( f"Spawning {structure_type} to research upgrade {upgrade_id} via research ability {research_ability}" ) await self._advance_steps(2) # Wait for the structure to spawn while not self.structures(structure_type): # logger.info(f"Waiting for structure {structure_type} to spawn, structures close to center so far: {self.structures.closer_than(20, map_center)}") await self._advance_steps(2) # If cannot afford to research: cheat money while not self.can_afford(upgrade_id): # logger.info(f"Cheating money to be able to afford {upgrade_id}, cost: {self.calculate_cost(upgrade_id)}") await self.client.debug_all_resources() await self._advance_steps(2) # Research upgrade assert upgrade_id in upgrade_types, "Given upgrade is not in the list of upgrade types" assert self.structures(structure_type), f"Structure {structure_type} has not been spawned in time" # Try to research the upgrade while True: upgrader_structures: Units = self.structures(structure_type) # Upgrade has been researched, break if upgrader_structures: upgrader_structure: Unit = upgrader_structures.closest_to(map_center) if upgrader_structure.is_idle: # logger.info(f"Making {upgrader_structure} research upgrade {upgrade_id}") upgrader_structure.research(upgrade_id) await self._advance_steps(2) if upgrade_id in self.state.upgrades: break await self.clean_up_center() logger.warning("Action test 1 successful.") class EmptyBot(BotAI): async def on_start(self): if self.units: await self.client.debug_kill_unit(self.units) async def on_step(self, iteration: int): map_center = self.game_info.map_center enemies = self.enemy_units | self.enemy_structures if enemies: enemies = enemies.closer_than(20, map_center) if enemies: # If attacker is visible: move command to attacker but try to not attack for unit in self.units: unit.move(enemies.closest_to(unit).position) else: # If attacker is invisible: dont move for unit in self.units: unit.hold_position() def main(): run_game(maps.get("Empty128"), [Bot(Race.Terran, TestBot()), Bot(Race.Zerg, EmptyBot())], realtime=False) if __name__ == "__main__": main()