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
================================================
[](https://github.com/BurnySc2/python-sc2/actions)
[](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()